mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Add chapter metadata - added m4b format, generating chapters info and converting via ffmpeg
This commit is contained in:
+64
-1
@@ -10,7 +10,7 @@ import soundfile as sf
|
||||
from utils import clean_text
|
||||
from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS
|
||||
from voice_formulas import get_new_voice
|
||||
|
||||
import static_ffmpeg
|
||||
|
||||
def get_sample_voice_text(lang_code):
|
||||
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"])
|
||||
@@ -334,6 +334,12 @@ class ConversionThread(QThread):
|
||||
# Initialize current segment counter
|
||||
current_segment = 0
|
||||
|
||||
# Initialize chapter times
|
||||
chapters_time = [
|
||||
{"chapter": chapter[0], "start": 0.0, "end": 0.0}
|
||||
for chapter in chapters
|
||||
]
|
||||
|
||||
# Instead of processing the whole text, process by chapter
|
||||
for chapter_idx, (chapter_name, chapter_text) in enumerate(chapters, 1):
|
||||
if total_chapters > 1:
|
||||
@@ -349,6 +355,10 @@ class ConversionThread(QThread):
|
||||
chapter_subtitle_entries = []
|
||||
chapter_current_time = 0.0
|
||||
|
||||
# chapter start time
|
||||
chapter_time = chapters_time[chapter_idx - 1]
|
||||
chapter_time["start"] = current_time
|
||||
|
||||
# Set split_pattern to \n+ which will split on one or more newlines
|
||||
split_pattern = r"\n+"
|
||||
|
||||
@@ -462,6 +472,9 @@ class ConversionThread(QThread):
|
||||
# Update progress more frequently (after each result)
|
||||
self.progress_updated.emit(percent, etr_str)
|
||||
|
||||
# Update chapter end time
|
||||
chapter_time["end"] = current_time
|
||||
|
||||
# Save the individual chapter output if save_chapters_separately is enabled
|
||||
if (
|
||||
save_chapters_separately
|
||||
@@ -532,7 +545,17 @@ class ConversionThread(QThread):
|
||||
out_filename = f"{base_name}{suffix}.{self.output_format}"
|
||||
out_path = os.path.join(out_dir, out_filename)
|
||||
srt_path = os.path.splitext(out_path)[0] + ".srt"
|
||||
|
||||
# in case the user picked m4b format, we need to change the output format to wav
|
||||
if self.output_format == "m4b":
|
||||
picked_out_path = out_path
|
||||
out_path = os.path.splitext(out_path)[0] + ".wav"
|
||||
self.output_format = "wav"
|
||||
m4b_picked = True
|
||||
sf.write(out_path, audio, 24000, format=self.output_format)
|
||||
if m4b_picked:
|
||||
out_path = self._generate_m4b_with_chapters(out_path, chapters_time)
|
||||
|
||||
if self.subtitle_mode != "Disabled":
|
||||
with open(srt_path, "w", encoding="utf-8", errors="replace") as srt_file:
|
||||
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
||||
@@ -571,6 +594,46 @@ class ConversionThread(QThread):
|
||||
self.merge_chapters_at_end = options["merge_chapters_at_end"]
|
||||
self.waiting_for_user_input = False
|
||||
|
||||
def _generate_m4b_with_chapters(self, out_path, chapters_time):
|
||||
# generate chapters.txt in the same folder as the output file
|
||||
chapters_info_path = os.path.splitext(out_path)[0] + "_chapters.txt"
|
||||
with open(chapters_info_path, "w", encoding="utf-8") as f:
|
||||
f.write(';FFMETADATA1\n') # required header for ffmpeg
|
||||
for chapter in chapters_time:
|
||||
f.write(f"[CHAPTER]\n")
|
||||
f.write(f"TIMEBASE=1/10\n")
|
||||
# use 10th of second for start/end time
|
||||
f.write(f"START={int(chapter["start"]*10)}\n")
|
||||
f.write(f"END={int(chapter["end"]*10)}\n")
|
||||
f.write(f"title={chapter["chapter"]}\n\n")
|
||||
# call ffmpeg to merge the chapters into the output file
|
||||
# ffmpeg installed on first call to add_paths()
|
||||
static_ffmpeg.add_paths()
|
||||
import subprocess
|
||||
out_path_m4b = os.path.splitext(out_path)[0] + ".m4b"
|
||||
# ffmpeg -i input.m4b -i ch.txt -map 0:a -map_chapters 1 output.m4b
|
||||
ffmpeg_cmd = [
|
||||
"ffmpeg",
|
||||
"-i", out_path,
|
||||
"-i", chapters_info_path,
|
||||
"-map", "0:a",
|
||||
"-map_chapters", "1",
|
||||
out_path_m4b
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(ffmpeg_cmd, capture_output=True, text=True)
|
||||
# TODO Check for errors in the ffmpeg command
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.log_updated.emit(
|
||||
(f"FFmpeg error: {e.stderr}", "red")
|
||||
)
|
||||
return out_path
|
||||
# delete the chapters path and original (wav) file
|
||||
os.remove(chapters_info_path)
|
||||
os.remove(out_path)
|
||||
# the new file is in m4b format - use that for messages
|
||||
return out_path_m4b
|
||||
|
||||
def _srt_time(self, t):
|
||||
"""Helper function to format time for SRT files"""
|
||||
h = int(t // 3600)
|
||||
|
||||
+1
-1
@@ -652,7 +652,7 @@ class abogen(QWidget):
|
||||
self.format_combo.setStyleSheet(
|
||||
"QComboBox { min-height: 20px; padding: 6px 12px; }"
|
||||
)
|
||||
format_options = ["wav", "flac", "mp3"]
|
||||
format_options = ["wav", "flac", "mp3", "m4b"]
|
||||
self.format_combo.addItems(format_options)
|
||||
self.format_combo.setCurrentText(self.selected_format)
|
||||
self.format_combo.currentTextChanged.connect(self.on_format_changed)
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ dependencies = [
|
||||
"soundfile>=0.13.1",
|
||||
"pygame>=2.6.1",
|
||||
"charset_normalizer>=3.4.1",
|
||||
"chardet>=5.2.0"
|
||||
"chardet>=5.2.0",
|
||||
"static_ffmpeg>=2.13"
|
||||
]
|
||||
|
||||
classifiers = [
|
||||
|
||||
Reference in New Issue
Block a user