mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Added new option: Subtitle format
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# v1.0.9 (pre-release)
|
# v1.0.9 (pre-release)
|
||||||
- Added chunking/segmenting system that fixes memory outage issues when processing large audio files.
|
- Added chunking/segmenting system that fixes memory outage issues when processing large audio files.
|
||||||
|
- Added new option: `Subtitle format`, allowing users to choose between `srt` , `ass (wide)`, `ass (narrow)`, and `ass (centered wide)` and `ass (centered narrow)`
|
||||||
- Improved chapter filename generation with smart word-boundary truncation at 80 characters, preventing mid-word cuts in filenames.
|
- Improved chapter filename generation with smart word-boundary truncation at 80 characters, preventing mid-word cuts in filenames.
|
||||||
- `Composer` and `Genre` metadata fields for M4B files are now editable from the text editor.
|
- `Composer` and `Genre` metadata fields for M4B files are now editable from the text editor.
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ Here’s Abogen in action: in this demo, it processes ∼3,000 characters of tex
|
|||||||
- **Options**:
|
- **Options**:
|
||||||
- **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.
|
||||||
- **Configure max words per subtitle**: Configures the maximum number of words per subtitle entry.
|
- **Configure max words per subtitle**: Configures the maximum number of words per subtitle entry.
|
||||||
- **Separate chapters audio format**: Configures the audio format for separate chapters (e.g., `WAV`, `FLAC`, `MP3`, `OPUS`).
|
- **Subtitle format**: Configures the subtitle format as `srt`, `ass (wide)`, `ass (narrow)`, `ass (centered wide)`, or `ass (centered narrow)`.
|
||||||
|
- **Separate chapters audio format**: Configures the audio format for separate chapters as `wav`, `flac`, `mp3`, or `opus`.
|
||||||
- **Create desktop shortcut**: Creates a shortcut on your desktop for easy access.
|
- **Create desktop shortcut**: Creates a shortcut on your desktop for easy access.
|
||||||
- **Open config.json directory**: Opens the directory where the configuration file is stored.
|
- **Open config.json directory**: Opens the directory where the configuration file is stored.
|
||||||
- **Open temp directory**: Opens the temporary directory where converted text files are stored.
|
- **Open temp directory**: Opens the temporary directory where converted text files are stored.
|
||||||
@@ -194,11 +195,13 @@ For a complete list of supported languages and voices, refer to Kokoro's [VOICES
|
|||||||
## `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`:
|
||||||
```
|
```
|
||||||
|
# --- MPV Settings ---
|
||||||
save-position-on-quit
|
save-position-on-quit
|
||||||
keep-open=yes
|
keep-open=yes
|
||||||
--audio-device=openal
|
# --- Subtitle ---
|
||||||
--sub-margin-x=235
|
sub-ass-override=no
|
||||||
--sub-pos=60
|
sub-margin-y=50
|
||||||
|
sub-margin-x=50
|
||||||
# --- Audio Quality ---
|
# --- Audio Quality ---
|
||||||
audio-spdif=ac3,dts,eac3,truehd,dts-hd
|
audio-spdif=ac3,dts,eac3,truehd,dts-hd
|
||||||
audio-channels=auto
|
audio-channels=auto
|
||||||
|
|||||||
+73
-20
@@ -333,6 +333,7 @@ class ConversionThread(QThread):
|
|||||||
self.log_updated.emit(f"- Speed: {self.speed}")
|
self.log_updated.emit(f"- Speed: {self.speed}")
|
||||||
self.log_updated.emit(f"- Subtitle mode: {self.subtitle_mode}")
|
self.log_updated.emit(f"- Subtitle mode: {self.subtitle_mode}")
|
||||||
self.log_updated.emit(f"- Output format: {self.output_format}")
|
self.log_updated.emit(f"- Output format: {self.output_format}")
|
||||||
|
self.log_updated.emit(f"- Subtitle format: {getattr(self, 'subtitle_format', 'srt')}")
|
||||||
self.log_updated.emit(f"- Save option: {self.save_option}")
|
self.log_updated.emit(f"- Save option: {self.save_option}")
|
||||||
if self.replace_single_newlines:
|
if self.replace_single_newlines:
|
||||||
self.log_updated.emit(f"- Replace single newlines: Yes")
|
self.log_updated.emit(f"- Replace single newlines: Yes")
|
||||||
@@ -682,24 +683,34 @@ class ConversionThread(QThread):
|
|||||||
self.log_updated.emit((f"Failed to write {separate_format.upper()} file.", "red"))
|
self.log_updated.emit((f"Failed to write {separate_format.upper()} file.", "red"))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Generate .srt subtitle file for chapter if not Disabled
|
# Generate subtitle file for chapter if not Disabled
|
||||||
if self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
|
if self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
|
||||||
chapter_srt_path = os.path.join(
|
subtitle_format = getattr(self, 'subtitle_format', 'srt')
|
||||||
chapters_out_dir, f"{chapter_filename}.srt"
|
file_extension = 'ass' if 'ass' in subtitle_format else 'srt'
|
||||||
|
chapter_subtitle_path = os.path.join(
|
||||||
|
chapters_out_dir, f"{chapter_filename}.{file_extension}"
|
||||||
)
|
)
|
||||||
with open(
|
|
||||||
chapter_srt_path, "w", encoding="utf-8", errors="replace"
|
if 'ass' in subtitle_format:
|
||||||
) as srt_file:
|
# Generate ASS subtitle
|
||||||
for i, (start, end, text) in enumerate(
|
is_centered = 'centered' in subtitle_format
|
||||||
chapter_subtitle_entries, 1
|
is_narrow = 'narrow' in subtitle_format
|
||||||
):
|
self._write_ass_subtitle(chapter_subtitle_path, chapter_subtitle_entries, is_centered, is_narrow)
|
||||||
srt_file.write(
|
else:
|
||||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
# Generate SRT subtitle (default)
|
||||||
)
|
with open(
|
||||||
|
chapter_subtitle_path, "w", encoding="utf-8", errors="replace"
|
||||||
|
) as srt_file:
|
||||||
|
for i, (start, end, text) in enumerate(
|
||||||
|
chapter_subtitle_entries, 1
|
||||||
|
):
|
||||||
|
srt_file.write(
|
||||||
|
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
(
|
(
|
||||||
f"\nChapter {chapter_idx} saved to: {chapter_out_path}\n\nSubtitle saved to: {chapter_srt_path}",
|
f"\nChapter {chapter_idx} saved to: {chapter_out_path}\n\nSubtitle saved to: {chapter_subtitle_path}",
|
||||||
"green",
|
"green",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -756,15 +767,25 @@ class ConversionThread(QThread):
|
|||||||
# Subtitle and final message logic
|
# Subtitle and final message logic
|
||||||
if final_out_path:
|
if final_out_path:
|
||||||
if self.subtitle_mode != "Disabled":
|
if self.subtitle_mode != "Disabled":
|
||||||
srt_path = os.path.splitext(final_out_path)[0] + ".srt"
|
subtitle_format = getattr(self, 'subtitle_format', 'srt')
|
||||||
with open(srt_path, "w", encoding="utf-8", errors="replace") as srt_file:
|
file_extension = 'ass' if 'ass' in subtitle_format else 'srt'
|
||||||
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
subtitle_path = os.path.splitext(final_out_path)[0] + f".{file_extension}"
|
||||||
srt_file.write(
|
|
||||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
if 'ass' in subtitle_format:
|
||||||
)
|
# Generate ASS subtitle with optional centering and margin
|
||||||
|
is_centered = 'centered' in subtitle_format
|
||||||
|
is_narrow = 'narrow' in subtitle_format
|
||||||
|
self._write_ass_subtitle(subtitle_path, subtitle_entries, is_centered, is_narrow)
|
||||||
|
else:
|
||||||
|
# Generate SRT subtitle (default)
|
||||||
|
with open(subtitle_path, "w", encoding="utf-8", errors="replace") as srt_file:
|
||||||
|
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
||||||
|
srt_file.write(
|
||||||
|
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||||
|
)
|
||||||
self.conversion_finished.emit(
|
self.conversion_finished.emit(
|
||||||
(
|
(
|
||||||
f"\nAudiobook saved to: {final_out_path}\n\nSubtitle saved to: {srt_path}",
|
f"\nAudiobook saved to: {final_out_path}\n\nSubtitle saved to: {subtitle_path}",
|
||||||
"green",
|
"green",
|
||||||
),
|
),
|
||||||
final_out_path,
|
final_out_path,
|
||||||
@@ -982,6 +1003,14 @@ class ConversionThread(QThread):
|
|||||||
ms = int((t - int(t)) * 1000)
|
ms = int((t - int(t)) * 1000)
|
||||||
return f"{h:02}:{m:02}:{s:02},{ms:03}"
|
return f"{h:02}:{m:02}:{s:02},{ms:03}"
|
||||||
|
|
||||||
|
def _ass_time(self, t):
|
||||||
|
"""Helper function to format time for ASS files"""
|
||||||
|
h = int(t // 3600)
|
||||||
|
m = int((t % 3600) // 60)
|
||||||
|
s = int(t % 60)
|
||||||
|
cs = int((t - int(t)) * 100) # Centiseconds for ASS format
|
||||||
|
return f"{h}:{m:02}:{s:02}.{cs:02}"
|
||||||
|
|
||||||
def _process_subtitle_tokens(
|
def _process_subtitle_tokens(
|
||||||
self, tokens_with_timestamps, subtitle_entries, max_subtitle_words
|
self, tokens_with_timestamps, subtitle_entries, max_subtitle_words
|
||||||
):
|
):
|
||||||
@@ -1068,6 +1097,30 @@ class ConversionThread(QThread):
|
|||||||
(group[0]["start"], group[-1]["end"], text.strip())
|
(group[0]["start"], group[-1]["end"], text.strip())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _write_ass_subtitle(self, file_path, subtitle_entries, is_centered=False, is_narrow=False):
|
||||||
|
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
||||||
|
# Minimal ASS header
|
||||||
|
f.write("[Script Info]\n")
|
||||||
|
f.write("Title: Generated by Abogen\n")
|
||||||
|
f.write("ScriptType: v4.00+\n\n")
|
||||||
|
|
||||||
|
# Only events section, use override tags for positioning
|
||||||
|
f.write("[Events]\n")
|
||||||
|
f.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
|
||||||
|
|
||||||
|
# Set margin based on is_narrow parameter
|
||||||
|
margin = "90" if is_narrow else ""
|
||||||
|
alignment_tag = ""
|
||||||
|
if is_centered:
|
||||||
|
alignment = 5
|
||||||
|
alignment_tag = f"{{\\an{alignment}}}"
|
||||||
|
|
||||||
|
# Write each subtitle with override tag for alignment and margins
|
||||||
|
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
||||||
|
start_time = self._ass_time(start)
|
||||||
|
end_time = self._ass_time(end)
|
||||||
|
f.write(f"Dialogue: 0,{start_time},{end_time},Default,,{margin},{margin},0,,{alignment_tag}{text}\n")
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
self.cancel_requested = True
|
self.cancel_requested = True
|
||||||
self.waiting_for_user_input = (
|
self.waiting_for_user_input = (
|
||||||
|
|||||||
+26
-1
@@ -1329,6 +1329,8 @@ class abogen(QWidget):
|
|||||||
)
|
)
|
||||||
# Pass separate_chapters_format setting
|
# Pass separate_chapters_format setting
|
||||||
self.conversion_thread.separate_chapters_format = self.separate_chapters_format
|
self.conversion_thread.separate_chapters_format = self.separate_chapters_format
|
||||||
|
# Pass subtitle format setting
|
||||||
|
self.conversion_thread.subtitle_format = self.config.get("subtitle_format", "srt")
|
||||||
# Pass chapter count for EPUB or PDF files
|
# Pass chapter count for EPUB or PDF files
|
||||||
if self.selected_file_type in ["epub", "pdf"] and hasattr(
|
if self.selected_file_type in ["epub", "pdf"] and hasattr(
|
||||||
self, "selected_chapters"
|
self, "selected_chapters"
|
||||||
@@ -1925,6 +1927,24 @@ class abogen(QWidget):
|
|||||||
max_words_action.triggered.connect(self.set_max_subtitle_words)
|
max_words_action.triggered.connect(self.set_max_subtitle_words)
|
||||||
menu.addAction(max_words_action)
|
menu.addAction(max_words_action)
|
||||||
|
|
||||||
|
# Add subtitle format option
|
||||||
|
subtitle_format_menu = QMenu("Subtitle format", self)
|
||||||
|
subtitle_format_menu.setToolTip("Choose the format for generated subtitles")
|
||||||
|
|
||||||
|
subtitle_format_group = QActionGroup(self)
|
||||||
|
subtitle_format_group.setExclusive(True)
|
||||||
|
|
||||||
|
subtitle_format = self.config.get("subtitle_format", "srt")
|
||||||
|
for format_option in ["srt", "ass (wide)", "ass (narrow)", "ass (centered wide)", "ass (centered narrow)"]:
|
||||||
|
format_action = QAction(f"{format_option}", self)
|
||||||
|
format_action.setCheckable(True)
|
||||||
|
format_action.setChecked(subtitle_format == format_option)
|
||||||
|
format_action.triggered.connect(lambda checked, fmt=format_option: self.set_subtitle_format(fmt))
|
||||||
|
subtitle_format_group.addAction(format_action)
|
||||||
|
subtitle_format_menu.addAction(format_action)
|
||||||
|
|
||||||
|
menu.addMenu(subtitle_format_menu)
|
||||||
|
|
||||||
# Add separate chapters format option
|
# Add separate chapters format option
|
||||||
separate_chapters_format_menu = QMenu("Separate chapters audio format", self)
|
separate_chapters_format_menu = QMenu("Separate chapters audio format", self)
|
||||||
separate_chapters_format_menu.setToolTip("Choose the format for individual chapter files")
|
separate_chapters_format_menu.setToolTip("Choose the format for individual chapter files")
|
||||||
@@ -1932,7 +1952,7 @@ class abogen(QWidget):
|
|||||||
format_group = QActionGroup(self)
|
format_group = QActionGroup(self)
|
||||||
format_group.setExclusive(True)
|
format_group.setExclusive(True)
|
||||||
|
|
||||||
for format_option in ["wav", "mp3", "flac", "opus"]:
|
for format_option in ["wav", "flac", "mp3", "opus"]:
|
||||||
format_action = QAction(format_option, self)
|
format_action = QAction(format_option, self)
|
||||||
format_action.setCheckable(True)
|
format_action.setCheckable(True)
|
||||||
format_action.setChecked(self.separate_chapters_format == format_option)
|
format_action.setChecked(self.separate_chapters_format == format_option)
|
||||||
@@ -2475,6 +2495,11 @@ Categories=AudioVideo;Audio;Utility;
|
|||||||
self.config["separate_chapters_format"] = fmt
|
self.config["separate_chapters_format"] = fmt
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
|
|
||||||
|
def set_subtitle_format(self, fmt):
|
||||||
|
"""Set the subtitle format."""
|
||||||
|
self.config["subtitle_format"] = fmt
|
||||||
|
save_config(self.config)
|
||||||
|
|
||||||
def show_model_download_warning(self, title, message):
|
def show_model_download_warning(self, title, message):
|
||||||
QMessageBox.information(self, title, message)
|
QMessageBox.information(self, title, message)
|
||||||
|
|
||||||
|
|||||||
+6
-15
@@ -7,9 +7,10 @@ https://github.com/user-attachments/assets/9e4fc237-a3cd-46bd-b82c-c608336d6411
|
|||||||
### What You Need
|
### What You Need
|
||||||
|
|
||||||
- A background image (bg.jpg)
|
- A background image (bg.jpg)
|
||||||
- The subtitle file (.srt) **(created by Abogen)**
|
- The subtitle file (.ass) **(created by Abogen)**
|
||||||
|
- Select `ass(centered narrow)` as subtitle format in settings.
|
||||||
- The audio recording (.wav) **(created by Abogen)**
|
- The audio recording (.wav) **(created by Abogen)**
|
||||||
- [Python](https://www.python.org/downloads/) and FFmpeg installed on your computer:
|
- FFmpeg installed on your computer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Windows
|
# Windows
|
||||||
@@ -22,22 +23,12 @@ brew install ffmpeg
|
|||||||
sudo apt install ffmpeg
|
sudo apt install ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 1: Process the Subtitle File
|
### Create the Video (.webm)
|
||||||
|
|
||||||
Run this command to process Abogen's subtitle file:
|
|
||||||
|
|
||||||
```
|
|
||||||
python convert.py your_subtitle.srt
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates a properly formatted subtitle file called "your_subtitle_demo.ass" with centered text and appropriate styling.
|
|
||||||
|
|
||||||
### Step 2: Create the Video (.webm)
|
|
||||||
|
|
||||||
Run this FFmpeg command to create the tiny video:
|
Run this FFmpeg command to create the tiny video:
|
||||||
|
|
||||||
```
|
```
|
||||||
ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.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 audio.wav -vf "ass=subtitle.ass" -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus -shortest demo.webm
|
||||||
```
|
```
|
||||||
|
|
||||||
## For Higher Quality (But Larger) Video (.mp4)
|
## For Higher Quality (But Larger) Video (.mp4)
|
||||||
@@ -45,7 +36,7 @@ ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.
|
|||||||
If you need better quality for distribution, use this command instead:
|
If you need better quality for distribution, use this command instead:
|
||||||
|
|
||||||
```
|
```
|
||||||
ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.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 audio.wav -vf "ass=subtitle.ass" -c:v libx264 -preset slow -crf 18 -movflags +faststart -c:a copy -shortest demo.mp4
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates an MP4 file that's compatible with more devices but larger in size.
|
This creates an MP4 file that's compatible with more devices but larger in size.
|
||||||
|
|||||||
-220
@@ -1,220 +0,0 @@
|
|||||||
# FFmpeg commands for creating videos:
|
|
||||||
#
|
|
||||||
# For WebM (smaller filesize):
|
|
||||||
# ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.ass" -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus -shortest demo.webm
|
|
||||||
#
|
|
||||||
# For MP4 (higher quality):
|
|
||||||
# ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.ass" -c:v libx264 -preset slow -crf 18 -movflags +faststart -c:a copy -shortest demo.mp4
|
|
||||||
#
|
|
||||||
# Note: Replace 'audio.wav' with your audio file and 'your_subtitle_demo.ass' with your processed subtitle file
|
|
||||||
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
|
|
||||||
def parse_time(s):
|
|
||||||
# For ASS format
|
|
||||||
if ":" in s and "." in s:
|
|
||||||
h, m, s = s.split(":")
|
|
||||||
sec, cs = s.split(".")
|
|
||||||
return timedelta(
|
|
||||||
hours=int(h), minutes=int(m), seconds=int(sec), milliseconds=int(cs) * 10
|
|
||||||
)
|
|
||||||
# For SRT format (00:00:00,000)
|
|
||||||
elif ":" in s and "," in s:
|
|
||||||
parts = s.split(":")
|
|
||||||
h = int(parts[0])
|
|
||||||
m = int(parts[1])
|
|
||||||
sec_parts = parts[2].split(",")
|
|
||||||
sec = int(sec_parts[0])
|
|
||||||
ms = int(sec_parts[1])
|
|
||||||
return timedelta(hours=h, minutes=m, seconds=sec, milliseconds=ms)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def format_time(t):
|
|
||||||
total_seconds = int(t.total_seconds())
|
|
||||||
cs = int((t.total_seconds() - total_seconds) * 100)
|
|
||||||
h = total_seconds // 3600
|
|
||||||
m = (total_seconds % 3600) // 60
|
|
||||||
s = total_seconds % 60
|
|
||||||
return f"{h}:{m:02}:{s:02}.{cs:02}"
|
|
||||||
|
|
||||||
|
|
||||||
# Desired script info and style
|
|
||||||
DESIRED_SCRIPT_INFO = """[Script Info]
|
|
||||||
Title: Centered Subs
|
|
||||||
ScriptType: v4.00+
|
|
||||||
PlayResX: 1280
|
|
||||||
PlayResY: 720
|
|
||||||
"""
|
|
||||||
|
|
||||||
DESIRED_STYLE = """[V4+ Styles]
|
|
||||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
||||||
Style: Default,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H64000000,-1,0,0,0,100,100,0,0,1,2,0,5,10,10,10,1
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def process_subtitle_file(input_file):
|
|
||||||
output_file = input_file.replace(".ass", "_demo.ass").replace(".srt", "_demo.ass")
|
|
||||||
if output_file == input_file:
|
|
||||||
output_file = f"{input_file}_demo.ass"
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(input_file, "r", encoding="utf-8") as f:
|
|
||||||
lines = f.readlines()
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"❌ Error: Input file '{input_file}' not found")
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error reading file: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Check if it's an SRT file
|
|
||||||
is_srt = input_file.lower().endswith(".srt")
|
|
||||||
|
|
||||||
if is_srt:
|
|
||||||
return convert_srt_to_ass(input_file, output_file, lines)
|
|
||||||
|
|
||||||
# Process ASS file
|
|
||||||
header = []
|
|
||||||
events = []
|
|
||||||
in_events = False
|
|
||||||
script_info_found = False
|
|
||||||
styles_found = False
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if line.strip().startswith("[Script Info]"):
|
|
||||||
script_info_found = True
|
|
||||||
header.append(DESIRED_SCRIPT_INFO)
|
|
||||||
continue
|
|
||||||
elif line.strip().startswith("[V4+ Styles]"):
|
|
||||||
styles_found = True
|
|
||||||
header.append(DESIRED_STYLE)
|
|
||||||
continue
|
|
||||||
elif line.strip().startswith("[Events]"):
|
|
||||||
in_events = True
|
|
||||||
header.append(line)
|
|
||||||
elif not in_events:
|
|
||||||
if not script_info_found and not styles_found:
|
|
||||||
header.append(line)
|
|
||||||
else:
|
|
||||||
if line.startswith("Dialogue:"):
|
|
||||||
events.append(line)
|
|
||||||
else:
|
|
||||||
header.append(line)
|
|
||||||
|
|
||||||
parsed_events = []
|
|
||||||
for line in events:
|
|
||||||
match = re.match(r"Dialogue: \d,([^,]+),([^,]+),", line)
|
|
||||||
if not match:
|
|
||||||
parsed_events.append((None, None, line))
|
|
||||||
continue
|
|
||||||
start = parse_time(match.group(1))
|
|
||||||
end = parse_time(match.group(2))
|
|
||||||
parsed_events.append((start, end, line))
|
|
||||||
|
|
||||||
# Cut overlaps
|
|
||||||
fixed_lines = []
|
|
||||||
for i in range(len(parsed_events)):
|
|
||||||
start, end, line = parsed_events[i]
|
|
||||||
if start is None:
|
|
||||||
fixed_lines.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check for next subtitle
|
|
||||||
if i + 1 < len(parsed_events):
|
|
||||||
next_start, _, _ = parsed_events[i + 1]
|
|
||||||
if end > next_start:
|
|
||||||
end = next_start # cut current subtitle to stop at next one's start
|
|
||||||
|
|
||||||
fixed_line = re.sub(
|
|
||||||
r"Dialogue: \d,[^,]+,[^,]+,",
|
|
||||||
f"Dialogue: 0,{format_time(start)},{format_time(end)},",
|
|
||||||
line,
|
|
||||||
count=1,
|
|
||||||
)
|
|
||||||
fixed_lines.append(fixed_line)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(output_file, "w", encoding="utf-8") as f:
|
|
||||||
f.writelines(header)
|
|
||||||
f.writelines(fixed_lines)
|
|
||||||
print(f"✅ Successfully processed. Output file: {output_file}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error writing output file: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_srt_to_ass(input_file, output_file, lines):
|
|
||||||
"""Convert SRT format to ASS format."""
|
|
||||||
events = []
|
|
||||||
subtitle_blocks = []
|
|
||||||
current_block = []
|
|
||||||
|
|
||||||
# Parse SRT file
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
if current_block:
|
|
||||||
subtitle_blocks.append(current_block)
|
|
||||||
current_block = []
|
|
||||||
else:
|
|
||||||
current_block.append(line)
|
|
||||||
|
|
||||||
# Don't forget the last block
|
|
||||||
if current_block:
|
|
||||||
subtitle_blocks.append(current_block)
|
|
||||||
|
|
||||||
# Process subtitle blocks
|
|
||||||
for block in subtitle_blocks:
|
|
||||||
if len(block) < 3:
|
|
||||||
continue # Invalid block
|
|
||||||
|
|
||||||
# Skip the subtitle number
|
|
||||||
timing_line = block[1]
|
|
||||||
|
|
||||||
# Parse timing information
|
|
||||||
timing_match = re.match(
|
|
||||||
r"(\d+:\d+:\d+,\d+)\s+-->\s+(\d+:\d+:\d+,\d+)", timing_line
|
|
||||||
)
|
|
||||||
if not timing_match:
|
|
||||||
continue
|
|
||||||
|
|
||||||
start_time = parse_time(timing_match.group(1))
|
|
||||||
end_time = parse_time(timing_match.group(2))
|
|
||||||
|
|
||||||
# Combine text lines
|
|
||||||
text = "\\N".join(block[2:])
|
|
||||||
|
|
||||||
# Create ASS Dialogue line
|
|
||||||
dialogue_line = f"Dialogue: 0,{format_time(start_time)},{format_time(end_time)},Default,,0,0,0,,{text}\n"
|
|
||||||
events.append(dialogue_line)
|
|
||||||
|
|
||||||
# Create ASS file
|
|
||||||
try:
|
|
||||||
with open(output_file, "w", encoding="utf-8") as f:
|
|
||||||
f.write(DESIRED_SCRIPT_INFO)
|
|
||||||
f.write(DESIRED_STYLE)
|
|
||||||
f.write("[Events]\n")
|
|
||||||
f.write(
|
|
||||||
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
|
||||||
)
|
|
||||||
f.writelines(events)
|
|
||||||
print(f"✅ Successfully converted SRT to ASS. Output file: {output_file}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error writing output file: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("❌ Error: No input file specified")
|
|
||||||
print(f"Usage: {sys.argv[0]} <input_file.ass|input_file.srt>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
input_file = sys.argv[1]
|
|
||||||
process_subtitle_file(input_file)
|
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
[Script Info]
|
||||||
|
Title: Generated by Abogen
|
||||||
|
ScriptType: v4.00+
|
||||||
|
|
||||||
|
[Events]
|
||||||
|
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.83,0:00:00.96,Default,,100,100,0,,{\an5}is
|
||||||
|
Dialogue: 0,0:00:00.96,0:00:01.11,Default,,100,100,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.66,0:00:02.04,Default,,100,100,0,,{\an5}tool
|
||||||
|
Dialogue: 0,0:00:02.04,0:00:02.17,Default,,100,100,0,,{\an5}for
|
||||||
|
Dialogue: 0,0:00:02.17,0:00:02.53,Default,,100,100,0,,{\an5}turning
|
||||||
|
Dialogue: 0,0:00:02.53,0:00:03.22,Default,,100,100,0,,{\an5}text
|
||||||
|
Dialogue: 0,0:00:03.22,0:00:03.45,Default,,100,100,0,,{\an5}into
|
||||||
|
Dialogue: 0,0:00:03.45,0:00:03.75,Default,,100,100,0,,{\an5}something
|
||||||
|
Dialogue: 0,0:00:03.75,0:00:03.85,Default,,100,100,0,,{\an5}you
|
||||||
|
Dialogue: 0,0:00:03.85,0:00:04.04,Default,,100,100,0,,{\an5}can
|
||||||
|
Dialogue: 0,0:00:04.04,0:00:04.32,Default,,100,100,0,,{\an5}see
|
||||||
|
Dialogue: 0,0:00:04.32,0:00:04.45,Default,,100,100,0,,{\an5}and
|
||||||
|
Dialogue: 0,0:00:04.45,0:00:05.61,Default,,100,100,0,,{\an5}hear.
|
||||||
|
Dialogue: 0,0:00:05.61,0:00:06.02,Default,,100,100,0,,{\an5}Easily
|
||||||
|
Dialogue: 0,0:00:06.02,0:00:06.48,Default,,100,100,0,,{\an5}convert
|
||||||
|
Dialogue: 0,0:00:06.48,0:00:07.31,Default,,100,100,0,,{\an5}ePub,
|
||||||
|
Dialogue: 0,0:00:07.31,0:00:08.40,Default,,100,100,0,,{\an5}PDF,
|
||||||
|
Dialogue: 0,0:00:08.40,0:00:08.55,Default,,100,100,0,,{\an5}or
|
||||||
|
Dialogue: 0,0:00:08.55,0:00:08.90,Default,,100,100,0,,{\an5}text
|
||||||
|
Dialogue: 0,0:00:08.90,0:00:09.21,Default,,100,100,0,,{\an5}file
|
||||||
|
Dialogue: 0,0:00:09.21,0:00:09.46,Default,,100,100,0,,{\an5}into
|
||||||
|
Dialogue: 0,0:00:09.46,0:00:10.34,Default,,100,100,0,,{\an5}audio
|
||||||
|
Dialogue: 0,0:00:10.34,0:00:10.56,Default,,100,100,0,,{\an5}with
|
||||||
|
Dialogue: 0,0:00:10.56,0:00:11.17,Default,,100,100,0,,{\an5}subtitles
|
||||||
|
Dialogue: 0,0:00:11.17,0:00:11.32,Default,,100,100,0,,{\an5}that
|
||||||
|
Dialogue: 0,0:00:11.32,0:00:11.51,Default,,100,100,0,,{\an5}light
|
||||||
|
Dialogue: 0,0:00:11.51,0:00:11.71,Default,,100,100,0,,{\an5}up
|
||||||
|
Dialogue: 0,0:00:11.71,0:00:12.05,Default,,100,100,0,,{\an5}every
|
||||||
|
Dialogue: 0,0:00:12.05,0:00:12.40,Default,,100,100,0,,{\an5}word
|
||||||
|
Dialogue: 0,0:00:12.40,0:00:12.51,Default,,100,100,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.65,0:00:14.21,Default,,100,100,0,,{\an5}spoken.
|
||||||
|
Dialogue: 0,0:00:14.21,0:00:14.32,Default,,100,100,0,,{\an5}The
|
||||||
|
Dialogue: 0,0:00:14.32,0:00:14.62,Default,,100,100,0,,{\an5}voice
|
||||||
|
Dialogue: 0,0:00:14.62,0:00:14.77,Default,,100,100,0,,{\an5}you
|
||||||
|
Dialogue: 0,0:00:14.77,0:00:15.65,Default,,100,100,0,,{\an5}hear,
|
||||||
|
Dialogue: 0,0:00:15.65,0:00:15.76,Default,,100,100,0,,{\an5}and
|
||||||
|
Dialogue: 0,0:00:15.76,0:00:15.86,Default,,100,100,0,,{\an5}the
|
||||||
|
Dialogue: 0,0:00:15.86,0:00:16.44,Default,,100,100,0,,{\an5}subtitles
|
||||||
|
Dialogue: 0,0:00:16.44,0:00:16.58,Default,,100,100,0,,{\an5}you
|
||||||
|
Dialogue: 0,0:00:16.58,0:00:17.03,Default,,100,100,0,,{\an5}see
|
||||||
|
Dialogue: 0,0:00:17.03,0:00:17.14,Default,,100,100,0,,{\an5}are
|
||||||
|
Dialogue: 0,0:00:17.14,0:00:17.44,Default,,100,100,0,,{\an5}both
|
||||||
|
Dialogue: 0,0:00:17.44,0:00:17.91,Default,,100,100,0,,{\an5}created
|
||||||
|
Dialogue: 0,0:00:17.91,0:00:18.96,Default,,100,100,0,,{\an5}automatically
|
||||||
|
Dialogue: 0,0:00:18.96,0:00:19.14,Default,,100,100,0,,{\an5}by
|
||||||
|
Dialogue: 0,0:00:19.14,0:00:19.64,Default,,100,100,0,,{\an5}Abogen
|
||||||
|
Dialogue: 0,0:00:19.64,0:00:20.00,Default,,100,100,0,,{\an5}using
|
||||||
|
Dialogue: 0,0:00:20.00,0:00:21.14,Default,,100,100,0,,{\an5}Kokoro,
|
||||||
|
Dialogue: 0,0:00:21.14,0:00:21.28,Default,,100,100,0,,{\an5}an
|
||||||
|
Dialogue: 0,0:00:21.28,0:00:21.88,Default,,100,100,0,,{\an5}open-weight
|
||||||
|
Dialogue: 0,0:00:21.88,0:00:22.52,Default,,100,100,0,,{\an5}TTS
|
||||||
|
Dialogue: 0,0:00:22.52,0:00:23.18,Default,,100,100,0,,{\an5}model
|
||||||
|
Dialogue: 0,0:00:23.18,0:00:23.38,Default,,100,100,0,,{\an5}with
|
||||||
|
Dialogue: 0,0:00:23.38,0:00:24.01,Default,,100,100,0,,{\an5}82
|
||||||
|
Dialogue: 0,0:00:24.01,0:00:24.37,Default,,100,100,0,,{\an5}million
|
||||||
|
Dialogue: 0,0:00:24.37,0:00:25.98,Default,,100,100,0,,{\an5}parameters.
|
||||||
|
Dialogue: 0,0:00:25.98,0:00:26.47,Default,,100,100,0,,{\an5}Everything
|
||||||
|
Dialogue: 0,0:00:26.47,0:00:26.77,Default,,100,100,0,,{\an5}works
|
||||||
|
Dialogue: 0,0:00:26.77,0:00:27.01,Default,,100,100,0,,{\an5}right
|
||||||
|
Dialogue: 0,0:00:27.01,0:00:27.11,Default,,100,100,0,,{\an5}on
|
||||||
|
Dialogue: 0,0:00:27.11,0:00:27.22,Default,,100,100,0,,{\an5}your
|
||||||
|
Dialogue: 0,0:00:27.22,0:00:28.23,Default,,100,100,0,,{\an5}computer,
|
||||||
|
Dialogue: 0,0:00:28.23,0:00:28.37,Default,,100,100,0,,{\an5}so
|
||||||
|
Dialogue: 0,0:00:28.37,0:00:28.53,Default,,100,100,0,,{\an5}your
|
||||||
|
Dialogue: 0,0:00:28.53,0:00:28.94,Default,,100,100,0,,{\an5}files
|
||||||
|
Dialogue: 0,0:00:28.94,0:00:29.23,Default,,100,100,0,,{\an5}stay
|
||||||
|
Dialogue: 0,0:00:29.23,0:00:29.88,Default,,100,100,0,,{\an5}private
|
||||||
|
Dialogue: 0,0:00:29.88,0:00:30.05,Default,,100,100,0,,{\an5}and
|
||||||
|
Dialogue: 0,0:00:30.05,0:00:31.16,Default,,100,100,0,,{\an5}secure.
|
||||||
|
Dialogue: 0,0:00:31.50,0:00:31.63,Default,,100,100,0,,{\an5}The
|
||||||
|
Dialogue: 0,0:00:31.63,0:00:31.97,Default,,100,100,0,,{\an5}easy
|
||||||
|
Dialogue: 0,0:00:31.97,0:00:32.54,Default,,100,100,0,,{\an5}interface
|
||||||
|
Dialogue: 0,0:00:32.54,0:00:32.81,Default,,100,100,0,,{\an5}lets
|
||||||
|
Dialogue: 0,0:00:32.81,0:00:33.23,Default,,100,100,0,,{\an5}anyone
|
||||||
|
Dialogue: 0,0:00:33.23,0:00:33.66,Default,,100,100,0,,{\an5}create
|
||||||
|
Dialogue: 0,0:00:33.66,0:00:34.88,Default,,100,100,0,,{\an5}audiobooks,
|
||||||
|
Dialogue: 0,0:00:34.88,0:00:35.54,Default,,100,100,0,,{\an5}voiceovers
|
||||||
|
Dialogue: 0,0:00:35.54,0:00:35.70,Default,,100,100,0,,{\an5}for
|
||||||
|
Dialogue: 0,0:00:35.70,0:00:36.68,Default,,100,100,0,,{\an5}Instagram,
|
||||||
|
Dialogue: 0,0:00:36.68,0:00:37.23,Default,,100,100,0,,{\an5}YouTube
|
||||||
|
Dialogue: 0,0:00:37.23,0:00:37.41,Default,,100,100,0,,{\an5}or
|
||||||
|
Dialogue: 0,0:00:37.41,0:00:38.52,Default,,100,100,0,,{\an5}TikTok,
|
||||||
|
Dialogue: 0,0:00:38.52,0:00:38.64,Default,,100,100,0,,{\an5}or
|
||||||
|
Dialogue: 0,0:00:38.64,0:00:38.89,Default,,100,100,0,,{\an5}just
|
||||||
|
Dialogue: 0,0:00:38.89,0:00:39.57,Default,,100,100,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:40.39,0:00:40.56,Default,,100,100,0,,{\an5}in
|
||||||
|
Dialogue: 0,0:00:40.56,0:00:42.03,Default,,100,100,0,,{\an5}seconds.
|
||||||
|
Dialogue: 0,0:00:42.03,0:00:42.32,Default,,100,100,0,,{\an5}Whether
|
||||||
|
Dialogue: 0,0:00:42.32,0:00:42.47,Default,,100,100,0,,{\an5}you
|
||||||
|
Dialogue: 0,0:00:42.47,0:00:42.67,Default,,100,100,0,,{\an5}want
|
||||||
|
Dialogue: 0,0:00:42.67,0:00:42.77,Default,,100,100,0,,{\an5}to
|
||||||
|
Dialogue: 0,0:00:42.77,0:00:43.43,Default,,100,100,0,,{\an5}listen,
|
||||||
|
Dialogue: 0,0:00:43.43,0:00:43.77,Default,,100,100,0,,{\an5}create
|
||||||
|
Dialogue: 0,0:00:43.77,0:00:44.70,Default,,100,100,0,,{\an5}content,
|
||||||
|
Dialogue: 0,0:00:44.70,0:00:44.85,Default,,100,100,0,,{\an5}or
|
||||||
|
Dialogue: 0,0:00:44.85,0:00:45.07,Default,,100,100,0,,{\an5}bring
|
||||||
|
Dialogue: 0,0:00:45.07,0:00:45.22,Default,,100,100,0,,{\an5}your
|
||||||
|
Dialogue: 0,0:00:45.22,0:00:45.54,Default,,100,100,0,,{\an5}words
|
||||||
|
Dialogue: 0,0:00:45.54,0:00:45.67,Default,,100,100,0,,{\an5}to
|
||||||
|
Dialogue: 0,0:00:45.67,0:00:46.58,Default,,100,100,0,,{\an5}life,
|
||||||
|
Dialogue: 0,0:00:46.58,0:00:47.10,Default,,100,100,0,,{\an5}Abogen
|
||||||
|
Dialogue: 0,0:00:47.10,0:00:47.37,Default,,100,100,0,,{\an5}makes
|
||||||
|
Dialogue: 0,0:00:47.37,0:00:47.53,Default,,100,100,0,,{\an5}it
|
||||||
|
Dialogue: 0,0:00:47.53,0:00:48.31,Default,,100,100,0,,{\an5}fast,
|
||||||
|
Dialogue: 0,0:00:48.31,0:00:48.89,Default,,100,100,0,,{\an5}fun,
|
||||||
|
Dialogue: 0,0:00:48.89,0:00:49.02,Default,,100,100,0,,{\an5}and
|
||||||
|
Dialogue: 0,0:00:49.02,0:00:50.62,Default,,100,100,0,,{\an5}effortless.
|
||||||
|
Dialogue: 0,0:00:50.62,0:00:51.35,Default,,100,100,0,,{\an5}Experience
|
||||||
|
Dialogue: 0,0:00:51.35,0:00:51.47,Default,,100,100,0,,{\an5}your
|
||||||
|
Dialogue: 0,0:00:51.47,0:00:52.23,Default,,100,100,0,,{\an5}stories,
|
||||||
|
Dialogue: 0,0:00:52.23,0:00:52.97,Default,,100,100,0,,{\an5}scripts,
|
||||||
|
Dialogue: 0,0:00:52.97,0:00:53.11,Default,,100,100,0,,{\an5}and
|
||||||
|
Dialogue: 0,0:00:53.11,0:00:53.64,Default,,100,100,0,,{\an5}ideas
|
||||||
|
Dialogue: 0,0:00:53.64,0:00:53.73,Default,,100,100,0,,{\an5}in
|
||||||
|
Dialogue: 0,0:00:53.73,0:00:53.83,Default,,100,100,0,,{\an5}a
|
||||||
|
Dialogue: 0,0:00:53.83,0:00:54.14,Default,,100,100,0,,{\an5}whole
|
||||||
|
Dialogue: 0,0:00:54.14,0:00:54.35,Default,,100,100,0,,{\an5}new
|
||||||
|
Dialogue: 0,0:00:54.35,0:00:54.64,Default,,100,100,0,,{\an5}way
|
||||||
|
Dialogue: 0,0:00:54.64,0:00:54.79,Default,,100,100,0,,{\an5}with
|
||||||
|
Dialogue: 0,0:00:54.79,0:00:55.85,Default,,100,100,0,,{\an5}Abogen.
|
||||||
-480
@@ -1,480 +0,0 @@
|
|||||||
1
|
|
||||||
00:00:00,350 --> 00:00:00,962
|
|
||||||
Abogen
|
|
||||||
|
|
||||||
2
|
|
||||||
00:00:00,962 --> 00:00:01,100
|
|
||||||
is
|
|
||||||
|
|
||||||
3
|
|
||||||
00:00:01,100 --> 00:00:01,300
|
|
||||||
your
|
|
||||||
|
|
||||||
4
|
|
||||||
00:00:01,300 --> 00:00:01,887
|
|
||||||
all-in-one
|
|
||||||
|
|
||||||
5
|
|
||||||
00:00:01,887 --> 00:00:02,250
|
|
||||||
tool
|
|
||||||
|
|
||||||
6
|
|
||||||
00:00:02,250 --> 00:00:02,375
|
|
||||||
for
|
|
||||||
|
|
||||||
7
|
|
||||||
00:00:02,375 --> 00:00:02,762
|
|
||||||
turning
|
|
||||||
|
|
||||||
8
|
|
||||||
00:00:02,762 --> 00:00:03,162
|
|
||||||
text
|
|
||||||
|
|
||||||
9
|
|
||||||
00:00:03,162 --> 00:00:03,375
|
|
||||||
into
|
|
||||||
|
|
||||||
10
|
|
||||||
00:00:03,375 --> 00:00:03,725
|
|
||||||
something
|
|
||||||
|
|
||||||
11
|
|
||||||
00:00:03,725 --> 00:00:03,825
|
|
||||||
you
|
|
||||||
|
|
||||||
12
|
|
||||||
00:00:03,825 --> 00:00:04,025
|
|
||||||
can
|
|
||||||
|
|
||||||
13
|
|
||||||
00:00:04,025 --> 00:00:04,312
|
|
||||||
see
|
|
||||||
|
|
||||||
14
|
|
||||||
00:00:04,312 --> 00:00:04,450
|
|
||||||
and
|
|
||||||
|
|
||||||
15
|
|
||||||
00:00:04,450 --> 00:00:05,450
|
|
||||||
hear.
|
|
||||||
|
|
||||||
16
|
|
||||||
00:00:05,450 --> 00:00:05,900
|
|
||||||
Easily
|
|
||||||
|
|
||||||
17
|
|
||||||
00:00:05,900 --> 00:00:06,337
|
|
||||||
convert
|
|
||||||
|
|
||||||
18
|
|
||||||
00:00:06,337 --> 00:00:07,187
|
|
||||||
ePub,
|
|
||||||
|
|
||||||
19
|
|
||||||
00:00:07,187 --> 00:00:08,250
|
|
||||||
PDF,
|
|
||||||
|
|
||||||
20
|
|
||||||
00:00:08,250 --> 00:00:08,412
|
|
||||||
or
|
|
||||||
|
|
||||||
21
|
|
||||||
00:00:08,412 --> 00:00:08,800
|
|
||||||
text
|
|
||||||
|
|
||||||
22
|
|
||||||
00:00:08,800 --> 00:00:09,162
|
|
||||||
file
|
|
||||||
|
|
||||||
23
|
|
||||||
00:00:09,162 --> 00:00:09,425
|
|
||||||
into
|
|
||||||
|
|
||||||
24
|
|
||||||
00:00:09,425 --> 00:00:10,037
|
|
||||||
audio
|
|
||||||
|
|
||||||
25
|
|
||||||
00:00:10,037 --> 00:00:10,237
|
|
||||||
with
|
|
||||||
|
|
||||||
26
|
|
||||||
00:00:10,237 --> 00:00:10,875
|
|
||||||
subtitles
|
|
||||||
|
|
||||||
27
|
|
||||||
00:00:10,875 --> 00:00:11,087
|
|
||||||
that
|
|
||||||
|
|
||||||
28
|
|
||||||
00:00:11,087 --> 00:00:11,287
|
|
||||||
light
|
|
||||||
|
|
||||||
29
|
|
||||||
00:00:11,287 --> 00:00:11,487
|
|
||||||
up
|
|
||||||
|
|
||||||
30
|
|
||||||
00:00:11,487 --> 00:00:11,824
|
|
||||||
every
|
|
||||||
|
|
||||||
31
|
|
||||||
00:00:11,824 --> 00:00:12,175
|
|
||||||
word
|
|
||||||
|
|
||||||
32
|
|
||||||
00:00:12,175 --> 00:00:12,300
|
|
||||||
as
|
|
||||||
|
|
||||||
33
|
|
||||||
00:00:12,300 --> 00:00:12,449
|
|
||||||
it’s
|
|
||||||
|
|
||||||
34
|
|
||||||
00:00:12,449 --> 00:00:13,787
|
|
||||||
spoken.
|
|
||||||
|
|
||||||
35
|
|
||||||
00:00:13,787 --> 00:00:13,925
|
|
||||||
The
|
|
||||||
|
|
||||||
36
|
|
||||||
00:00:13,925 --> 00:00:14,275
|
|
||||||
voice
|
|
||||||
|
|
||||||
37
|
|
||||||
00:00:14,275 --> 00:00:14,425
|
|
||||||
you
|
|
||||||
|
|
||||||
38
|
|
||||||
00:00:14,425 --> 00:00:15,212
|
|
||||||
hear,
|
|
||||||
|
|
||||||
39
|
|
||||||
00:00:15,212 --> 00:00:15,337
|
|
||||||
and
|
|
||||||
|
|
||||||
40
|
|
||||||
00:00:15,337 --> 00:00:15,437
|
|
||||||
the
|
|
||||||
|
|
||||||
41
|
|
||||||
00:00:15,437 --> 00:00:16,024
|
|
||||||
subtitles
|
|
||||||
|
|
||||||
42
|
|
||||||
00:00:16,024 --> 00:00:16,162
|
|
||||||
you
|
|
||||||
|
|
||||||
43
|
|
||||||
00:00:16,162 --> 00:00:16,487
|
|
||||||
see
|
|
||||||
|
|
||||||
44
|
|
||||||
00:00:16,487 --> 00:00:16,649
|
|
||||||
are
|
|
||||||
|
|
||||||
45
|
|
||||||
00:00:16,649 --> 00:00:16,975
|
|
||||||
both
|
|
||||||
|
|
||||||
46
|
|
||||||
00:00:16,975 --> 00:00:17,449
|
|
||||||
created
|
|
||||||
|
|
||||||
47
|
|
||||||
00:00:17,449 --> 00:00:18,362
|
|
||||||
automatically
|
|
||||||
|
|
||||||
48
|
|
||||||
00:00:18,362 --> 00:00:18,550
|
|
||||||
by
|
|
||||||
|
|
||||||
49
|
|
||||||
00:00:18,550 --> 00:00:19,074
|
|
||||||
Abogen
|
|
||||||
|
|
||||||
50
|
|
||||||
00:00:19,074 --> 00:00:19,425
|
|
||||||
using
|
|
||||||
|
|
||||||
51
|
|
||||||
00:00:19,425 --> 00:00:20,512
|
|
||||||
Kokoro,
|
|
||||||
|
|
||||||
52
|
|
||||||
00:00:20,512 --> 00:00:20,637
|
|
||||||
a
|
|
||||||
|
|
||||||
53
|
|
||||||
00:00:20,637 --> 00:00:21,237
|
|
||||||
powerful
|
|
||||||
|
|
||||||
54
|
|
||||||
00:00:21,237 --> 00:00:22,050
|
|
||||||
text-to-speech
|
|
||||||
|
|
||||||
55
|
|
||||||
00:00:22,050 --> 00:00:23,225
|
|
||||||
engine.
|
|
||||||
|
|
||||||
56
|
|
||||||
00:00:23,225 --> 00:00:23,750
|
|
||||||
Everything
|
|
||||||
|
|
||||||
57
|
|
||||||
00:00:23,750 --> 00:00:24,062
|
|
||||||
works
|
|
||||||
|
|
||||||
58
|
|
||||||
00:00:24,062 --> 00:00:24,312
|
|
||||||
right
|
|
||||||
|
|
||||||
59
|
|
||||||
00:00:24,312 --> 00:00:24,412
|
|
||||||
on
|
|
||||||
|
|
||||||
60
|
|
||||||
00:00:24,412 --> 00:00:24,524
|
|
||||||
your
|
|
||||||
|
|
||||||
61
|
|
||||||
00:00:24,524 --> 00:00:25,437
|
|
||||||
computer,
|
|
||||||
|
|
||||||
62
|
|
||||||
00:00:25,437 --> 00:00:25,625
|
|
||||||
so
|
|
||||||
|
|
||||||
63
|
|
||||||
00:00:25,625 --> 00:00:25,812
|
|
||||||
your
|
|
||||||
|
|
||||||
64
|
|
||||||
00:00:25,812 --> 00:00:26,225
|
|
||||||
files
|
|
||||||
|
|
||||||
65
|
|
||||||
00:00:26,225 --> 00:00:26,524
|
|
||||||
stay
|
|
||||||
|
|
||||||
66
|
|
||||||
00:00:26,524 --> 00:00:27,112
|
|
||||||
private
|
|
||||||
|
|
||||||
67
|
|
||||||
00:00:27,112 --> 00:00:27,274
|
|
||||||
and
|
|
||||||
|
|
||||||
68
|
|
||||||
00:00:27,274 --> 00:00:28,337
|
|
||||||
secure.
|
|
||||||
|
|
||||||
69
|
|
||||||
00:00:28,725 --> 00:00:28,862
|
|
||||||
The
|
|
||||||
|
|
||||||
70
|
|
||||||
00:00:28,862 --> 00:00:29,225
|
|
||||||
easy
|
|
||||||
|
|
||||||
71
|
|
||||||
00:00:29,225 --> 00:00:29,824
|
|
||||||
interface
|
|
||||||
|
|
||||||
72
|
|
||||||
00:00:29,824 --> 00:00:30,112
|
|
||||||
lets
|
|
||||||
|
|
||||||
73
|
|
||||||
00:00:30,112 --> 00:00:30,637
|
|
||||||
anyone
|
|
||||||
|
|
||||||
74
|
|
||||||
00:00:30,637 --> 00:00:31,062
|
|
||||||
create
|
|
||||||
|
|
||||||
75
|
|
||||||
00:00:31,062 --> 00:00:32,299
|
|
||||||
audiobooks,
|
|
||||||
|
|
||||||
76
|
|
||||||
00:00:32,299 --> 00:00:33,075
|
|
||||||
voiceovers
|
|
||||||
|
|
||||||
77
|
|
||||||
00:00:33,075 --> 00:00:33,237
|
|
||||||
for
|
|
||||||
|
|
||||||
78
|
|
||||||
00:00:33,237 --> 00:00:34,325
|
|
||||||
Instagram,
|
|
||||||
|
|
||||||
79
|
|
||||||
00:00:34,325 --> 00:00:34,962
|
|
||||||
YouTube
|
|
||||||
|
|
||||||
80
|
|
||||||
00:00:34,962 --> 00:00:35,137
|
|
||||||
or
|
|
||||||
|
|
||||||
81
|
|
||||||
00:00:35,137 --> 00:00:35,950
|
|
||||||
TikTok,
|
|
||||||
|
|
||||||
82
|
|
||||||
00:00:35,950 --> 00:00:36,100
|
|
||||||
or
|
|
||||||
|
|
||||||
83
|
|
||||||
00:00:36,100 --> 00:00:36,325
|
|
||||||
just
|
|
||||||
|
|
||||||
84
|
|
||||||
00:00:36,325 --> 00:00:37,024
|
|
||||||
high-quality
|
|
||||||
|
|
||||||
85
|
|
||||||
00:00:37,024 --> 00:00:37,850
|
|
||||||
text-to-speech
|
|
||||||
|
|
||||||
86
|
|
||||||
00:00:37,850 --> 00:00:38,000
|
|
||||||
in
|
|
||||||
|
|
||||||
87
|
|
||||||
00:00:38,000 --> 00:00:39,187
|
|
||||||
seconds.
|
|
||||||
|
|
||||||
88
|
|
||||||
00:00:39,187 --> 00:00:39,475
|
|
||||||
Whether
|
|
||||||
|
|
||||||
89
|
|
||||||
00:00:39,475 --> 00:00:39,625
|
|
||||||
you
|
|
||||||
|
|
||||||
90
|
|
||||||
00:00:39,625 --> 00:00:39,825
|
|
||||||
want
|
|
||||||
|
|
||||||
91
|
|
||||||
00:00:39,825 --> 00:00:39,924
|
|
||||||
to
|
|
||||||
|
|
||||||
92
|
|
||||||
00:00:39,924 --> 00:00:40,662
|
|
||||||
listen,
|
|
||||||
|
|
||||||
93
|
|
||||||
00:00:40,662 --> 00:00:41,000
|
|
||||||
create
|
|
||||||
|
|
||||||
94
|
|
||||||
00:00:41,000 --> 00:00:41,774
|
|
||||||
content,
|
|
||||||
|
|
||||||
95
|
|
||||||
00:00:41,774 --> 00:00:41,924
|
|
||||||
or
|
|
||||||
|
|
||||||
96
|
|
||||||
00:00:41,924 --> 00:00:42,137
|
|
||||||
bring
|
|
||||||
|
|
||||||
97
|
|
||||||
00:00:42,137 --> 00:00:42,250
|
|
||||||
your
|
|
||||||
|
|
||||||
98
|
|
||||||
00:00:42,250 --> 00:00:42,575
|
|
||||||
words
|
|
||||||
|
|
||||||
99
|
|
||||||
00:00:42,575 --> 00:00:42,700
|
|
||||||
to
|
|
||||||
|
|
||||||
100
|
|
||||||
00:00:42,700 --> 00:00:43,512
|
|
||||||
life,
|
|
||||||
|
|
||||||
101
|
|
||||||
00:00:43,512 --> 00:00:44,075
|
|
||||||
Abogen
|
|
||||||
|
|
||||||
102
|
|
||||||
00:00:44,075 --> 00:00:44,350
|
|
||||||
makes
|
|
||||||
|
|
||||||
103
|
|
||||||
00:00:44,350 --> 00:00:44,525
|
|
||||||
it
|
|
||||||
|
|
||||||
104
|
|
||||||
00:00:44,525 --> 00:00:45,299
|
|
||||||
fast,
|
|
||||||
|
|
||||||
105
|
|
||||||
00:00:45,299 --> 00:00:45,825
|
|
||||||
fun,
|
|
||||||
|
|
||||||
106
|
|
||||||
00:00:45,825 --> 00:00:45,950
|
|
||||||
and
|
|
||||||
|
|
||||||
107
|
|
||||||
00:00:45,950 --> 00:00:47,275
|
|
||||||
effortless.
|
|
||||||
|
|
||||||
108
|
|
||||||
00:00:47,275 --> 00:00:47,962
|
|
||||||
Experience
|
|
||||||
|
|
||||||
109
|
|
||||||
00:00:47,962 --> 00:00:48,075
|
|
||||||
your
|
|
||||||
|
|
||||||
110
|
|
||||||
00:00:48,075 --> 00:00:48,812
|
|
||||||
stories,
|
|
||||||
|
|
||||||
111
|
|
||||||
00:00:48,812 --> 00:00:49,350
|
|
||||||
scripts,
|
|
||||||
|
|
||||||
112
|
|
||||||
00:00:49,350 --> 00:00:49,500
|
|
||||||
and
|
|
||||||
|
|
||||||
113
|
|
||||||
00:00:49,500 --> 00:00:50,075
|
|
||||||
ideas
|
|
||||||
|
|
||||||
114
|
|
||||||
00:00:50,075 --> 00:00:50,174
|
|
||||||
in
|
|
||||||
|
|
||||||
115
|
|
||||||
00:00:50,174 --> 00:00:50,299
|
|
||||||
a
|
|
||||||
|
|
||||||
116
|
|
||||||
00:00:50,299 --> 00:00:50,662
|
|
||||||
whole
|
|
||||||
|
|
||||||
117
|
|
||||||
00:00:50,662 --> 00:00:50,850
|
|
||||||
new
|
|
||||||
|
|
||||||
118
|
|
||||||
00:00:50,850 --> 00:00:51,150
|
|
||||||
way
|
|
||||||
|
|
||||||
119
|
|
||||||
00:00:51,150 --> 00:00:51,299
|
|
||||||
with
|
|
||||||
|
|
||||||
120
|
|
||||||
00:00:51,299 --> 00:00:52,424
|
|
||||||
Abogen.
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user