20 Commits
Author SHA1 Message Date
Deniz Şafak 023884d6d8 1.1.7 2025-08-25 03:09:37 +03:00
Deniz Şafak aa98547456 Fixed sleep inhibition error 2025-08-25 02:19:05 +03:00
Deniz ŞafakandGitHub 528343f375 Fix demo video not playing in Firefox 2025-08-25 02:07:09 +03:00
Deniz Şafak 7fa8b23c4e Update demo video guide 2025-08-25 02:04:55 +03:00
Deniz Şafak 3171066f0c Improve karaoke mode and update changelog 2025-08-25 01:34:16 +03:00
Deniz ŞafakandGitHub c1c93ea00a Merge pull request #69 from denizsafak/test
Add word-by-word karaoke highlighting option
2025-08-25 01:07:07 +03:00
Deniz ŞafakandGitHub 795dee84ad Merge pull request #65 from robmckinnon/karaoke
Add word-by-word karaoke highlighting option
2025-08-25 00:50:57 +03:00
Deniz ŞafakandGitHub eb4575df23 Merge pull request #68 from denizsafak/apple
Add MPS GPU acceleratin on Silicon Mac
2025-08-25 00:49:00 +03:00
robmckinnon f58b254aed Add word-by-word karaoke highlighting option
Add option label: Sentence + Highlighting
2025-08-18 22:02:24 +01:00
Deniz ŞafakandGitHub e073c32e64 Update WINDOWS_INSTALL.bat
Fix wrong variable
2025-08-13 16:14:12 +03:00
Deniz ŞafakandGitHub 509477bafa Update test_publish.yml 2025-08-12 16:49:56 +03:00
Deniz ŞafakandGitHub 867d373eb9 Update test_publish.yml 2025-08-12 16:37:48 +03:00
Deniz ŞafakandGitHub f727c5ca70 Update test_publish.yml 2025-08-12 11:51:33 +03:00
Deniz Şafak e48df7c106 Test MPS GPU acceleratin on Silicon Mac 2025-08-07 05:22:43 +03:00
Deniz Şafak 395cd75038 v1.1.6 2025-07-30 02:33:52 +03:00
Deniz Şafak e60a1f7e42 Reformat using black 2025-07-30 02:31:49 +03:00
Deniz Şafak 1fe5a73c98 Improved EPUB chapter detection 2025-07-30 02:25:06 +03:00
Deniz Şafak 37db948d2f Include Mandarin Chinese (misaki[zh]) by default 2025-07-29 17:14:59 +03:00
Deniz Şafak 63679405a9 Ask user for CUDA installation in Windows installer script 2025-07-29 16:56:39 +03:00
Deniz Şafak 156fa75c76 Fixed SRT subtitle numbering issue in #41 2025-07-29 15:47:45 +03:00
17 changed files with 461 additions and 237 deletions
+7 -5
View File
@@ -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
View File
@@ -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 -2
View File
@@ -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 @@ Heres 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 @@ Heres 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
View File
@@ -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
View File
@@ -1 +1 @@
1.1.5 1.1.7
+66 -55
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+124 -124
View File
@@ -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}its Dialogue: 0,0:00:12.02,0:00:12.17,Default,,90,90,0,,{\an5}its
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.
View File
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1
View File
@@ -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",