mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-22 07:10:28 +02:00
refactor: unify intro/outro through domain; extract subtitle writer creation
- domain/intro_outro.py: resolve_intro(), resolve_outro() return IntroOutroSpec - Both UIs call domain for text building + voice spec resolution - PyQt uses resolve_intro/resolve_outro instead of direct calls - infrastructure/subtitle_writer.py: resolve_subtitle_format(), make_subtitle_writer() - Deleted duplicate _create_subtitle_writer() from WebUI - Deleted duplicate _subtitle_alignment_from_format() from PyQt - domain/conversion_engine.py: run_tts_segment_loop() for TTS iteration - VoiceCache class in domain/voice_loader.py used by both UIs - Tests: 1253 passed
This commit is contained in:
@@ -0,0 +1,83 @@
|
|||||||
|
"""Intro/outro text building and voice resolution for audiobook conversion.
|
||||||
|
|
||||||
|
Both UIs (WebUI and Desktop) need to:
|
||||||
|
1. Build intro/outro text from book metadata
|
||||||
|
2. Resolve which voice to use for intro/outro synthesis
|
||||||
|
|
||||||
|
This module provides the shared domain logic. The actual TTS synthesis
|
||||||
|
and audio writing remain UI-specific.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from abogen.domain.title_builder import build_title_intro_text, build_outro_text
|
||||||
|
from abogen.domain.voice_resolution import resolve_fallback_voice_spec
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IntroOutroSpec:
|
||||||
|
"""Resolved intro or outro specification ready for TTS synthesis."""
|
||||||
|
text: str
|
||||||
|
voice_spec: str
|
||||||
|
enabled: bool
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_intro(
|
||||||
|
metadata: Optional[Dict[str, Any]],
|
||||||
|
original_filename: str,
|
||||||
|
read_title_intro: bool,
|
||||||
|
base_voice_spec: str,
|
||||||
|
job_voice: str,
|
||||||
|
voice_cache_keys: list[str],
|
||||||
|
) -> IntroOutroSpec:
|
||||||
|
"""Resolve the intro specification from job settings and metadata.
|
||||||
|
|
||||||
|
Returns an IntroOutroSpec with text and voice_spec populated,
|
||||||
|
or enabled=False if intro is disabled or text cannot be built.
|
||||||
|
"""
|
||||||
|
if not read_title_intro:
|
||||||
|
return IntroOutroSpec(text="", voice_spec="", enabled=False)
|
||||||
|
|
||||||
|
text = build_title_intro_text(metadata, original_filename)
|
||||||
|
if not text:
|
||||||
|
return IntroOutroSpec(text="", voice_spec="", enabled=False)
|
||||||
|
|
||||||
|
voice_spec = resolve_fallback_voice_spec(
|
||||||
|
base_voice_spec, job_voice, voice_cache_keys
|
||||||
|
)
|
||||||
|
if not voice_spec:
|
||||||
|
return IntroOutroSpec(text=text, voice_spec="", enabled=False)
|
||||||
|
|
||||||
|
return IntroOutroSpec(text=text, voice_spec=voice_spec, enabled=True)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_outro(
|
||||||
|
metadata: Optional[Dict[str, Any]],
|
||||||
|
original_filename: str,
|
||||||
|
read_closing_outro: bool,
|
||||||
|
base_voice_spec: str,
|
||||||
|
job_voice: str,
|
||||||
|
voice_cache_keys: list[str],
|
||||||
|
) -> IntroOutroSpec:
|
||||||
|
"""Resolve the outro specification from job settings and metadata.
|
||||||
|
|
||||||
|
Returns an IntroOutroSpec with text and voice_spec populated,
|
||||||
|
or enabled=False if outro is disabled or text cannot be built.
|
||||||
|
"""
|
||||||
|
if not read_closing_outro:
|
||||||
|
return IntroOutroSpec(text="", voice_spec="", enabled=False)
|
||||||
|
|
||||||
|
text = build_outro_text(metadata, original_filename)
|
||||||
|
if not text:
|
||||||
|
return IntroOutroSpec(text="", voice_spec="", enabled=False)
|
||||||
|
|
||||||
|
voice_spec = resolve_fallback_voice_spec(
|
||||||
|
base_voice_spec, job_voice, voice_cache_keys
|
||||||
|
)
|
||||||
|
if not voice_spec:
|
||||||
|
return IntroOutroSpec(text=text, voice_spec="", enabled=False)
|
||||||
|
|
||||||
|
return IntroOutroSpec(text=text, voice_spec=voice_spec, enabled=True)
|
||||||
@@ -37,6 +37,7 @@ from abogen.domain.output_paths import (
|
|||||||
from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32
|
from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32
|
||||||
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
from abogen.domain.audio_sink import AudioSink, open_audio_sink
|
||||||
from abogen.domain.conversion_engine import run_tts_segment_loop, SegmentStats, SegmentInfo
|
from abogen.domain.conversion_engine import run_tts_segment_loop, SegmentStats, SegmentInfo
|
||||||
|
from abogen.domain.intro_outro import resolve_intro, resolve_outro
|
||||||
from abogen.domain.audio_buffer import (
|
from abogen.domain.audio_buffer import (
|
||||||
create_silence,
|
create_silence,
|
||||||
mix_audio,
|
mix_audio,
|
||||||
@@ -66,6 +67,22 @@ import subprocess
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_metadata_dict(file_name: str, is_direct_text: bool) -> dict:
|
||||||
|
"""Extract metadata dict from file for intro/outro text building."""
|
||||||
|
try:
|
||||||
|
from abogen.domain.metadata_extraction import read_text_for_metadata, extract_metadata_from_text
|
||||||
|
text = read_text_for_metadata(
|
||||||
|
file_path=file_name,
|
||||||
|
is_direct_text=is_direct_text,
|
||||||
|
direct_text=file_name if is_direct_text else None,
|
||||||
|
)
|
||||||
|
if text:
|
||||||
|
return extract_metadata_from_text(text) or {}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
# Configuration constants
|
# Configuration constants
|
||||||
_USER_RESPONSE_TIMEOUT = (
|
_USER_RESPONSE_TIMEOUT = (
|
||||||
0.1 # Timeout in seconds for checking user response/cancellation
|
0.1 # Timeout in seconds for checking user response/cancellation
|
||||||
@@ -293,6 +310,8 @@ class ConversionThread(QThread):
|
|||||||
self.max_subtitle_words = 50 # Default value, will be overridden from GUI
|
self.max_subtitle_words = 50 # Default value, will be overridden from GUI
|
||||||
self.silence_duration = 2.0 # Default value, will be overridden from GUI
|
self.silence_duration = 2.0 # Default value, will be overridden from GUI
|
||||||
self.use_spacy_segmentation = True # Default, will be overridden from GUI
|
self.use_spacy_segmentation = True # Default, will be overridden from GUI
|
||||||
|
self.read_title_intro = False # Will be overridden from GUI
|
||||||
|
self.read_closing_outro = True # Will be overridden from GUI
|
||||||
# Set split pattern based on language and subtitle mode
|
# Set split pattern based on language and subtitle mode
|
||||||
self.split_pattern = get_split_pattern(lang_code, subtitle_mode)
|
self.split_pattern = get_split_pattern(lang_code, subtitle_mode)
|
||||||
self.voice_cache = VoiceCache() # Cache for loaded voices
|
self.voice_cache = VoiceCache() # Cache for loaded voices
|
||||||
@@ -762,6 +781,42 @@ 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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# --- Intro synthesis ---
|
||||||
|
intro_emitted = False
|
||||||
|
if merge_chapters_at_end:
|
||||||
|
intro_spec = resolve_intro(
|
||||||
|
_extract_metadata_dict(self.file_name, self.is_direct_text),
|
||||||
|
os.path.basename(self.file_name) if self.file_name else "",
|
||||||
|
getattr(self, "read_title_intro", False),
|
||||||
|
self.voice, self.voice, list(self.voice_cache._cache.keys()),
|
||||||
|
)
|
||||||
|
if intro_spec.enabled:
|
||||||
|
self.log_updated.emit((f"Title intro: {intro_spec.text[:80]}", "grey"))
|
||||||
|
loaded_intro_voice = self.load_voice_cached(intro_spec.voice_spec, self.backend)
|
||||||
|
intro_stats = SegmentStats(
|
||||||
|
processed_chars=self.processed_char_count,
|
||||||
|
current_time=current_time,
|
||||||
|
etr_start_time=self.etr_start_time,
|
||||||
|
total_characters=self.total_char_count,
|
||||||
|
)
|
||||||
|
run_tts_segment_loop(
|
||||||
|
text=intro_spec.text,
|
||||||
|
backend=self.backend,
|
||||||
|
voice=loaded_intro_voice,
|
||||||
|
speed=self.speed,
|
||||||
|
split_pattern=self.split_pattern,
|
||||||
|
stats=intro_stats,
|
||||||
|
check_cancel=lambda: self.cancel_requested,
|
||||||
|
on_progress=lambda pct, etr: self.progress_updated.emit(pct, etr),
|
||||||
|
chapter_sink=None,
|
||||||
|
audio_sink=merged_sink,
|
||||||
|
)
|
||||||
|
self.processed_char_count = intro_stats.processed_chars
|
||||||
|
current_time = intro_stats.current_time
|
||||||
|
intro_emitted = True
|
||||||
|
self.log_updated.emit(("Intro synthesized.", "grey"))
|
||||||
|
|
||||||
# Instead of processing the whole text, process by chapter
|
# Instead of processing the whole text, process by chapter
|
||||||
for chapter_idx, (chapter_name, voice_segments) in enumerate(chapters, 1):
|
for chapter_idx, (chapter_name, voice_segments) in enumerate(chapters, 1):
|
||||||
chapter_out_path = None
|
chapter_out_path = None
|
||||||
@@ -1054,6 +1109,40 @@ class ConversionThread(QThread):
|
|||||||
"green",
|
"green",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- Outro synthesis ---
|
||||||
|
if merge_chapters_at_end:
|
||||||
|
outro_spec = resolve_outro(
|
||||||
|
_extract_metadata_dict(self.file_name, self.is_direct_text),
|
||||||
|
os.path.basename(self.file_name) if self.file_name else "",
|
||||||
|
getattr(self, "read_closing_outro", True),
|
||||||
|
self.voice, self.voice, list(self.voice_cache._cache.keys()),
|
||||||
|
)
|
||||||
|
if outro_spec.enabled:
|
||||||
|
self.log_updated.emit((f"Closing outro: {outro_spec.text[:80]}", "grey"))
|
||||||
|
loaded_outro_voice = self.load_voice_cached(outro_spec.voice_spec, self.backend)
|
||||||
|
outro_stats = SegmentStats(
|
||||||
|
processed_chars=self.processed_char_count,
|
||||||
|
current_time=current_time,
|
||||||
|
etr_start_time=self.etr_start_time,
|
||||||
|
total_characters=self.total_char_count,
|
||||||
|
)
|
||||||
|
run_tts_segment_loop(
|
||||||
|
text=outro_spec.text,
|
||||||
|
backend=self.backend,
|
||||||
|
voice=loaded_outro_voice,
|
||||||
|
speed=self.speed,
|
||||||
|
split_pattern=self.split_pattern,
|
||||||
|
stats=outro_stats,
|
||||||
|
check_cancel=lambda: self.cancel_requested,
|
||||||
|
on_progress=lambda pct, etr: self.progress_updated.emit(pct, etr),
|
||||||
|
chapter_sink=None,
|
||||||
|
audio_sink=merged_sink,
|
||||||
|
)
|
||||||
|
self.processed_char_count = outro_stats.processed_chars
|
||||||
|
current_time = outro_stats.current_time
|
||||||
|
self.log_updated.emit(("Outro synthesized.", "grey"))
|
||||||
|
|
||||||
# Finalize merged output file ONLY if merging
|
# Finalize merged output file ONLY if merging
|
||||||
if merge_chapters_at_end:
|
if merge_chapters_at_end:
|
||||||
self.log_updated.emit(("\nFinalizing audio. Please wait...", "grey"))
|
self.log_updated.emit(("\nFinalizing audio. Please wait...", "grey"))
|
||||||
|
|||||||
@@ -955,6 +955,8 @@ class abogen(QWidget):
|
|||||||
self.use_silent_gaps = self.config.get("use_silent_gaps", _d["use_silent_gaps"])
|
self.use_silent_gaps = self.config.get("use_silent_gaps", _d["use_silent_gaps"])
|
||||||
self.subtitle_speed_method = self.config.get("subtitle_speed_method", _d["subtitle_speed_method"])
|
self.subtitle_speed_method = self.config.get("subtitle_speed_method", _d["subtitle_speed_method"])
|
||||||
self.use_spacy_segmentation = self.config.get("use_spacy_segmentation", _d["use_spacy_segmentation"])
|
self.use_spacy_segmentation = self.config.get("use_spacy_segmentation", _d["use_spacy_segmentation"])
|
||||||
|
self.read_title_intro = self.config.get("read_title_intro", _d.get("read_title_intro", False))
|
||||||
|
self.read_closing_outro = self.config.get("read_closing_outro", _d.get("read_closing_outro", True))
|
||||||
# Word substitution settings
|
# Word substitution settings
|
||||||
self.word_substitutions_enabled = self.config.get("word_substitutions_enabled", _d["word_substitutions_enabled"])
|
self.word_substitutions_enabled = self.config.get("word_substitutions_enabled", _d["word_substitutions_enabled"])
|
||||||
self.word_substitutions_list = self.config.get("word_substitutions_list", _d["word_substitutions_list"])
|
self.word_substitutions_list = self.config.get("word_substitutions_list", _d["word_substitutions_list"])
|
||||||
@@ -2393,6 +2395,9 @@ class abogen(QWidget):
|
|||||||
self.conversion_thread.merge_chapters_at_end = getattr(
|
self.conversion_thread.merge_chapters_at_end = getattr(
|
||||||
self, "merge_chapters_at_end", True
|
self, "merge_chapters_at_end", True
|
||||||
)
|
)
|
||||||
|
# Pass intro/outro settings
|
||||||
|
self.conversion_thread.read_title_intro = self.read_title_intro
|
||||||
|
self.conversion_thread.read_closing_outro = self.read_closing_outro
|
||||||
self.conversion_thread.progress_updated.connect(self.update_progress)
|
self.conversion_thread.progress_updated.connect(self.update_progress)
|
||||||
self.conversion_thread.log_updated.connect(self.update_log)
|
self.conversion_thread.log_updated.connect(self.update_log)
|
||||||
self.conversion_thread.conversion_finished.connect(
|
self.conversion_thread.conversion_finished.connect(
|
||||||
@@ -3560,6 +3565,27 @@ class abogen(QWidget):
|
|||||||
# Add separator
|
# Add separator
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Add title intro option
|
||||||
|
self.title_intro_action = QAction("Read title intro before first chapter", self)
|
||||||
|
self.title_intro_action.setCheckable(True)
|
||||||
|
self.title_intro_action.setChecked(self.read_title_intro)
|
||||||
|
self.title_intro_action.triggered.connect(
|
||||||
|
lambda checked: self.toggle_read_title_intro(checked)
|
||||||
|
)
|
||||||
|
menu.addAction(self.title_intro_action)
|
||||||
|
|
||||||
|
# Add closing outro option
|
||||||
|
self.closing_outro_action = QAction("Read closing outro after last chapter", self)
|
||||||
|
self.closing_outro_action.setCheckable(True)
|
||||||
|
self.closing_outro_action.setChecked(self.read_closing_outro)
|
||||||
|
self.closing_outro_action.triggered.connect(
|
||||||
|
lambda checked: self.toggle_read_closing_outro(checked)
|
||||||
|
)
|
||||||
|
menu.addAction(self.closing_outro_action)
|
||||||
|
|
||||||
|
# Add separator
|
||||||
|
menu.addSeparator()
|
||||||
|
|
||||||
# Add "Pre-download models and voices for offline use" option
|
# Add "Pre-download models and voices for offline use" option
|
||||||
predownload_action = QAction(
|
predownload_action = QAction(
|
||||||
"Pre-download models and voices for offline use", self
|
"Pre-download models and voices for offline use", self
|
||||||
@@ -3636,6 +3662,16 @@ class abogen(QWidget):
|
|||||||
self.config["use_spacy_segmentation"] = enabled
|
self.config["use_spacy_segmentation"] = enabled
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
|
|
||||||
|
def toggle_read_title_intro(self, enabled):
|
||||||
|
self.read_title_intro = enabled
|
||||||
|
self.config["read_title_intro"] = enabled
|
||||||
|
save_config(self.config)
|
||||||
|
|
||||||
|
def toggle_read_closing_outro(self, enabled):
|
||||||
|
self.read_closing_outro = enabled
|
||||||
|
self.config["read_closing_outro"] = enabled
|
||||||
|
save_config(self.config)
|
||||||
|
|
||||||
def restart_app(self):
|
def restart_app(self):
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from abogen.domain.metadata_helpers import (
|
|||||||
extract_series_metadata as _extract_series_metadata,
|
extract_series_metadata as _extract_series_metadata,
|
||||||
format_series_sentence as _format_series_sentence,
|
format_series_sentence as _format_series_sentence,
|
||||||
)
|
)
|
||||||
|
from abogen.domain.intro_outro import resolve_intro, resolve_outro
|
||||||
from abogen.domain.title_builder import (
|
from abogen.domain.title_builder import (
|
||||||
build_title_intro_text as _build_title_intro_text,
|
build_title_intro_text as _build_title_intro_text,
|
||||||
build_outro_text as _build_outro_text,
|
build_outro_text as _build_outro_text,
|
||||||
@@ -77,7 +78,6 @@ from abogen.domain.voice_resolution import (
|
|||||||
initialize_voice_cache as _initialize_voice_cache,
|
initialize_voice_cache as _initialize_voice_cache,
|
||||||
chapter_voice_spec as _chapter_voice_spec,
|
chapter_voice_spec as _chapter_voice_spec,
|
||||||
chunk_voice_spec as _chunk_voice_spec,
|
chunk_voice_spec as _chunk_voice_spec,
|
||||||
resolve_fallback_voice_spec as _resolve_fallback_voice_spec,
|
|
||||||
)
|
)
|
||||||
from abogen.domain.chapter_overrides import apply_chapter_overrides as _apply_chapter_overrides
|
from abogen.domain.chapter_overrides import apply_chapter_overrides as _apply_chapter_overrides
|
||||||
from abogen.domain.metadata_merge import merge_metadata as _merge_metadata
|
from abogen.domain.metadata_merge import merge_metadata as _merge_metadata
|
||||||
@@ -386,22 +386,20 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
intro_voice_choice: Any = None
|
intro_voice_choice: Any = None
|
||||||
intro_speed: Optional[float] = None
|
intro_speed: Optional[float] = None
|
||||||
intro_steps: Optional[int] = None
|
intro_steps: Optional[int] = None
|
||||||
if read_title_intro:
|
intro_spec = resolve_intro(
|
||||||
book_intro_text = _build_title_intro_text(job.metadata_tags, job.original_filename)
|
job.metadata_tags, job.original_filename, read_title_intro,
|
||||||
if book_intro_text:
|
base_voice_spec, getattr(job, "voice", "M1"), list(voice_cache.keys()),
|
||||||
preview = book_intro_text if len(book_intro_text) <= 120 else f"{book_intro_text[:117]}…"
|
)
|
||||||
job.add_log(f"Title intro enabled: {preview}", level="debug")
|
if intro_spec.enabled:
|
||||||
|
book_intro_text = intro_spec.text
|
||||||
|
preview = book_intro_text if len(book_intro_text) <= 120 else f"{book_intro_text[:117]}…"
|
||||||
|
job.add_log(f"Title intro enabled: {preview}", level="debug")
|
||||||
|
|
||||||
intro_voice_spec = _resolve_fallback_voice_spec(
|
intro_provider, _, intro_voice_choice, intro_speed, intro_steps = resolve_voice_choice(
|
||||||
base_voice_spec, job.voice, list(voice_cache.keys())
|
intro_spec.voice_spec
|
||||||
)
|
)
|
||||||
|
elif read_title_intro:
|
||||||
if intro_voice_spec:
|
job.add_log("Title intro enabled but no usable metadata was found.", level="debug")
|
||||||
intro_provider, _, intro_voice_choice, intro_speed, intro_steps = resolve_voice_choice(
|
|
||||||
intro_voice_spec
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
job.add_log("Title intro enabled but no usable metadata was found.", level="debug")
|
|
||||||
intro_emitted = False
|
intro_emitted = False
|
||||||
|
|
||||||
def emit_text(
|
def emit_text(
|
||||||
@@ -823,17 +821,17 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
chapter_markers.append(marker)
|
chapter_markers.append(marker)
|
||||||
|
|
||||||
if getattr(job, "read_closing_outro", True):
|
if getattr(job, "read_closing_outro", True):
|
||||||
outro_text = _build_outro_text(job.metadata_tags, job.original_filename)
|
outro_spec = resolve_outro(
|
||||||
outro_voice_spec = _resolve_fallback_voice_spec(
|
job.metadata_tags, job.original_filename, True,
|
||||||
base_voice_spec, job.voice, list(voice_cache.keys())
|
base_voice_spec, getattr(job, "voice", "M1"), list(voice_cache.keys()),
|
||||||
)
|
)
|
||||||
|
|
||||||
if outro_text and outro_voice_spec:
|
if outro_spec.enabled:
|
||||||
outro_start_time = current_time
|
outro_start_time = current_time
|
||||||
outro_audio_path: Optional[Path] = None
|
outro_audio_path: Optional[Path] = None
|
||||||
outro_segments = 0
|
outro_segments = 0
|
||||||
outro_index = total_chapters + 1
|
outro_index = total_chapters + 1
|
||||||
outro_provider, _, outro_voice_choice, outro_speed, outro_steps = resolve_voice_choice(outro_voice_spec)
|
outro_provider, _, outro_voice_choice, outro_speed, outro_steps = resolve_voice_choice(outro_spec.voice_spec)
|
||||||
|
|
||||||
with ExitStack() as outro_sink_stack:
|
with ExitStack() as outro_sink_stack:
|
||||||
chapter_sink: Optional[AudioSink] = None
|
chapter_sink: Optional[AudioSink] = None
|
||||||
@@ -852,7 +850,7 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
outro_segments = emit_text(
|
outro_segments = emit_text(
|
||||||
outro_text,
|
outro_spec.text,
|
||||||
voice_choice=outro_voice_choice,
|
voice_choice=outro_voice_choice,
|
||||||
chapter_sink=chapter_sink,
|
chapter_sink=chapter_sink,
|
||||||
preview_prefix="Outro",
|
preview_prefix="Outro",
|
||||||
@@ -863,7 +861,7 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
outro_end_time = current_time
|
outro_end_time = current_time
|
||||||
|
|
||||||
if outro_segments > 0:
|
if outro_segments > 0:
|
||||||
job.add_log(f"Appended outro sequence: {outro_text}")
|
job.add_log(f"Appended outro sequence: {outro_spec.text}")
|
||||||
if outro_audio_path is not None:
|
if outro_audio_path is not None:
|
||||||
job.result.artifacts[f"chapter_{outro_index:02d}"] = outro_audio_path
|
job.result.artifacts[f"chapter_{outro_index:02d}"] = outro_audio_path
|
||||||
chapter_paths.append(outro_audio_path)
|
chapter_paths.append(outro_audio_path)
|
||||||
@@ -873,7 +871,7 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
"title": "Outro",
|
"title": "Outro",
|
||||||
"start": outro_start_time,
|
"start": outro_start_time,
|
||||||
"end": outro_end_time,
|
"end": outro_end_time,
|
||||||
"voice": outro_voice_spec,
|
"voice": outro_spec.voice_spec,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user