mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 21:50:28 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
023884d6d8 | ||
|
|
aa98547456 | ||
|
|
528343f375 | ||
|
|
7fa8b23c4e | ||
|
|
3171066f0c | ||
|
|
c1c93ea00a | ||
|
|
795dee84ad | ||
|
|
eb4575df23 | ||
|
|
f58b254aed | ||
|
|
e073c32e64 | ||
|
|
509477bafa | ||
|
|
867d373eb9 | ||
|
|
f727c5ca70 | ||
|
|
e48df7c106 | ||
|
|
395cd75038 | ||
|
|
e60a1f7e42 | ||
|
|
1fe5a73c98 | ||
|
|
37db948d2f | ||
|
|
63679405a9 | ||
|
|
156fa75c76 |
@@ -5,7 +5,7 @@ on:
|
|||||||
#release:
|
#release:
|
||||||
# types: [published]
|
# types: [published]
|
||||||
# Build only
|
# Build only
|
||||||
#push:
|
#push: it
|
||||||
# branches: [main]
|
# branches: [main]
|
||||||
# TODO - enable build on pull requests if build times can be reduced
|
# TODO - enable build on pull requests if build times can be reduced
|
||||||
# pull_request:
|
# pull_request:
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Login to Github Container Registry
|
- name: Login to Github Container Registry
|
||||||
# Only if we need to push an image
|
# Only if we need to push an image
|
||||||
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
# if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -51,11 +51,13 @@ jobs:
|
|||||||
context: ./abogen
|
context: ./abogen
|
||||||
file: ./abogen/Dockerfile
|
file: ./abogen/Dockerfile
|
||||||
# platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
# platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||||
platforms: linux/amd64,linux/arm64
|
# platforms: linux/amd64,linux/arm64
|
||||||
|
platforms: linux/amd64 # using the solution mentioned in https://github.com/denizsafak/abogen/issues/46
|
||||||
# Only push if we are publishing a release
|
# Only push if we are publishing a release
|
||||||
push: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
# push: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||||
|
push: true
|
||||||
# Use a 'temp' tag, that won't be pushed, for non-release builds
|
# Use a 'temp' tag, that won't be pushed, for non-release builds
|
||||||
tags: ${{ env.IMAGE_REPOSITORY }}:${{ github.event.release.tag_name || 'temp' }}
|
tags: ${{ env.IMAGE_REPOSITORY }}:${{ github.event.release.tag_name || 'latest' }}
|
||||||
# Use a cache to reduce build times
|
# Use a cache to reduce build times
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
|||||||
+14
-1
@@ -1,3 +1,16 @@
|
|||||||
|
# 1.1.7
|
||||||
|
- Added MPS GPU acceleration support for Silicon Mac, mentioned in https://github.com/denizsafak/abogen/issues/32#issuecomment-3155902040 by @jefro108. **Please read the [Mac](https://github.com/denizsafak/abogen?tab=readme-ov-file#mac) section in the documentation again, as it requires additional configuration.**
|
||||||
|
- Added word-by-word karaoke highlighting feature by @robmckinnon in PR #65
|
||||||
|
- Fixed sleep inhibition error occurring on some Linux systems that do not use systemd, mentioned in #67 by @hendrack
|
||||||
|
- Improvements in code and documentation.
|
||||||
|
|
||||||
|
# 1.1.6
|
||||||
|
- Improved EPUB chapter detection: Now reliably detects chapters from NAV HTML (TOC) files, even in non-standard EPUBs, fixes the issue mentioned by @jefro108 in #33
|
||||||
|
- Fixed SRT subtitle numbering issue, mentioned by @page-muncher in #41
|
||||||
|
- Fixed missing chapter contents issue in some EPUB files.
|
||||||
|
- Windows installer script now prompts the user to install the CUDA version of PyTorch even if no NVIDIA GPU is detected.
|
||||||
|
- Abogen now includes Mandarin Chinese (misaki[zh]) by default; manual installation is no longer required.
|
||||||
|
|
||||||
# 1.1.5
|
# 1.1.5
|
||||||
- Changed the temporary directory path to user's cache directory, which is more appropriate for storing cache files and avoids issues with unintended cleanup.
|
- Changed the temporary directory path to user's cache directory, which is more appropriate for storing cache files and avoids issues with unintended cleanup.
|
||||||
- Fixed the isssue where extra metadata information was not being saved to M4B files when they have no chapters, ensuring that all metadata is correctly written to the output file.
|
- Fixed the isssue where extra metadata information was not being saved to M4B files when they have no chapters, ensuring that all metadata is correctly written to the output file.
|
||||||
@@ -23,7 +36,7 @@
|
|||||||
- Added a new option: `Reset to default settings`, allowing users to reset all settings to their default values.
|
- Added a new option: `Reset to default settings`, allowing users to reset all settings to their default values.
|
||||||
- Added a new option: `Disable Kokoro's internet access`. This lets you prevent Kokoro from downloading models or voices from HuggingFace Hub, which can help avoid long waiting times if your computer is offline.
|
- Added a new option: `Disable Kokoro's internet access`. This lets you prevent Kokoro from downloading models or voices from HuggingFace Hub, which can help avoid long waiting times if your computer is offline.
|
||||||
- HuggingFace Hub telemetry is now disabled by default for improved privacy. (HuggingFace Hub is used by Kokoro to download its models)
|
- HuggingFace Hub telemetry is now disabled by default for improved privacy. (HuggingFace Hub is used by Kokoro to download its models)
|
||||||
- Potential fix for #37 and #38, where the program was becoming slow while processing large files.
|
- cPotential fix for #37 and #38, where the program was becoming slow while processing large files.
|
||||||
- Fixed `Open folder` and `Open file` buttons in the queue manager GUI.
|
- Fixed `Open folder` and `Open file` buttons in the queue manager GUI.
|
||||||
- Improvements in code structure.
|
- Improvements in code structure.
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ Abogen is a powerful text-to-speech conversion tool that makes it easy to turn e
|
|||||||
<img title="Abogen Main" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/abogen.png' width="380"> <img title="Abogen Processing" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/abogen2.png' width="380">
|
<img title="Abogen Main" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/abogen.png' width="380"> <img title="Abogen Processing" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/abogen2.png' width="380">
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
https://github.com/user-attachments/assets/cb66512d-0a52-48c3-bda4-f1e6a03fb8d6
|
|
||||||
|
https://github.com/user-attachments/assets/094ba3df-7d66-494a-bc31-0e4b41d0b865
|
||||||
|
|
||||||
> This demo was generated in just 5 seconds, producing ∼1 minute of audio with perfectly synced subtitles. To create a similar video, see [the demo guide](https://github.com/denizsafak/abogen/tree/main/demo).
|
> This demo was generated in just 5 seconds, producing ∼1 minute of audio with perfectly synced subtitles. To create a similar video, see [the demo guide](https://github.com/denizsafak/abogen/tree/main/demo).
|
||||||
|
|
||||||
@@ -60,6 +61,10 @@ source venv/bin/activate
|
|||||||
|
|
||||||
# Install abogen
|
# Install abogen
|
||||||
pip3 install abogen
|
pip3 install abogen
|
||||||
|
|
||||||
|
# For Silicon Mac (M1, M2 etc.)
|
||||||
|
# After installing abogen, we need to install Kokoro's development version which includes MPS support.
|
||||||
|
pip3 install git+https://github.com/hexgrad/kokoro.git
|
||||||
```
|
```
|
||||||
### Linux
|
### Linux
|
||||||
```bash
|
```bash
|
||||||
@@ -129,7 +134,7 @@ Here’s Abogen in action: in this demo, it processes ∼3,000 characters of tex
|
|||||||
| **Select Voice** | First letter of the language code (e.g., `a` for American English, `b` for British English, etc.), second letter is for `m` for male and `f` for female. |
|
| **Select Voice** | First letter of the language code (e.g., `a` for American English, `b` for British English, etc.), second letter is for `m` for male and `f` for female. |
|
||||||
| **Voice mixer** | Create custom voices by mixing different voice models with a profile system. See [Voice Mixer](#voice-mixer) for more details. |
|
| **Voice mixer** | Create custom voices by mixing different voice models with a profile system. See [Voice Mixer](#voice-mixer) for more details. |
|
||||||
| **Voice preview** | Listen to the selected voice before processing. |
|
| **Voice preview** | Listen to the selected voice before processing. |
|
||||||
| **Generate subtitles** | `Disabled`, `Sentence`, `Sentence + Comma`, `1 word`, `2 words`, `3 words`, etc. (Represents the number of words in each subtitle entry) |
|
| **Generate subtitles** | `Disabled`, `Sentence`, `Sentence + Comma`, `Sentence + Highlighting`, `1 word`, `2 words`, `3 words`, etc. (Represents the number of words in each subtitle entry) |
|
||||||
| **Output voice format** | `.WAV`, `.FLAC`, `.MP3`, `.OPUS (best compression)` and `M4B (with chapters)` (Special thanks to [@jborza](https://github.com/jborza) for chapter support in PR [#10](https://github.com/denizsafak/abogen/pull/10)) |
|
| **Output voice format** | `.WAV`, `.FLAC`, `.MP3`, `.OPUS (best compression)` and `M4B (with chapters)` (Special thanks to [@jborza](https://github.com/jborza) for chapter support in PR [#10](https://github.com/denizsafak/abogen/pull/10)) |
|
||||||
| **Output subtitle format** | Configures the subtitle format as `SRT (standard)`, `ASS (wide)`, `ASS (narrow)`, `ASS (centered wide)`, or `ASS (centered narrow)`. |
|
| **Output subtitle format** | Configures the subtitle format as `SRT (standard)`, `ASS (wide)`, `ASS (narrow)`, `ASS (centered wide)`, or `ASS (centered narrow)`. |
|
||||||
| **Replace single newlines with spaces** | Replaces single newlines with spaces in the text. This is useful for texts that have imaginary line breaks. |
|
| **Replace single newlines with spaces** | Replaces single newlines with spaces in the text. This is useful for texts that have imaginary line breaks. |
|
||||||
@@ -156,6 +161,8 @@ Here’s Abogen in action: in this demo, it processes ∼3,000 characters of tex
|
|||||||
| **Disable Kokoro's internet access** | Prevents Kokoro from downloading models or voices from HuggingFace Hub, useful for offline use. |
|
| **Disable Kokoro's internet access** | Prevents Kokoro from downloading models or voices from HuggingFace Hub, useful for offline use. |
|
||||||
| **Reset to default settings** | Resets all settings to their default values. |
|
| **Reset to default settings** | Resets all settings to their default values. |
|
||||||
|
|
||||||
|
> Special thanks to [@robmckinnon](https://github.com/robmckinnon) for adding Sentence + Highlighting feature in PR [#65](https://github.com/denizsafak/abogen/pull/65)
|
||||||
|
|
||||||
## `Voice Mixer`
|
## `Voice Mixer`
|
||||||
<img title="Abogen Voice Mixer" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/voice_mixer.png'>
|
<img title="Abogen Voice Mixer" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/voice_mixer.png'>
|
||||||
|
|
||||||
@@ -221,6 +228,9 @@ Similar to chapter markers, it is possible to add metadata tags for `M4B` files.
|
|||||||
```
|
```
|
||||||
For a complete list of supported languages and voices, refer to Kokoro's [VOICES.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md). To listen to sample audio outputs, see [SAMPLES.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/SAMPLES.md).
|
For a complete list of supported languages and voices, refer to Kokoro's [VOICES.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md). To listen to sample audio outputs, see [SAMPLES.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/SAMPLES.md).
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Japanese audio may require additional configuration. Please check [#56](https://github.com/denizsafak/abogen/issues/56) for more information.
|
||||||
|
|
||||||
## `MPV Config`
|
## `MPV Config`
|
||||||
I highly recommend using [MPV](https://mpv.io/installation/) to play your audio files, as it supports displaying subtitles even without a video track. Here's my `mpv.conf`:
|
I highly recommend using [MPV](https://mpv.io/installation/) to play your audio files, as it supports displaying subtitles even without a video track. Here's my `mpv.conf`:
|
||||||
```
|
```
|
||||||
|
|||||||
+23
-4
@@ -1,5 +1,5 @@
|
|||||||
@echo off
|
@echo off
|
||||||
setlocal
|
setlocal EnableDelayedExpansion
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
|
|
||||||
:: Set misaki language
|
:: Set misaki language
|
||||||
@@ -23,7 +23,6 @@ set CUDA_VERSION=128
|
|||||||
for /f "delims=: tokens=*" %%A in ('findstr /b ::: "%~f0"') do @echo(%%A
|
for /f "delims=: tokens=*" %%A in ('findstr /b ::: "%~f0"') do @echo(%%A
|
||||||
|
|
||||||
set CURRENT_DIR="%CD%"
|
set CURRENT_DIR="%CD%"
|
||||||
setlocal enabledelayedexpansion
|
|
||||||
set NAME=abogen
|
set NAME=abogen
|
||||||
set PROJECTFOLDER=abogen
|
set PROJECTFOLDER=abogen
|
||||||
set RUN=python_embedded\Scripts\abogen.exe
|
set RUN=python_embedded\Scripts\abogen.exe
|
||||||
@@ -264,7 +263,7 @@ if exist %PYPROJECT_FILE% (
|
|||||||
:: Install misaki again if MISAKI_LANG is not set to "en"
|
:: Install misaki again if MISAKI_LANG is not set to "en"
|
||||||
if "%MISAKI_LANG%" NEQ "en" (
|
if "%MISAKI_LANG%" NEQ "en" (
|
||||||
echo Configuring language pack: %MISAKI_LANG%
|
echo Configuring language pack: %MISAKI_LANG%
|
||||||
%PYTHON_CONSOLE_PATH% -m pip install misaki[lang] --upgrade --no-warn-script-location
|
%PYTHON_CONSOLE_PATH% -m pip install misaki[%MISAKI_LANG%] --upgrade --no-warn-script-location
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo Failed to install misaki language pack.
|
echo Failed to install misaki language pack.
|
||||||
pause
|
pause
|
||||||
@@ -276,6 +275,7 @@ if "%MISAKI_LANG%" NEQ "en" (
|
|||||||
for /f %%i in ('%PYTHON_CONSOLE_PATH% -c "from abogen.is_nvidia import check; print(check())"') do set IS_NVIDIA=%%i
|
for /f %%i in ('%PYTHON_CONSOLE_PATH% -c "from abogen.is_nvidia import check; print(check())"') do set IS_NVIDIA=%%i
|
||||||
|
|
||||||
:: Check if torch is installed with CUDA support
|
:: Check if torch is installed with CUDA support
|
||||||
|
echo.
|
||||||
echo Checking CUDA availability...
|
echo Checking CUDA availability...
|
||||||
if /I "%IS_NVIDIA%"=="true" (
|
if /I "%IS_NVIDIA%"=="true" (
|
||||||
for /f %%i in ('%PYTHON_CONSOLE_PATH% -c "from torch.cuda import is_available; print(is_available())"') do set cuda_available=%%i
|
for /f %%i in ('%PYTHON_CONSOLE_PATH% -c "from torch.cuda import is_available; print(is_available())"') do set cuda_available=%%i
|
||||||
@@ -293,7 +293,26 @@ if /I "%IS_NVIDIA%"=="true" (
|
|||||||
echo CUDA is available on NVIDIA GPU.
|
echo CUDA is available on NVIDIA GPU.
|
||||||
)
|
)
|
||||||
) else (
|
) else (
|
||||||
echo GPU is not NVIDIA. Skipping PyTorch CUDA installation.
|
echo.
|
||||||
|
echo Unable to detect an NVIDIA GPU in your system.
|
||||||
|
echo.
|
||||||
|
echo Do you want to install PyTorch anyway?
|
||||||
|
echo.
|
||||||
|
echo If you DO have an NVIDIA GPU, please press Y.
|
||||||
|
echo If you DO NOT have an NVIDIA GPU, please press N.
|
||||||
|
echo.
|
||||||
|
choice /C YN /M "Y=Yes, N=No"
|
||||||
|
if errorlevel 2 (
|
||||||
|
echo Skipping PyTorch installation.
|
||||||
|
) else (
|
||||||
|
echo Installing PyTorch with CUDA %CUDA_VERSION% support...
|
||||||
|
%PYTHON_CONSOLE_PATH% -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu%CUDA_VERSION% --no-warn-script-location
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Failed to install PyTorch.
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
:: Ask user if they want to create a desktop shortcut
|
:: Ask user if they want to create a desktop shortcut
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.1.5
|
1.1.7
|
||||||
+66
-55
@@ -324,14 +324,31 @@ class HandlerDialog(QDialog):
|
|||||||
f"Found NCX item via ITEM_NAVIGATION: {ncx_in_nav.get_name()}"
|
f"Found NCX item via ITEM_NAVIGATION: {ncx_in_nav.get_name()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. If still no nav_item, check for ITEM_NCX directly
|
# 3. If still no nav_item, check for NCX or fallback to NAV HTML in all ITEM_DOCUMENTs
|
||||||
if not nav_item:
|
ncx_constant = getattr(epub, "ITEM_NCX", None)
|
||||||
ncx_items = list(self.book.get_items_of_type(ebooklib.ITEM_NCX))
|
if not nav_item and ncx_constant is not None:
|
||||||
|
ncx_items = list(self.book.get_items_of_type(ncx_constant))
|
||||||
if ncx_items:
|
if ncx_items:
|
||||||
nav_item = ncx_items[0] # Take the first one
|
nav_item = ncx_items[0]
|
||||||
nav_type = "ncx"
|
nav_type = "ncx"
|
||||||
logging.info(f"Found NCX item via ITEM_NCX: {ncx_items[0].get_name()}")
|
logging.info(f"Found NCX item via ITEM_NCX: {nav_item.get_name()}")
|
||||||
|
# Fallback: search all ITEM_DOCUMENTs for a NAV HTML with <nav epub:type="toc">
|
||||||
|
if not nav_item:
|
||||||
|
for item in self.book.get_items_of_type(ebooklib.ITEM_DOCUMENT):
|
||||||
|
try:
|
||||||
|
html_content = item.get_content().decode("utf-8", errors="ignore")
|
||||||
|
if "<nav" in html_content and 'epub:type="toc"' in html_content:
|
||||||
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
|
nav_tag = soup.find("nav", attrs={"epub:type": "toc"})
|
||||||
|
if nav_tag:
|
||||||
|
nav_item = item
|
||||||
|
nav_type = "html"
|
||||||
|
logging.info(
|
||||||
|
f"Found NAV HTML with TOC in: {item.get_name()}"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
continue
|
||||||
# 4. If no navigation item found by any method, trigger fallback
|
# 4. If no navigation item found by any method, trigger fallback
|
||||||
if not nav_item or not nav_type:
|
if not nav_item or not nav_type:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
@@ -516,10 +533,14 @@ class HandlerDialog(QDialog):
|
|||||||
slice_html += self.doc_content.get(intermediate_doc_href, "")
|
slice_html += self.doc_content.get(intermediate_doc_href, "")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Fallback: if slice_html is empty, try to get the whole file's text
|
||||||
|
if not slice_html.strip() and current_doc_html:
|
||||||
|
logging.warning(
|
||||||
|
f"No content found for src '{current_src}', using full file as fallback."
|
||||||
|
)
|
||||||
|
slice_html = current_doc_html
|
||||||
if slice_html.strip():
|
if slice_html.strip():
|
||||||
slice_soup = BeautifulSoup(slice_html, "html.parser")
|
slice_soup = BeautifulSoup(slice_html, "html.parser")
|
||||||
# Add double newlines after <p> and <div> tags
|
|
||||||
for tag in slice_soup.find_all(["p", "div"]):
|
for tag in slice_soup.find_all(["p", "div"]):
|
||||||
tag.append("\n\n")
|
tag.append("\n\n")
|
||||||
for tag in slice_soup.find_all(["sup", "sub"]):
|
for tag in slice_soup.find_all(["sup", "sub"]):
|
||||||
@@ -581,6 +602,23 @@ class HandlerDialog(QDialog):
|
|||||||
f"Finished processing EPUB navigation. Found {len(self.content_texts)} content sections linked to TOC."
|
f"Finished processing EPUB navigation. Found {len(self.content_texts)} content sections linked to TOC."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _find_doc_key(self, base_href, doc_order, doc_order_decoded):
|
||||||
|
"""Find the best matching doc_key for a given base_href using robust matching."""
|
||||||
|
candidates = [
|
||||||
|
base_href,
|
||||||
|
urllib.parse.unquote(base_href),
|
||||||
|
]
|
||||||
|
base_name = os.path.basename(base_href).lower()
|
||||||
|
for k in list(doc_order.keys()) + list(doc_order_decoded.keys()):
|
||||||
|
if os.path.basename(k).lower() == base_name:
|
||||||
|
candidates.append(k)
|
||||||
|
for candidate in candidates:
|
||||||
|
if candidate in doc_order:
|
||||||
|
return candidate, doc_order[candidate]
|
||||||
|
elif candidate in doc_order_decoded:
|
||||||
|
return candidate, doc_order_decoded[candidate]
|
||||||
|
return None, None
|
||||||
|
|
||||||
def _parse_ncx_navpoint(
|
def _parse_ncx_navpoint(
|
||||||
self,
|
self,
|
||||||
nav_point,
|
nav_point,
|
||||||
@@ -603,27 +641,15 @@ class HandlerDialog(QDialog):
|
|||||||
|
|
||||||
if src:
|
if src:
|
||||||
base_href, fragment = src.split("#", 1) if "#" in src else (src, None)
|
base_href, fragment = src.split("#", 1) if "#" in src else (src, None)
|
||||||
# Try both original and decoded hrefs
|
doc_key, doc_idx = self._find_doc_key(
|
||||||
doc_key = None
|
base_href, doc_order, doc_order_decoded
|
||||||
if base_href in doc_order:
|
)
|
||||||
doc_key = base_href
|
if not doc_key:
|
||||||
doc_idx = doc_order[base_href]
|
|
||||||
elif urllib.parse.unquote(base_href) in doc_order:
|
|
||||||
doc_key = urllib.parse.unquote(base_href)
|
|
||||||
doc_idx = doc_order[doc_key]
|
|
||||||
elif base_href in doc_order_decoded:
|
|
||||||
doc_key = base_href
|
|
||||||
doc_idx = doc_order_decoded[base_href]
|
|
||||||
elif urllib.parse.unquote(base_href) in doc_order_decoded:
|
|
||||||
doc_key = urllib.parse.unquote(base_href)
|
|
||||||
doc_idx = doc_order_decoded[doc_key]
|
|
||||||
else:
|
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Navigation entry '{title}' points to '{base_href}', which is not in the spine or document list."
|
f"Navigation entry '{title}' points to '{base_href}', which is not in the spine or document list (even after basename fallback)."
|
||||||
)
|
)
|
||||||
current_entry_node["has_content"] = False
|
current_entry_node["has_content"] = False
|
||||||
doc_key = None
|
else:
|
||||||
if doc_key is not None:
|
|
||||||
position = find_position_func(doc_key, fragment)
|
position = find_position_func(doc_key, fragment)
|
||||||
entry_data = {
|
entry_data = {
|
||||||
"src": src,
|
"src": src,
|
||||||
@@ -698,28 +724,15 @@ class HandlerDialog(QDialog):
|
|||||||
current_entry_node["title"] = title
|
current_entry_node["title"] = title
|
||||||
current_entry_node["src"] = src
|
current_entry_node["src"] = src
|
||||||
|
|
||||||
|
doc_key = None
|
||||||
|
doc_idx = None
|
||||||
|
position = 0
|
||||||
|
fragment = None
|
||||||
if src:
|
if src:
|
||||||
base_href, fragment = src.split("#", 1) if "#" in src else (src, None)
|
base_href, fragment = src.split("#", 1) if "#" in src else (src, None)
|
||||||
# Try both original and decoded hrefs
|
doc_key, doc_idx = self._find_doc_key(
|
||||||
doc_key = None
|
base_href, doc_order, doc_order_decoded
|
||||||
if base_href in doc_order:
|
)
|
||||||
doc_key = base_href
|
|
||||||
doc_idx = doc_order[base_href]
|
|
||||||
elif urllib.parse.unquote(base_href) in doc_order:
|
|
||||||
doc_key = urllib.parse.unquote(base_href)
|
|
||||||
doc_idx = doc_order[doc_key]
|
|
||||||
elif base_href in doc_order_decoded:
|
|
||||||
doc_key = base_href
|
|
||||||
doc_idx = doc_order_decoded[base_href]
|
|
||||||
elif urllib.parse.unquote(base_href) in doc_order_decoded:
|
|
||||||
doc_key = urllib.parse.unquote(base_href)
|
|
||||||
doc_idx = doc_order_decoded[doc_key]
|
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
f"Navigation entry '{title}' points to '{base_href}', which is not in the spine or document list."
|
|
||||||
)
|
|
||||||
current_entry_node["has_content"] = False
|
|
||||||
doc_key = None
|
|
||||||
if doc_key is not None:
|
if doc_key is not None:
|
||||||
position = find_position_func(doc_key, fragment)
|
position = find_position_func(doc_key, fragment)
|
||||||
entry_data = {
|
entry_data = {
|
||||||
@@ -731,13 +744,16 @@ class HandlerDialog(QDialog):
|
|||||||
}
|
}
|
||||||
ordered_entries.append(entry_data)
|
ordered_entries.append(entry_data)
|
||||||
current_entry_node["has_content"] = True
|
current_entry_node["has_content"] = True
|
||||||
|
else:
|
||||||
|
logging.warning(
|
||||||
|
f"Navigation entry '{title}' points to '{base_href}', which is not in the spine or document list (even after basename fallback)."
|
||||||
|
)
|
||||||
|
current_entry_node["has_content"] = False
|
||||||
else:
|
else:
|
||||||
current_entry_node["has_content"] = False
|
current_entry_node["has_content"] = False
|
||||||
|
|
||||||
child_ol = li_element.find("ol", recursive=False)
|
for child_ol in li_element.find_all("ol", recursive=False):
|
||||||
if child_ol:
|
|
||||||
for child_li in child_ol.find_all("li", recursive=False):
|
for child_li in child_ol.find_all("li", recursive=False):
|
||||||
# Pass find_position_func down recursively
|
|
||||||
self._parse_html_nav_li(
|
self._parse_html_nav_li(
|
||||||
child_li,
|
child_li,
|
||||||
ordered_entries,
|
ordered_entries,
|
||||||
@@ -746,12 +762,7 @@ class HandlerDialog(QDialog):
|
|||||||
current_entry_node["children"],
|
current_entry_node["children"],
|
||||||
find_position_func,
|
find_position_func,
|
||||||
)
|
)
|
||||||
|
tree_structure_list.append(current_entry_node)
|
||||||
if title and (
|
|
||||||
current_entry_node.get("has_content", False)
|
|
||||||
or current_entry_node["children"]
|
|
||||||
):
|
|
||||||
tree_structure_list.append(current_entry_node)
|
|
||||||
|
|
||||||
def _find_position_robust(self, doc_href, fragment_id):
|
def _find_position_robust(self, doc_href, fragment_id):
|
||||||
if doc_href not in self.doc_content:
|
if doc_href not in self.doc_content:
|
||||||
|
|||||||
+111
-18
@@ -23,6 +23,7 @@ import abogen.hf_tracker as hf_tracker
|
|||||||
import static_ffmpeg
|
import static_ffmpeg
|
||||||
import threading # for efficient waiting
|
import threading # for efficient waiting
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import platform
|
||||||
|
|
||||||
|
|
||||||
def get_sample_voice_text(lang_code):
|
def get_sample_voice_text(lang_code):
|
||||||
@@ -310,8 +311,15 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
self.log_updated.emit("\nInitializing TTS pipeline...")
|
self.log_updated.emit("\nInitializing TTS pipeline...")
|
||||||
|
|
||||||
# Set device based on use_gpu setting
|
# Set device based on use_gpu setting and platform
|
||||||
device = "cuda" if self.use_gpu else "cpu"
|
if self.use_gpu:
|
||||||
|
if platform.system() == "Darwin" and platform.processor() == "arm":
|
||||||
|
device = "mps" # Use MPS for Apple Silicon
|
||||||
|
else:
|
||||||
|
device = "cuda" # Use CUDA for other platforms
|
||||||
|
else:
|
||||||
|
device = "cpu"
|
||||||
|
|
||||||
tts = self.KPipeline(
|
tts = self.KPipeline(
|
||||||
lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device
|
lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device
|
||||||
)
|
)
|
||||||
@@ -434,8 +442,8 @@ class ConversionThread(QThread):
|
|||||||
)
|
)
|
||||||
# Only check for files with allowed extensions (extension without dot, case-insensitive)
|
# Only check for files with allowed extensions (extension without dot, case-insensitive)
|
||||||
clash = any(
|
clash = any(
|
||||||
os.path.splitext(fname)[0] == f"{base_name}{suffix}" and
|
os.path.splitext(fname)[0] == f"{base_name}{suffix}"
|
||||||
os.path.splitext(fname)[1][1:].lower() in allowed_exts
|
and os.path.splitext(fname)[1][1:].lower() in allowed_exts
|
||||||
for fname in os.listdir(parent_dir)
|
for fname in os.listdir(parent_dir)
|
||||||
)
|
)
|
||||||
if not os.path.exists(chapters_out_dir_candidate) and not clash:
|
if not os.path.exists(chapters_out_dir_candidate) and not clash:
|
||||||
@@ -467,6 +475,8 @@ class ConversionThread(QThread):
|
|||||||
{"chapter": chapter[0], "start": 0.0, "end": 0.0}
|
{"chapter": chapter[0], "start": 0.0, "end": 0.0}
|
||||||
for chapter in chapters
|
for chapter in chapters
|
||||||
]
|
]
|
||||||
|
# SRT numbering fix: use a global counter
|
||||||
|
merged_srt_index = 1 # SRT numbering for merged file
|
||||||
# Prepare output file/ffmpeg process for merged output
|
# Prepare output file/ffmpeg process for merged output
|
||||||
if self.output_format in ["wav", "mp3", "flac"]:
|
if self.output_format in ["wav", "mp3", "flac"]:
|
||||||
merged_out_file = sf.SoundFile(
|
merged_out_file = sf.SoundFile(
|
||||||
@@ -483,8 +493,8 @@ class ConversionThread(QThread):
|
|||||||
merged_out_file = None
|
merged_out_file = None
|
||||||
ffmpeg_proc = None
|
ffmpeg_proc = None
|
||||||
metadata_options = (
|
metadata_options = (
|
||||||
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
|
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
|
||||||
)
|
)
|
||||||
# Prepare ffmpeg command for m4b output
|
# Prepare ffmpeg command for m4b output
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
@@ -556,6 +566,11 @@ class ConversionThread(QThread):
|
|||||||
merged_subtitle_file.write("[Script Info]\n")
|
merged_subtitle_file.write("[Script Info]\n")
|
||||||
merged_subtitle_file.write("Title: Generated by Abogen\n")
|
merged_subtitle_file.write("Title: Generated by Abogen\n")
|
||||||
merged_subtitle_file.write("ScriptType: v4.00+\n\n")
|
merged_subtitle_file.write("ScriptType: v4.00+\n\n")
|
||||||
|
# Add style definitions for karaoke highlighting
|
||||||
|
if self.subtitle_mode == "Sentence + Highlighting":
|
||||||
|
merged_subtitle_file.write("[V4+ Styles]\n")
|
||||||
|
merged_subtitle_file.write("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n")
|
||||||
|
merged_subtitle_file.write("Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n")
|
||||||
merged_subtitle_file.write("[Events]\n")
|
merged_subtitle_file.write("[Events]\n")
|
||||||
merged_subtitle_file.write(
|
merged_subtitle_file.write(
|
||||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
||||||
@@ -599,6 +614,7 @@ class ConversionThread(QThread):
|
|||||||
{"chapter": chapter[0], "start": 0.0, "end": 0.0}
|
{"chapter": chapter[0], "start": 0.0, "end": 0.0}
|
||||||
for chapter in chapters
|
for chapter in chapters
|
||||||
]
|
]
|
||||||
|
srt_index = 1 # SRT numbering fix for chapter-only mode
|
||||||
# Instead of processing the whole text, process by chapter
|
# Instead of processing the whole text, process by chapter
|
||||||
for chapter_idx, (chapter_name, chapter_text) in enumerate(chapters, 1):
|
for chapter_idx, (chapter_name, chapter_text) in enumerate(chapters, 1):
|
||||||
chapter_out_path = None
|
chapter_out_path = None
|
||||||
@@ -677,6 +693,9 @@ class ConversionThread(QThread):
|
|||||||
continue
|
continue
|
||||||
# Open chapter subtitle file for incremental writing if needed
|
# Open chapter subtitle file for incremental writing if needed
|
||||||
chapter_subtitle_file = None
|
chapter_subtitle_file = None
|
||||||
|
chapter_srt_index = (
|
||||||
|
1 # Initialize SRT numbering for this chapter file
|
||||||
|
)
|
||||||
if self.subtitle_mode != "Disabled":
|
if self.subtitle_mode != "Disabled":
|
||||||
subtitle_format = getattr(self, "subtitle_format", "srt")
|
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||||
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||||
@@ -694,6 +713,13 @@ class ConversionThread(QThread):
|
|||||||
chapter_subtitle_file.write("[Script Info]\n")
|
chapter_subtitle_file.write("[Script Info]\n")
|
||||||
chapter_subtitle_file.write("Title: Generated by Abogen\n")
|
chapter_subtitle_file.write("Title: Generated by Abogen\n")
|
||||||
chapter_subtitle_file.write("ScriptType: v4.00+\n\n")
|
chapter_subtitle_file.write("ScriptType: v4.00+\n\n")
|
||||||
|
|
||||||
|
# Add style definitions for karaoke highlighting
|
||||||
|
if self.subtitle_mode == "Sentence + Highlighting":
|
||||||
|
chapter_subtitle_file.write("[V4+ Styles]\n")
|
||||||
|
chapter_subtitle_file.write("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n")
|
||||||
|
chapter_subtitle_file.write("Style: Default,Arial,24,&H00FFFFFF,&H00808080,&H00000000,&H00404040,0,0,0,0,100,100,0,0,3,2,0,5,10,10,10,1\n\n")
|
||||||
|
|
||||||
chapter_subtitle_file.write("[Events]\n")
|
chapter_subtitle_file.write("[Events]\n")
|
||||||
chapter_subtitle_file.write(
|
chapter_subtitle_file.write(
|
||||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
||||||
@@ -817,16 +843,18 @@ class ConversionThread(QThread):
|
|||||||
for start, end, text in new_entries:
|
for start, end, text in new_entries:
|
||||||
start_time = self._ass_time(start)
|
start_time = self._ass_time(start)
|
||||||
end_time = self._ass_time(end)
|
end_time = self._ass_time(end)
|
||||||
|
# Use karaoke effect for highlighting mode
|
||||||
|
effect = "karaoke" if self.subtitle_mode == "Sentence + Highlighting" else ""
|
||||||
merged_subtitle_file.write(
|
merged_subtitle_file.write(
|
||||||
f"Dialogue: 0,{start_time},{end_time},Default,,{merged_subtitle_margin},{merged_subtitle_margin},0,,{merged_subtitle_alignment_tag}{text}\n"
|
f"Dialogue: 0,{start_time},{end_time},Default,,{merged_subtitle_margin},{merged_subtitle_margin},0,{effect},{merged_subtitle_alignment_tag}{text}\n"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for i, (start, end, text) in enumerate(
|
for entry in new_entries:
|
||||||
new_entries, 1
|
start, end, text = entry
|
||||||
):
|
|
||||||
merged_subtitle_file.write(
|
merged_subtitle_file.write(
|
||||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
f"{merged_srt_index}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||||
)
|
)
|
||||||
|
merged_srt_index += 1
|
||||||
# Per-chapter subtitle processing for both file and ffmpeg_proc
|
# Per-chapter subtitle processing for both file and ffmpeg_proc
|
||||||
if chapter_out_file or chapter_ffmpeg_proc:
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
new_chapter_entries = []
|
new_chapter_entries = []
|
||||||
@@ -844,16 +872,18 @@ class ConversionThread(QThread):
|
|||||||
for start, end, text in new_chapter_entries:
|
for start, end, text in new_chapter_entries:
|
||||||
start_time = self._ass_time(start)
|
start_time = self._ass_time(start)
|
||||||
end_time = self._ass_time(end)
|
end_time = self._ass_time(end)
|
||||||
|
# Use karaoke effect for highlighting mode
|
||||||
|
effect = "karaoke" if self.subtitle_mode == "Sentence + Highlighting" else ""
|
||||||
chapter_subtitle_file.write(
|
chapter_subtitle_file.write(
|
||||||
f"Dialogue: 0,{start_time},{end_time},Default,,{chapter_subtitle_margin},{chapter_subtitle_margin},0,,{chapter_subtitle_alignment_tag}{text}\n"
|
f"Dialogue: 0,{start_time},{end_time},Default,,{chapter_subtitle_margin},{chapter_subtitle_margin},0,{effect},{chapter_subtitle_alignment_tag}{text}\n"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for i, (start, end, text) in enumerate(
|
for entry in new_chapter_entries:
|
||||||
new_chapter_entries, 1
|
start, end, text = entry
|
||||||
):
|
|
||||||
chapter_subtitle_file.write(
|
chapter_subtitle_file.write(
|
||||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
f"{chapter_srt_index}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||||
)
|
)
|
||||||
|
chapter_srt_index += 1
|
||||||
if merge_chapters_at_end:
|
if merge_chapters_at_end:
|
||||||
current_time += chunk_dur
|
current_time += chunk_dur
|
||||||
if chapter_out_file or chapter_ffmpeg_proc:
|
if chapter_out_file or chapter_ffmpeg_proc:
|
||||||
@@ -1146,7 +1176,61 @@ class ConversionThread(QThread):
|
|||||||
if not tokens_with_timestamps:
|
if not tokens_with_timestamps:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.subtitle_mode == "Sentence" or self.subtitle_mode == "Sentence + Comma":
|
if self.subtitle_mode == "Sentence + Highlighting":
|
||||||
|
# Sentence-based processing with karaoke highlighting
|
||||||
|
separator = r"[.!?]"
|
||||||
|
current_sentence = []
|
||||||
|
word_count = 0
|
||||||
|
|
||||||
|
for token in tokens_with_timestamps:
|
||||||
|
current_sentence.append(token)
|
||||||
|
word_count += 1
|
||||||
|
|
||||||
|
# Split sentences based on separator or word count
|
||||||
|
if (
|
||||||
|
re.search(separator, token["text"]) and token["whitespace"] == " "
|
||||||
|
) or word_count >= max_subtitle_words:
|
||||||
|
if current_sentence:
|
||||||
|
# Create karaoke subtitle entry for this sentence
|
||||||
|
start_time = current_sentence[0]["start"]
|
||||||
|
end_time = current_sentence[-1]["end"]
|
||||||
|
|
||||||
|
# Generate karaoke text with background highlighting
|
||||||
|
karaoke_text = ""
|
||||||
|
for t in current_sentence:
|
||||||
|
# Calculate duration in centiseconds
|
||||||
|
duration = t["end"] - t["start"] if t["end"] and t["start"] else 0.5
|
||||||
|
duration_cs = int(duration * 100)
|
||||||
|
# Add karaoke effect - relies on style's SecondaryColour for highlighting
|
||||||
|
karaoke_text += f"{{\\kf{duration_cs}}}{t['text']}{t.get('whitespace', '') or ''}"
|
||||||
|
|
||||||
|
subtitle_entries.append(
|
||||||
|
(start_time, end_time, karaoke_text.strip())
|
||||||
|
)
|
||||||
|
current_sentence = []
|
||||||
|
word_count = 0
|
||||||
|
|
||||||
|
# Add any remaining tokens as a sentence
|
||||||
|
if current_sentence:
|
||||||
|
start_time = current_sentence[0]["start"]
|
||||||
|
end_time = current_sentence[-1]["end"]
|
||||||
|
|
||||||
|
# Generate karaoke text for remaining tokens
|
||||||
|
karaoke_text = ""
|
||||||
|
for t in current_sentence:
|
||||||
|
duration = t["end"] - t["start"] if t["end"] and t["start"] else 0.5
|
||||||
|
duration_cs = int(duration * 100)
|
||||||
|
karaoke_text += f"{{\\kf{duration_cs}}}{t['text']}{t.get('whitespace', '') or ''}"
|
||||||
|
subtitle_entries.append((start_time, end_time, karaoke_text.strip()))
|
||||||
|
|
||||||
|
# Fallback for last entry
|
||||||
|
if subtitle_entries and fallback_end_time is not None:
|
||||||
|
last_entry = subtitle_entries[-1]
|
||||||
|
start, end, text = last_entry
|
||||||
|
if end is None or end <= start or end <= 0:
|
||||||
|
subtitle_entries[-1] = (start, fallback_end_time, text)
|
||||||
|
|
||||||
|
elif self.subtitle_mode == "Sentence" or self.subtitle_mode == "Sentence + Comma":
|
||||||
# Define separator pattern based on mode
|
# Define separator pattern based on mode
|
||||||
separator = r"[.!?]" if self.subtitle_mode == "Sentence" else r"[.!?,]"
|
separator = r"[.!?]" if self.subtitle_mode == "Sentence" else r"[.!?,]"
|
||||||
current_sentence = []
|
current_sentence = []
|
||||||
@@ -1313,7 +1397,16 @@ class VoicePreviewThread(QThread):
|
|||||||
|
|
||||||
# Generate the preview and save to cache
|
# Generate the preview and save to cache
|
||||||
try:
|
try:
|
||||||
device = "cuda" if self.use_gpu else "cpu"
|
|
||||||
|
# Set device based on use_gpu setting and platform
|
||||||
|
if self.use_gpu:
|
||||||
|
if platform.system() == "Darwin" and platform.processor() == "arm":
|
||||||
|
device = "mps" # Use MPS for Apple Silicon
|
||||||
|
else:
|
||||||
|
device = "cuda" # Use CUDA for other platforms
|
||||||
|
else:
|
||||||
|
device = "cpu"
|
||||||
|
|
||||||
tts = self.kpipeline_class(
|
tts = self.kpipeline_class(
|
||||||
lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device
|
lang_code=self.lang_code, repo_id="hexgrad/Kokoro-82M", device=device
|
||||||
)
|
)
|
||||||
|
|||||||
+45
-1
@@ -895,6 +895,7 @@ class abogen(QWidget):
|
|||||||
"Disabled: No subtitles will be generated.\n"
|
"Disabled: No subtitles will be generated.\n"
|
||||||
"Sentence: Subtitles will be generated for each sentence.\n"
|
"Sentence: Subtitles will be generated for each sentence.\n"
|
||||||
"Sentence + Comma: Subtitles will be generated for each sentence and comma.\n"
|
"Sentence + Comma: Subtitles will be generated for each sentence and comma.\n"
|
||||||
|
"Sentence + Highlighting: Subtitles with word-by-word karaoke highlighting.\n"
|
||||||
"1+ word: Subtitles will be generated for each word(s).\n\n"
|
"1+ word: Subtitles will be generated for each word(s).\n\n"
|
||||||
"Supported languages for subtitle generation:\n"
|
"Supported languages for subtitle generation:\n"
|
||||||
+ "\n".join(
|
+ "\n".join(
|
||||||
@@ -902,7 +903,7 @@ class abogen(QWidget):
|
|||||||
for lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
|
for lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
subtitle_options = ["Disabled", "Sentence", "Sentence + Comma"] + [
|
subtitle_options = ["Disabled", "Sentence", "Sentence + Comma", "Sentence + Highlighting"] + [
|
||||||
f"{i} word" if i == 1 else f"{i} words" for i in range(1, 11)
|
f"{i} word" if i == 1 else f"{i} words" for i in range(1, 11)
|
||||||
]
|
]
|
||||||
self.subtitle_combo.addItems(subtitle_options)
|
self.subtitle_combo.addItems(subtitle_options)
|
||||||
@@ -970,6 +971,25 @@ class abogen(QWidget):
|
|||||||
lambda i: self.set_subtitle_format(self.subtitle_format_combo.itemData(i))
|
lambda i: self.set_subtitle_format(self.subtitle_format_combo.itemData(i))
|
||||||
)
|
)
|
||||||
subtitle_format_layout.addWidget(self.subtitle_format_combo)
|
subtitle_format_layout.addWidget(self.subtitle_format_combo)
|
||||||
|
# If subtitle mode requires highlighting, SRT is not supported. Disable SRT item
|
||||||
|
# and auto-switch to a compatible ASS format if SRT is currently selected.
|
||||||
|
try:
|
||||||
|
if hasattr(self, "subtitle_mode") and self.subtitle_mode == "Sentence + Highlighting":
|
||||||
|
idx_srt = self.subtitle_format_combo.findData("srt")
|
||||||
|
if idx_srt >= 0:
|
||||||
|
item = self.subtitle_format_combo.model().item(idx_srt)
|
||||||
|
if item is not None:
|
||||||
|
item.setEnabled(False)
|
||||||
|
# If current selection is SRT, switch to centered narrow ASS
|
||||||
|
if self.subtitle_format_combo.currentData() == "srt":
|
||||||
|
new_idx = self.subtitle_format_combo.findData("ass_centered_narrow")
|
||||||
|
if new_idx >= 0:
|
||||||
|
self.subtitle_format_combo.setCurrentIndex(new_idx)
|
||||||
|
# Persist the change
|
||||||
|
self.set_subtitle_format(self.subtitle_format_combo.itemData(new_idx))
|
||||||
|
except Exception:
|
||||||
|
# Fail-safe: don't crash UI if model manipulation isn't supported on some platforms
|
||||||
|
pass
|
||||||
controls_layout.addLayout(subtitle_format_layout)
|
controls_layout.addLayout(subtitle_format_layout)
|
||||||
|
|
||||||
# Replace single newlines dropdown (acts like checkbox)
|
# Replace single newlines dropdown (acts like checkbox)
|
||||||
@@ -2471,6 +2491,30 @@ class abogen(QWidget):
|
|||||||
self.subtitle_format_combo.setEnabled(False)
|
self.subtitle_format_combo.setEnabled(False)
|
||||||
else:
|
else:
|
||||||
self.subtitle_format_combo.setEnabled(True)
|
self.subtitle_format_combo.setEnabled(True)
|
||||||
|
# If highlighting mode selected, SRT is not supported. Disable SRT option and
|
||||||
|
# switch away from it if currently selected.
|
||||||
|
try:
|
||||||
|
idx_srt = self.subtitle_format_combo.findData("srt")
|
||||||
|
if mode == "Sentence + Highlighting":
|
||||||
|
if idx_srt >= 0:
|
||||||
|
item = self.subtitle_format_combo.model().item(idx_srt)
|
||||||
|
if item is not None:
|
||||||
|
item.setEnabled(False)
|
||||||
|
# If current format is SRT, switch to a compatible ASS format
|
||||||
|
if self.subtitle_format_combo.currentData() == "srt":
|
||||||
|
new_idx = self.subtitle_format_combo.findData("ass_centered_narrow")
|
||||||
|
if new_idx >= 0:
|
||||||
|
self.subtitle_format_combo.setCurrentIndex(new_idx)
|
||||||
|
self.set_subtitle_format(self.subtitle_format_combo.itemData(new_idx))
|
||||||
|
else:
|
||||||
|
# Re-enable SRT option when not in highlighting mode
|
||||||
|
if idx_srt >= 0:
|
||||||
|
item = self.subtitle_format_combo.model().item(idx_srt)
|
||||||
|
if item is not None:
|
||||||
|
item.setEnabled(True)
|
||||||
|
except Exception:
|
||||||
|
# Ignore errors interacting with model (defensive)
|
||||||
|
pass
|
||||||
|
|
||||||
def on_format_changed(self, fmt):
|
def on_format_changed(self, fmt):
|
||||||
self.selected_format = fmt
|
self.selected_format = fmt
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ os.environ["MIOPEN_CONV_PRECISE_ROCM_TUNING"] = "0"
|
|||||||
# Reset sleep states
|
# Reset sleep states
|
||||||
atexit.register(prevent_sleep_end)
|
atexit.register(prevent_sleep_end)
|
||||||
|
|
||||||
|
|
||||||
# Also handle signals (Ctrl+C, kill, etc.)
|
# Also handle signals (Ctrl+C, kill, etc.)
|
||||||
def _cleanup_sleep(signum, frame):
|
def _cleanup_sleep(signum, frame):
|
||||||
prevent_sleep_end()
|
prevent_sleep_end()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, _cleanup_sleep)
|
signal.signal(signal.SIGINT, _cleanup_sleep)
|
||||||
signal.signal(signal.SIGTERM, _cleanup_sleep)
|
signal.signal(signal.SIGTERM, _cleanup_sleep)
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ if sys.stderr is None:
|
|||||||
if platform.system() == "Darwin" and platform.processor() == "arm":
|
if platform.system() == "Darwin" and platform.processor() == "arm":
|
||||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||||
|
|
||||||
|
|
||||||
# Custom message handler to filter out specific Qt warnings
|
# Custom message handler to filter out specific Qt warnings
|
||||||
def qt_message_handler(mode, context, message):
|
def qt_message_handler(mode, context, message):
|
||||||
if "Wayland does not support QWindow::requestActivate()" in message:
|
if "Wayland does not support QWindow::requestActivate()" in message:
|
||||||
|
|||||||
+45
-19
@@ -3,6 +3,7 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import warnings
|
import warnings
|
||||||
import platform
|
import platform
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@@ -74,24 +75,32 @@ def get_user_config_path():
|
|||||||
if os.path.exists(custom_dir):
|
if os.path.exists(custom_dir):
|
||||||
config_dir = custom_dir
|
config_dir = custom_dir
|
||||||
else:
|
else:
|
||||||
config_dir = user_config_dir("abogen", appauthor=False, roaming=True, ensure_exists=True)
|
config_dir = user_config_dir(
|
||||||
|
"abogen", appauthor=False, roaming=True, ensure_exists=True
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Windows and fallback case
|
# Windows and fallback case
|
||||||
config_dir = user_config_dir("abogen", appauthor=False, roaming=True, ensure_exists=True)
|
config_dir = user_config_dir(
|
||||||
|
"abogen", appauthor=False, roaming=True, ensure_exists=True
|
||||||
|
)
|
||||||
|
|
||||||
return os.path.join(config_dir, "config.json")
|
return os.path.join(config_dir, "config.json")
|
||||||
|
|
||||||
|
|
||||||
# Define cache path
|
# Define cache path
|
||||||
def get_user_cache_path(folder=None):
|
def get_user_cache_path(folder=None):
|
||||||
from platformdirs import user_cache_dir
|
from platformdirs import user_cache_dir
|
||||||
|
|
||||||
cache_dir = user_cache_dir("abogen", appauthor=False, opinion=True, ensure_exists=True)
|
cache_dir = user_cache_dir(
|
||||||
|
"abogen", appauthor=False, opinion=True, ensure_exists=True
|
||||||
|
)
|
||||||
if folder:
|
if folder:
|
||||||
cache_dir = os.path.join(cache_dir, folder)
|
cache_dir = os.path.join(cache_dir, folder)
|
||||||
# Ensure the directory exists
|
# Ensure the directory exists
|
||||||
os.makedirs(cache_dir, exist_ok=True)
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
return cache_dir
|
return cache_dir
|
||||||
|
|
||||||
|
|
||||||
_sleep_procs = {"Darwin": None, "Linux": None} # Store sleep prevention processes
|
_sleep_procs = {"Darwin": None, "Linux": None} # Store sleep prevention processes
|
||||||
|
|
||||||
|
|
||||||
@@ -231,13 +240,23 @@ def calculate_text_length(text):
|
|||||||
def get_gpu_acceleration(enabled):
|
def get_gpu_acceleration(enabled):
|
||||||
try:
|
try:
|
||||||
import torch
|
import torch
|
||||||
from torch.cuda import is_available
|
from torch.cuda import is_available as cuda_available
|
||||||
|
|
||||||
if not enabled:
|
if not enabled:
|
||||||
return "CUDA GPU available but using CPU.", False
|
return "GPU available but using CPU.", False
|
||||||
if is_available():
|
|
||||||
|
# Check for Apple Silicon MPS
|
||||||
|
if platform.system() == "Darwin" and platform.processor() == "arm":
|
||||||
|
if torch.backends.mps.is_available():
|
||||||
|
return "MPS GPU available and enabled.", True
|
||||||
|
else:
|
||||||
|
return "MPS GPU not available on Apple Silicon. Using CPU.", False
|
||||||
|
|
||||||
|
# Check for CUDA
|
||||||
|
if cuda_available():
|
||||||
return "CUDA GPU available and enabled.", True
|
return "CUDA GPU available and enabled.", True
|
||||||
# Gather more diagnostic info if CUDA is not available
|
|
||||||
|
# Gather CUDA diagnostic info if not available
|
||||||
try:
|
try:
|
||||||
cuda_devices = torch.cuda.device_count()
|
cuda_devices = torch.cuda.device_count()
|
||||||
cuda_error = (
|
cuda_error = (
|
||||||
@@ -249,7 +268,7 @@ def get_gpu_acceleration(enabled):
|
|||||||
cuda_error = str(e)
|
cuda_error = str(e)
|
||||||
return f"CUDA GPU is not available. Using CPU. ({cuda_error})", False
|
return f"CUDA GPU is not available. Using CPU. ({cuda_error})", False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error checking CUDA GPU: {e}", False
|
return f"Error checking GPU: {e}", False
|
||||||
|
|
||||||
|
|
||||||
def prevent_sleep_start():
|
def prevent_sleep_start():
|
||||||
@@ -268,17 +287,24 @@ def prevent_sleep_start():
|
|||||||
# Add program name and reason for inhibition
|
# Add program name and reason for inhibition
|
||||||
program_name = PROGRAM_NAME
|
program_name = PROGRAM_NAME
|
||||||
reason = "Prevent sleep during abogen process"
|
reason = "Prevent sleep during abogen process"
|
||||||
_sleep_procs["Linux"] = create_process(
|
# Only attempt to use systemd-inhibit if it's available on the system.
|
||||||
[
|
if shutil.which("systemd-inhibit"):
|
||||||
"systemd-inhibit",
|
_sleep_procs["Linux"] = create_process(
|
||||||
f"--who={program_name}",
|
[
|
||||||
f"--why={reason}",
|
"systemd-inhibit",
|
||||||
"--what=sleep",
|
f"--who={program_name}",
|
||||||
"--mode=block",
|
f"--why={reason}",
|
||||||
"sleep",
|
"--what=sleep",
|
||||||
"infinity",
|
"--mode=block",
|
||||||
]
|
"sleep",
|
||||||
)
|
"infinity",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Non-systemd distro or systemd tools not installed: skip inhibition rather than crash
|
||||||
|
print(
|
||||||
|
"systemd-inhibit not found: skipping sleep inhibition on this Linux system."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def prevent_sleep_end():
|
def prevent_sleep_end():
|
||||||
|
|||||||
+9
-7
@@ -23,20 +23,22 @@ brew install ffmpeg
|
|||||||
sudo apt install ffmpeg
|
sudo apt install ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create the Video (.webm)
|
## Create the Video (.mp4) (Recommended)
|
||||||
|
|
||||||
Run this FFmpeg command to create the tiny video:
|
Run this FFmpeg command to create the video:
|
||||||
|
|
||||||
```
|
```
|
||||||
ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=subtitle.ass" -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus -shortest demo.webm
|
ffmpeg -loop 1 -framerate 24 -i bg.jpg -i demo.wav -vf "ass=demo.ass" -c:v libx264 -pix_fmt yuv420p -preset slow -crf 18 -c:a aac -b:a 192k -movflags +faststart -shortest demo.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
## For Higher Quality (But Larger) Video (.mp4)
|
## For Smaller Filesize (.webm)
|
||||||
|
|
||||||
If you need better quality for distribution, use this command instead:
|
If you need a smaller video file, use this command:
|
||||||
|
|
||||||
```
|
```
|
||||||
ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=subtitle.ass" -c:v libx264 -preset slow -crf 18 -movflags +faststart -c:a copy -shortest demo.mp4
|
ffmpeg -loop 1 -framerate 24 -i bg.jpg -i demo.wav -vf "ass=demo.ass" -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus -shortest demo.webm
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates an MP4 file that's compatible with more devices but larger in size.
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 12 KiB |
+124
-124
@@ -4,127 +4,127 @@ ScriptType: v4.00+
|
|||||||
|
|
||||||
[Events]
|
[Events]
|
||||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||||
Dialogue: 0,0:00:00.27,0:00:00.83,Default,,100,100,0,,{\an5}Abogen
|
Dialogue: 0,0:00:00.35,0:00:00.93,Default,,90,90,0,,{\an5}Abogen
|
||||||
Dialogue: 0,0:00:00.83,0:00:00.96,Default,,100,100,0,,{\an5}is
|
Dialogue: 0,0:00:00.93,0:00:01.07,Default,,90,90,0,,{\an5}is
|
||||||
Dialogue: 0,0:00:00.96,0:00:01.11,Default,,100,100,0,,{\an5}your
|
Dialogue: 0,0:00:01.07,0:00:01.27,Default,,90,90,0,,{\an5}your
|
||||||
Dialogue: 0,0:00:01.11,0:00:01.66,Default,,100,100,0,,{\an5}all-in-one
|
Dialogue: 0,0:00:01.27,0:00:01.86,Default,,90,90,0,,{\an5}all-in-one
|
||||||
Dialogue: 0,0:00:01.66,0:00:02.04,Default,,100,100,0,,{\an5}tool
|
Dialogue: 0,0:00:01.86,0:00:02.22,Default,,90,90,0,,{\an5}tool
|
||||||
Dialogue: 0,0:00:02.04,0:00:02.17,Default,,100,100,0,,{\an5}for
|
Dialogue: 0,0:00:02.22,0:00:02.35,Default,,90,90,0,,{\an5}for
|
||||||
Dialogue: 0,0:00:02.17,0:00:02.53,Default,,100,100,0,,{\an5}turning
|
Dialogue: 0,0:00:02.35,0:00:02.73,Default,,90,90,0,,{\an5}turning
|
||||||
Dialogue: 0,0:00:02.53,0:00:03.22,Default,,100,100,0,,{\an5}text
|
Dialogue: 0,0:00:02.73,0:00:03.13,Default,,90,90,0,,{\an5}text
|
||||||
Dialogue: 0,0:00:03.22,0:00:03.45,Default,,100,100,0,,{\an5}into
|
Dialogue: 0,0:00:03.13,0:00:03.35,Default,,90,90,0,,{\an5}into
|
||||||
Dialogue: 0,0:00:03.45,0:00:03.75,Default,,100,100,0,,{\an5}something
|
Dialogue: 0,0:00:03.35,0:00:03.70,Default,,90,90,0,,{\an5}something
|
||||||
Dialogue: 0,0:00:03.75,0:00:03.85,Default,,100,100,0,,{\an5}you
|
Dialogue: 0,0:00:03.70,0:00:03.79,Default,,90,90,0,,{\an5}you
|
||||||
Dialogue: 0,0:00:03.85,0:00:04.04,Default,,100,100,0,,{\an5}can
|
Dialogue: 0,0:00:03.79,0:00:03.98,Default,,90,90,0,,{\an5}can
|
||||||
Dialogue: 0,0:00:04.04,0:00:04.32,Default,,100,100,0,,{\an5}see
|
Dialogue: 0,0:00:03.98,0:00:04.25,Default,,90,90,0,,{\an5}see
|
||||||
Dialogue: 0,0:00:04.32,0:00:04.45,Default,,100,100,0,,{\an5}and
|
Dialogue: 0,0:00:04.25,0:00:04.37,Default,,90,90,0,,{\an5}and
|
||||||
Dialogue: 0,0:00:04.45,0:00:05.61,Default,,100,100,0,,{\an5}hear.
|
Dialogue: 0,0:00:04.37,0:00:05.37,Default,,90,90,0,,{\an5}hear.
|
||||||
Dialogue: 0,0:00:05.61,0:00:06.02,Default,,100,100,0,,{\an5}Easily
|
Dialogue: 0,0:00:05.37,0:00:05.79,Default,,90,90,0,,{\an5}Easily
|
||||||
Dialogue: 0,0:00:06.02,0:00:06.48,Default,,100,100,0,,{\an5}convert
|
Dialogue: 0,0:00:05.79,0:00:06.23,Default,,90,90,0,,{\an5}convert
|
||||||
Dialogue: 0,0:00:06.48,0:00:07.31,Default,,100,100,0,,{\an5}ePub,
|
Dialogue: 0,0:00:06.23,0:00:07.11,Default,,90,90,0,,{\an5}ePub,
|
||||||
Dialogue: 0,0:00:07.31,0:00:08.40,Default,,100,100,0,,{\an5}PDF,
|
Dialogue: 0,0:00:07.11,0:00:08.17,Default,,90,90,0,,{\an5}PDF,
|
||||||
Dialogue: 0,0:00:08.40,0:00:08.55,Default,,100,100,0,,{\an5}or
|
Dialogue: 0,0:00:08.17,0:00:08.33,Default,,90,90,0,,{\an5}or
|
||||||
Dialogue: 0,0:00:08.55,0:00:08.90,Default,,100,100,0,,{\an5}text
|
Dialogue: 0,0:00:08.33,0:00:08.67,Default,,90,90,0,,{\an5}text
|
||||||
Dialogue: 0,0:00:08.90,0:00:09.21,Default,,100,100,0,,{\an5}file
|
Dialogue: 0,0:00:08.67,0:00:08.98,Default,,90,90,0,,{\an5}file
|
||||||
Dialogue: 0,0:00:09.21,0:00:09.46,Default,,100,100,0,,{\an5}into
|
Dialogue: 0,0:00:08.98,0:00:09.23,Default,,90,90,0,,{\an5}into
|
||||||
Dialogue: 0,0:00:09.46,0:00:10.34,Default,,100,100,0,,{\an5}audio
|
Dialogue: 0,0:00:09.23,0:00:09.91,Default,,90,90,0,,{\an5}audio
|
||||||
Dialogue: 0,0:00:10.34,0:00:10.56,Default,,100,100,0,,{\an5}with
|
Dialogue: 0,0:00:09.91,0:00:10.09,Default,,90,90,0,,{\an5}with
|
||||||
Dialogue: 0,0:00:10.56,0:00:11.17,Default,,100,100,0,,{\an5}subtitles
|
Dialogue: 0,0:00:10.09,0:00:10.69,Default,,90,90,0,,{\an5}subtitles
|
||||||
Dialogue: 0,0:00:11.17,0:00:11.32,Default,,100,100,0,,{\an5}that
|
Dialogue: 0,0:00:10.69,0:00:10.87,Default,,90,90,0,,{\an5}that
|
||||||
Dialogue: 0,0:00:11.32,0:00:11.51,Default,,100,100,0,,{\an5}light
|
Dialogue: 0,0:00:10.87,0:00:11.06,Default,,90,90,0,,{\an5}light
|
||||||
Dialogue: 0,0:00:11.51,0:00:11.71,Default,,100,100,0,,{\an5}up
|
Dialogue: 0,0:00:11.06,0:00:11.26,Default,,90,90,0,,{\an5}up
|
||||||
Dialogue: 0,0:00:11.71,0:00:12.05,Default,,100,100,0,,{\an5}every
|
Dialogue: 0,0:00:11.26,0:00:11.59,Default,,90,90,0,,{\an5}every
|
||||||
Dialogue: 0,0:00:12.05,0:00:12.40,Default,,100,100,0,,{\an5}word
|
Dialogue: 0,0:00:11.59,0:00:11.90,Default,,90,90,0,,{\an5}word
|
||||||
Dialogue: 0,0:00:12.40,0:00:12.51,Default,,100,100,0,,{\an5}as
|
Dialogue: 0,0:00:11.90,0:00:12.02,Default,,90,90,0,,{\an5}as
|
||||||
Dialogue: 0,0:00:12.51,0:00:12.65,Default,,100,100,0,,{\an5}it’s
|
Dialogue: 0,0:00:12.02,0:00:12.17,Default,,90,90,0,,{\an5}it’s
|
||||||
Dialogue: 0,0:00:12.65,0:00:14.21,Default,,100,100,0,,{\an5}spoken.
|
Dialogue: 0,0:00:12.17,0:00:13.46,Default,,90,90,0,,{\an5}spoken.
|
||||||
Dialogue: 0,0:00:14.21,0:00:14.32,Default,,100,100,0,,{\an5}The
|
Dialogue: 0,0:00:13.46,0:00:13.59,Default,,90,90,0,,{\an5}The
|
||||||
Dialogue: 0,0:00:14.32,0:00:14.62,Default,,100,100,0,,{\an5}voice
|
Dialogue: 0,0:00:13.59,0:00:13.92,Default,,90,90,0,,{\an5}voice
|
||||||
Dialogue: 0,0:00:14.62,0:00:14.77,Default,,100,100,0,,{\an5}you
|
Dialogue: 0,0:00:13.92,0:00:14.07,Default,,90,90,0,,{\an5}you
|
||||||
Dialogue: 0,0:00:14.77,0:00:15.65,Default,,100,100,0,,{\an5}hear,
|
Dialogue: 0,0:00:14.07,0:00:14.86,Default,,90,90,0,,{\an5}hear,
|
||||||
Dialogue: 0,0:00:15.65,0:00:15.76,Default,,100,100,0,,{\an5}and
|
Dialogue: 0,0:00:14.86,0:00:14.98,Default,,90,90,0,,{\an5}and
|
||||||
Dialogue: 0,0:00:15.76,0:00:15.86,Default,,100,100,0,,{\an5}the
|
Dialogue: 0,0:00:14.98,0:00:15.08,Default,,90,90,0,,{\an5}the
|
||||||
Dialogue: 0,0:00:15.86,0:00:16.44,Default,,100,100,0,,{\an5}subtitles
|
Dialogue: 0,0:00:15.08,0:00:15.67,Default,,90,90,0,,{\an5}subtitles
|
||||||
Dialogue: 0,0:00:16.44,0:00:16.58,Default,,100,100,0,,{\an5}you
|
Dialogue: 0,0:00:15.67,0:00:15.81,Default,,90,90,0,,{\an5}you
|
||||||
Dialogue: 0,0:00:16.58,0:00:17.03,Default,,100,100,0,,{\an5}see
|
Dialogue: 0,0:00:15.81,0:00:16.13,Default,,90,90,0,,{\an5}see
|
||||||
Dialogue: 0,0:00:17.03,0:00:17.14,Default,,100,100,0,,{\an5}are
|
Dialogue: 0,0:00:16.13,0:00:16.27,Default,,90,90,0,,{\an5}are
|
||||||
Dialogue: 0,0:00:17.14,0:00:17.44,Default,,100,100,0,,{\an5}both
|
Dialogue: 0,0:00:16.27,0:00:16.57,Default,,90,90,0,,{\an5}both
|
||||||
Dialogue: 0,0:00:17.44,0:00:17.91,Default,,100,100,0,,{\an5}created
|
Dialogue: 0,0:00:16.57,0:00:17.03,Default,,90,90,0,,{\an5}created
|
||||||
Dialogue: 0,0:00:17.91,0:00:18.96,Default,,100,100,0,,{\an5}automatically
|
Dialogue: 0,0:00:17.03,0:00:17.96,Default,,90,90,0,,{\an5}automatically
|
||||||
Dialogue: 0,0:00:18.96,0:00:19.14,Default,,100,100,0,,{\an5}by
|
Dialogue: 0,0:00:17.96,0:00:18.14,Default,,90,90,0,,{\an5}by
|
||||||
Dialogue: 0,0:00:19.14,0:00:19.64,Default,,100,100,0,,{\an5}Abogen
|
Dialogue: 0,0:00:18.14,0:00:18.67,Default,,90,90,0,,{\an5}Abogen
|
||||||
Dialogue: 0,0:00:19.64,0:00:20.00,Default,,100,100,0,,{\an5}using
|
Dialogue: 0,0:00:18.67,0:00:19.02,Default,,90,90,0,,{\an5}using
|
||||||
Dialogue: 0,0:00:20.00,0:00:21.14,Default,,100,100,0,,{\an5}Kokoro,
|
Dialogue: 0,0:00:19.02,0:00:20.10,Default,,90,90,0,,{\an5}Kokoro,
|
||||||
Dialogue: 0,0:00:21.14,0:00:21.28,Default,,100,100,0,,{\an5}an
|
Dialogue: 0,0:00:20.10,0:00:20.27,Default,,90,90,0,,{\an5}an
|
||||||
Dialogue: 0,0:00:21.28,0:00:21.88,Default,,100,100,0,,{\an5}open-weight
|
Dialogue: 0,0:00:20.27,0:00:20.88,Default,,90,90,0,,{\an5}open-weight
|
||||||
Dialogue: 0,0:00:21.88,0:00:22.52,Default,,100,100,0,,{\an5}TTS
|
Dialogue: 0,0:00:20.88,0:00:21.55,Default,,90,90,0,,{\an5}TTS
|
||||||
Dialogue: 0,0:00:22.52,0:00:23.18,Default,,100,100,0,,{\an5}model
|
Dialogue: 0,0:00:21.55,0:00:21.94,Default,,90,90,0,,{\an5}model
|
||||||
Dialogue: 0,0:00:23.18,0:00:23.38,Default,,100,100,0,,{\an5}with
|
Dialogue: 0,0:00:21.94,0:00:22.14,Default,,90,90,0,,{\an5}with
|
||||||
Dialogue: 0,0:00:23.38,0:00:24.01,Default,,100,100,0,,{\an5}82
|
Dialogue: 0,0:00:22.14,0:00:22.78,Default,,90,90,0,,{\an5}82
|
||||||
Dialogue: 0,0:00:24.01,0:00:24.37,Default,,100,100,0,,{\an5}million
|
Dialogue: 0,0:00:22.78,0:00:23.14,Default,,90,90,0,,{\an5}million
|
||||||
Dialogue: 0,0:00:24.37,0:00:25.98,Default,,100,100,0,,{\an5}parameters.
|
Dialogue: 0,0:00:23.14,0:00:24.55,Default,,90,90,0,,{\an5}parameters.
|
||||||
Dialogue: 0,0:00:25.98,0:00:26.47,Default,,100,100,0,,{\an5}Everything
|
Dialogue: 0,0:00:24.55,0:00:25.05,Default,,90,90,0,,{\an5}Everything
|
||||||
Dialogue: 0,0:00:26.47,0:00:26.77,Default,,100,100,0,,{\an5}works
|
Dialogue: 0,0:00:25.05,0:00:25.35,Default,,90,90,0,,{\an5}works
|
||||||
Dialogue: 0,0:00:26.77,0:00:27.01,Default,,100,100,0,,{\an5}right
|
Dialogue: 0,0:00:25.35,0:00:25.58,Default,,90,90,0,,{\an5}right
|
||||||
Dialogue: 0,0:00:27.01,0:00:27.11,Default,,100,100,0,,{\an5}on
|
Dialogue: 0,0:00:25.58,0:00:25.68,Default,,90,90,0,,{\an5}on
|
||||||
Dialogue: 0,0:00:27.11,0:00:27.22,Default,,100,100,0,,{\an5}your
|
Dialogue: 0,0:00:25.68,0:00:25.80,Default,,90,90,0,,{\an5}your
|
||||||
Dialogue: 0,0:00:27.22,0:00:28.23,Default,,100,100,0,,{\an5}computer,
|
Dialogue: 0,0:00:25.80,0:00:26.69,Default,,90,90,0,,{\an5}computer,
|
||||||
Dialogue: 0,0:00:28.23,0:00:28.37,Default,,100,100,0,,{\an5}so
|
Dialogue: 0,0:00:26.69,0:00:26.87,Default,,90,90,0,,{\an5}so
|
||||||
Dialogue: 0,0:00:28.37,0:00:28.53,Default,,100,100,0,,{\an5}your
|
Dialogue: 0,0:00:26.87,0:00:27.06,Default,,90,90,0,,{\an5}your
|
||||||
Dialogue: 0,0:00:28.53,0:00:28.94,Default,,100,100,0,,{\an5}files
|
Dialogue: 0,0:00:27.06,0:00:27.47,Default,,90,90,0,,{\an5}files
|
||||||
Dialogue: 0,0:00:28.94,0:00:29.23,Default,,100,100,0,,{\an5}stay
|
Dialogue: 0,0:00:27.47,0:00:27.73,Default,,90,90,0,,{\an5}stay
|
||||||
Dialogue: 0,0:00:29.23,0:00:29.88,Default,,100,100,0,,{\an5}private
|
Dialogue: 0,0:00:27.73,0:00:28.28,Default,,90,90,0,,{\an5}private
|
||||||
Dialogue: 0,0:00:29.88,0:00:30.05,Default,,100,100,0,,{\an5}and
|
Dialogue: 0,0:00:28.28,0:00:28.44,Default,,90,90,0,,{\an5}and
|
||||||
Dialogue: 0,0:00:30.05,0:00:31.16,Default,,100,100,0,,{\an5}secure.
|
Dialogue: 0,0:00:28.44,0:00:29.51,Default,,90,90,0,,{\an5}secure.
|
||||||
Dialogue: 0,0:00:31.50,0:00:31.63,Default,,100,100,0,,{\an5}The
|
Dialogue: 0,0:00:29.90,0:00:30.03,Default,,90,90,0,,{\an5}The
|
||||||
Dialogue: 0,0:00:31.63,0:00:31.97,Default,,100,100,0,,{\an5}easy
|
Dialogue: 0,0:00:30.03,0:00:30.37,Default,,90,90,0,,{\an5}easy
|
||||||
Dialogue: 0,0:00:31.97,0:00:32.54,Default,,100,100,0,,{\an5}interface
|
Dialogue: 0,0:00:30.37,0:00:30.95,Default,,90,90,0,,{\an5}interface
|
||||||
Dialogue: 0,0:00:32.54,0:00:32.81,Default,,100,100,0,,{\an5}lets
|
Dialogue: 0,0:00:30.95,0:00:31.23,Default,,90,90,0,,{\an5}lets
|
||||||
Dialogue: 0,0:00:32.81,0:00:33.23,Default,,100,100,0,,{\an5}anyone
|
Dialogue: 0,0:00:31.23,0:00:31.71,Default,,90,90,0,,{\an5}anyone
|
||||||
Dialogue: 0,0:00:33.23,0:00:33.66,Default,,100,100,0,,{\an5}create
|
Dialogue: 0,0:00:31.71,0:00:32.13,Default,,90,90,0,,{\an5}create
|
||||||
Dialogue: 0,0:00:33.66,0:00:34.88,Default,,100,100,0,,{\an5}audiobooks,
|
Dialogue: 0,0:00:32.13,0:00:33.33,Default,,90,90,0,,{\an5}audiobooks,
|
||||||
Dialogue: 0,0:00:34.88,0:00:35.54,Default,,100,100,0,,{\an5}voiceovers
|
Dialogue: 0,0:00:33.33,0:00:34.07,Default,,90,90,0,,{\an5}voiceovers
|
||||||
Dialogue: 0,0:00:35.54,0:00:35.70,Default,,100,100,0,,{\an5}for
|
Dialogue: 0,0:00:34.07,0:00:34.23,Default,,90,90,0,,{\an5}for
|
||||||
Dialogue: 0,0:00:35.70,0:00:36.68,Default,,100,100,0,,{\an5}Instagram,
|
Dialogue: 0,0:00:34.23,0:00:35.30,Default,,90,90,0,,{\an5}Instagram,
|
||||||
Dialogue: 0,0:00:36.68,0:00:37.23,Default,,100,100,0,,{\an5}YouTube
|
Dialogue: 0,0:00:35.30,0:00:35.93,Default,,90,90,0,,{\an5}YouTube
|
||||||
Dialogue: 0,0:00:37.23,0:00:37.41,Default,,100,100,0,,{\an5}or
|
Dialogue: 0,0:00:35.93,0:00:36.11,Default,,90,90,0,,{\an5}or
|
||||||
Dialogue: 0,0:00:37.41,0:00:38.52,Default,,100,100,0,,{\an5}TikTok,
|
Dialogue: 0,0:00:36.11,0:00:36.92,Default,,90,90,0,,{\an5}TikTok,
|
||||||
Dialogue: 0,0:00:38.52,0:00:38.64,Default,,100,100,0,,{\an5}or
|
Dialogue: 0,0:00:36.92,0:00:37.07,Default,,90,90,0,,{\an5}or
|
||||||
Dialogue: 0,0:00:38.64,0:00:38.89,Default,,100,100,0,,{\an5}just
|
Dialogue: 0,0:00:37.07,0:00:37.30,Default,,90,90,0,,{\an5}just
|
||||||
Dialogue: 0,0:00:38.89,0:00:39.57,Default,,100,100,0,,{\an5}high-quality
|
Dialogue: 0,0:00:37.30,0:00:38.00,Default,,90,90,0,,{\an5}high-quality
|
||||||
Dialogue: 0,0:00:39.57,0:00:40.39,Default,,100,100,0,,{\an5}text-to-speech
|
Dialogue: 0,0:00:38.00,0:00:38.82,Default,,90,90,0,,{\an5}text-to-speech
|
||||||
Dialogue: 0,0:00:40.39,0:00:40.56,Default,,100,100,0,,{\an5}in
|
Dialogue: 0,0:00:38.82,0:00:38.97,Default,,90,90,0,,{\an5}in
|
||||||
Dialogue: 0,0:00:40.56,0:00:42.03,Default,,100,100,0,,{\an5}seconds.
|
Dialogue: 0,0:00:38.97,0:00:40.16,Default,,90,90,0,,{\an5}seconds.
|
||||||
Dialogue: 0,0:00:42.03,0:00:42.32,Default,,100,100,0,,{\an5}Whether
|
Dialogue: 0,0:00:40.16,0:00:40.45,Default,,90,90,0,,{\an5}Whether
|
||||||
Dialogue: 0,0:00:42.32,0:00:42.47,Default,,100,100,0,,{\an5}you
|
Dialogue: 0,0:00:40.45,0:00:40.57,Default,,90,90,0,,{\an5}you
|
||||||
Dialogue: 0,0:00:42.47,0:00:42.67,Default,,100,100,0,,{\an5}want
|
Dialogue: 0,0:00:40.57,0:00:40.77,Default,,90,90,0,,{\an5}want
|
||||||
Dialogue: 0,0:00:42.67,0:00:42.77,Default,,100,100,0,,{\an5}to
|
Dialogue: 0,0:00:40.77,0:00:40.87,Default,,90,90,0,,{\an5}to
|
||||||
Dialogue: 0,0:00:42.77,0:00:43.43,Default,,100,100,0,,{\an5}listen,
|
Dialogue: 0,0:00:40.87,0:00:41.58,Default,,90,90,0,,{\an5}listen,
|
||||||
Dialogue: 0,0:00:43.43,0:00:43.77,Default,,100,100,0,,{\an5}create
|
Dialogue: 0,0:00:41.58,0:00:41.92,Default,,90,90,0,,{\an5}create
|
||||||
Dialogue: 0,0:00:43.77,0:00:44.70,Default,,100,100,0,,{\an5}content,
|
Dialogue: 0,0:00:41.92,0:00:42.65,Default,,90,90,0,,{\an5}content,
|
||||||
Dialogue: 0,0:00:44.70,0:00:44.85,Default,,100,100,0,,{\an5}or
|
Dialogue: 0,0:00:42.65,0:00:42.79,Default,,90,90,0,,{\an5}or
|
||||||
Dialogue: 0,0:00:44.85,0:00:45.07,Default,,100,100,0,,{\an5}bring
|
Dialogue: 0,0:00:42.79,0:00:43.01,Default,,90,90,0,,{\an5}bring
|
||||||
Dialogue: 0,0:00:45.07,0:00:45.22,Default,,100,100,0,,{\an5}your
|
Dialogue: 0,0:00:43.01,0:00:43.12,Default,,90,90,0,,{\an5}your
|
||||||
Dialogue: 0,0:00:45.22,0:00:45.54,Default,,100,100,0,,{\an5}words
|
Dialogue: 0,0:00:43.12,0:00:43.42,Default,,90,90,0,,{\an5}words
|
||||||
Dialogue: 0,0:00:45.54,0:00:45.67,Default,,100,100,0,,{\an5}to
|
Dialogue: 0,0:00:43.42,0:00:43.54,Default,,90,90,0,,{\an5}to
|
||||||
Dialogue: 0,0:00:45.67,0:00:46.58,Default,,100,100,0,,{\an5}life,
|
Dialogue: 0,0:00:43.54,0:00:44.33,Default,,90,90,0,,{\an5}life,
|
||||||
Dialogue: 0,0:00:46.58,0:00:47.10,Default,,100,100,0,,{\an5}Abogen
|
Dialogue: 0,0:00:44.33,0:00:44.90,Default,,90,90,0,,{\an5}Abogen
|
||||||
Dialogue: 0,0:00:47.10,0:00:47.37,Default,,100,100,0,,{\an5}makes
|
Dialogue: 0,0:00:44.90,0:00:45.17,Default,,90,90,0,,{\an5}makes
|
||||||
Dialogue: 0,0:00:47.37,0:00:47.53,Default,,100,100,0,,{\an5}it
|
Dialogue: 0,0:00:45.17,0:00:45.35,Default,,90,90,0,,{\an5}it
|
||||||
Dialogue: 0,0:00:47.53,0:00:48.31,Default,,100,100,0,,{\an5}fast,
|
Dialogue: 0,0:00:45.35,0:00:46.12,Default,,90,90,0,,{\an5}fast,
|
||||||
Dialogue: 0,0:00:48.31,0:00:48.89,Default,,100,100,0,,{\an5}fun,
|
Dialogue: 0,0:00:46.12,0:00:46.65,Default,,90,90,0,,{\an5}fun,
|
||||||
Dialogue: 0,0:00:48.89,0:00:49.02,Default,,100,100,0,,{\an5}and
|
Dialogue: 0,0:00:46.65,0:00:46.77,Default,,90,90,0,,{\an5}and
|
||||||
Dialogue: 0,0:00:49.02,0:00:50.62,Default,,100,100,0,,{\an5}effortless.
|
Dialogue: 0,0:00:46.77,0:00:48.07,Default,,90,90,0,,{\an5}effortless.
|
||||||
Dialogue: 0,0:00:50.62,0:00:51.35,Default,,100,100,0,,{\an5}Experience
|
Dialogue: 0,0:00:48.07,0:00:48.76,Default,,90,90,0,,{\an5}Experience
|
||||||
Dialogue: 0,0:00:51.35,0:00:51.47,Default,,100,100,0,,{\an5}your
|
Dialogue: 0,0:00:48.76,0:00:48.87,Default,,90,90,0,,{\an5}your
|
||||||
Dialogue: 0,0:00:51.47,0:00:52.23,Default,,100,100,0,,{\an5}stories,
|
Dialogue: 0,0:00:48.87,0:00:49.61,Default,,90,90,0,,{\an5}stories,
|
||||||
Dialogue: 0,0:00:52.23,0:00:52.97,Default,,100,100,0,,{\an5}scripts,
|
Dialogue: 0,0:00:49.61,0:00:50.15,Default,,90,90,0,,{\an5}scripts,
|
||||||
Dialogue: 0,0:00:52.97,0:00:53.11,Default,,100,100,0,,{\an5}and
|
Dialogue: 0,0:00:50.15,0:00:50.29,Default,,90,90,0,,{\an5}and
|
||||||
Dialogue: 0,0:00:53.11,0:00:53.64,Default,,100,100,0,,{\an5}ideas
|
Dialogue: 0,0:00:50.29,0:00:50.87,Default,,90,90,0,,{\an5}ideas
|
||||||
Dialogue: 0,0:00:53.64,0:00:53.73,Default,,100,100,0,,{\an5}in
|
Dialogue: 0,0:00:50.87,0:00:50.97,Default,,90,90,0,,{\an5}in
|
||||||
Dialogue: 0,0:00:53.73,0:00:53.83,Default,,100,100,0,,{\an5}a
|
Dialogue: 0,0:00:50.97,0:00:51.10,Default,,90,90,0,,{\an5}a
|
||||||
Dialogue: 0,0:00:53.83,0:00:54.14,Default,,100,100,0,,{\an5}whole
|
Dialogue: 0,0:00:51.10,0:00:51.46,Default,,90,90,0,,{\an5}whole
|
||||||
Dialogue: 0,0:00:54.14,0:00:54.35,Default,,100,100,0,,{\an5}new
|
Dialogue: 0,0:00:51.46,0:00:51.65,Default,,90,90,0,,{\an5}new
|
||||||
Dialogue: 0,0:00:54.35,0:00:54.64,Default,,100,100,0,,{\an5}way
|
Dialogue: 0,0:00:51.65,0:00:51.95,Default,,90,90,0,,{\an5}way
|
||||||
Dialogue: 0,0:00:54.64,0:00:54.79,Default,,100,100,0,,{\an5}with
|
Dialogue: 0,0:00:51.95,0:00:52.10,Default,,90,90,0,,{\an5}with
|
||||||
Dialogue: 0,0:00:54.79,0:00:55.85,Default,,100,100,0,,{\an5}Abogen.
|
Dialogue: 0,0:00:52.10,0:00:53.20,Default,,90,90,0,,{\an5}Abogen.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -15,6 +15,7 @@ keywords = ["audiobook", "epub", "pdf", "text-to-speech", "subtitle", "tts", "ko
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"PyQt5>=5.15.11",
|
"PyQt5>=5.15.11",
|
||||||
"kokoro>=0.9.4",
|
"kokoro>=0.9.4",
|
||||||
|
"misaki[zh]>=0.9.4",
|
||||||
"ebooklib>=0.19",
|
"ebooklib>=0.19",
|
||||||
"beautifulsoup4>=4.13.4",
|
"beautifulsoup4>=4.13.4",
|
||||||
"PyMuPDF>=1.25.5",
|
"PyMuPDF>=1.25.5",
|
||||||
|
|||||||
Reference in New Issue
Block a user