mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Add chapter intro delay setting and implement in conversion process
This commit is contained in:
+145
-69
@@ -204,6 +204,18 @@ def _escape_ffmetadata_value(value: str) -> str:
|
|||||||
return escaped
|
return escaped
|
||||||
|
|
||||||
|
|
||||||
|
def _metadata_to_ffmpeg_args(metadata: Dict[str, Any]) -> List[str]:
|
||||||
|
args: List[str] = []
|
||||||
|
for key, value in (metadata or {}).items():
|
||||||
|
if value in (None, ""):
|
||||||
|
continue
|
||||||
|
key_str = str(key).strip()
|
||||||
|
if not key_str:
|
||||||
|
continue
|
||||||
|
args.extend(["-metadata", f"{key_str}={value}"])
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
def _render_ffmetadata(metadata: Dict[str, Any], chapters: List[Dict[str, Any]]) -> str:
|
def _render_ffmetadata(metadata: Dict[str, Any], chapters: List[Dict[str, Any]]) -> str:
|
||||||
lines: List[str] = [";FFMETADATA1"]
|
lines: List[str] = [";FFMETADATA1"]
|
||||||
for key, value in (metadata or {}).items():
|
for key, value in (metadata or {}).items():
|
||||||
@@ -274,10 +286,12 @@ def _embed_m4b_metadata(
|
|||||||
if candidate.exists():
|
if candidate.exists():
|
||||||
cover_path = candidate
|
cover_path = candidate
|
||||||
|
|
||||||
if not ffmetadata_path and not cover_path:
|
metadata_args = _metadata_to_ffmpeg_args(metadata_map)
|
||||||
|
|
||||||
|
if not ffmetadata_path and not cover_path and not metadata_args:
|
||||||
return
|
return
|
||||||
|
|
||||||
job.add_log("Embedding chapters and cover metadata into m4b output")
|
job.add_log("Embedding metadata into m4b output")
|
||||||
|
|
||||||
command: List[str] = ["ffmpeg", "-y", "-i", str(audio_path)]
|
command: List[str] = ["ffmpeg", "-y", "-i", str(audio_path)]
|
||||||
metadata_index: Optional[int] = None
|
metadata_index: Optional[int] = None
|
||||||
@@ -311,6 +325,9 @@ def _embed_m4b_metadata(
|
|||||||
else:
|
else:
|
||||||
command += ["-map_metadata", "0"]
|
command += ["-map_metadata", "0"]
|
||||||
|
|
||||||
|
if metadata_args:
|
||||||
|
command.extend(metadata_args)
|
||||||
|
|
||||||
command += ["-movflags", "+faststart+use_metadata_tags"]
|
command += ["-movflags", "+faststart+use_metadata_tags"]
|
||||||
|
|
||||||
temp_output = audio_path.with_suffix(audio_path.suffix + ".tmp")
|
temp_output = audio_path.with_suffix(audio_path.suffix + ".tmp")
|
||||||
@@ -332,6 +349,7 @@ def _embed_m4b_metadata(
|
|||||||
raise RuntimeError(f"ffmpeg failed to embed metadata (exit code {return_code})")
|
raise RuntimeError(f"ffmpeg failed to embed metadata (exit code {return_code})")
|
||||||
|
|
||||||
temp_output.replace(audio_path)
|
temp_output.replace(audio_path)
|
||||||
|
job.add_log("Embedded metadata and chapters into m4b output", level="info")
|
||||||
|
|
||||||
|
|
||||||
def run_conversion_job(job: Job) -> None:
|
def run_conversion_job(job: Job) -> None:
|
||||||
@@ -408,6 +426,83 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
total_chapters = len(extraction.chapters)
|
total_chapters = len(extraction.chapters)
|
||||||
job.add_log(f"Detected {total_chapters} chapter{'s' if total_chapters != 1 else ''}")
|
job.add_log(f"Detected {total_chapters} chapter{'s' if total_chapters != 1 else ''}")
|
||||||
|
|
||||||
|
def emit_text(
|
||||||
|
text: str,
|
||||||
|
*,
|
||||||
|
voice_choice: Any,
|
||||||
|
chapter_sink: Optional[AudioSink],
|
||||||
|
preview_prefix: Optional[str] = None,
|
||||||
|
split_pattern: Optional[str] = SPLIT_PATTERN,
|
||||||
|
) -> int:
|
||||||
|
nonlocal processed_chars, subtitle_index, current_time
|
||||||
|
normalized = _normalize_for_pipeline(text)
|
||||||
|
local_segments = 0
|
||||||
|
|
||||||
|
for segment in pipeline(
|
||||||
|
normalized,
|
||||||
|
voice=voice_choice,
|
||||||
|
speed=job.speed,
|
||||||
|
split_pattern=split_pattern,
|
||||||
|
):
|
||||||
|
canceller()
|
||||||
|
graphemes_raw = getattr(segment, "graphemes", "") or ""
|
||||||
|
graphemes = graphemes_raw.strip()
|
||||||
|
|
||||||
|
audio = _to_float32(getattr(segment, "audio", None))
|
||||||
|
if audio.size == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
local_segments += 1
|
||||||
|
if chapter_sink:
|
||||||
|
chapter_sink.write(audio)
|
||||||
|
if audio_sink:
|
||||||
|
audio_sink.write(audio)
|
||||||
|
|
||||||
|
duration = len(audio) / SAMPLE_RATE
|
||||||
|
processed_chars += len(graphemes)
|
||||||
|
job.processed_characters = processed_chars
|
||||||
|
if job.total_characters:
|
||||||
|
job.progress = min(processed_chars / job.total_characters, 0.999)
|
||||||
|
else:
|
||||||
|
job.progress = 0.0 if processed_chars == 0 else 0.999
|
||||||
|
|
||||||
|
preview_text = graphemes or (graphemes_raw[:80] if graphemes_raw else "[silence]")
|
||||||
|
prefix = f"{preview_prefix} · " if preview_prefix else ""
|
||||||
|
job.add_log(f"{prefix}{processed_chars:,}/{job.total_characters or '—'}: {preview_text[:80]}")
|
||||||
|
|
||||||
|
if subtitle_writer and audio_sink and graphemes:
|
||||||
|
subtitle_writer.write_segment(
|
||||||
|
index=subtitle_index,
|
||||||
|
text=graphemes,
|
||||||
|
start=current_time,
|
||||||
|
end=current_time + duration,
|
||||||
|
)
|
||||||
|
subtitle_index += 1
|
||||||
|
|
||||||
|
if audio_sink:
|
||||||
|
current_time += duration
|
||||||
|
|
||||||
|
return local_segments
|
||||||
|
|
||||||
|
def append_silence(
|
||||||
|
duration_seconds: float,
|
||||||
|
*,
|
||||||
|
include_in_chapter: bool,
|
||||||
|
chapter_sink: Optional[AudioSink],
|
||||||
|
) -> None:
|
||||||
|
nonlocal current_time
|
||||||
|
if duration_seconds <= 0:
|
||||||
|
return
|
||||||
|
samples = int(round(duration_seconds * SAMPLE_RATE))
|
||||||
|
if samples <= 0:
|
||||||
|
return
|
||||||
|
silence = np.zeros(samples, dtype="float32")
|
||||||
|
if include_in_chapter and chapter_sink:
|
||||||
|
chapter_sink.write(silence)
|
||||||
|
if audio_sink:
|
||||||
|
audio_sink.write(silence)
|
||||||
|
current_time += duration_seconds
|
||||||
|
|
||||||
for idx, chapter in enumerate(extraction.chapters, start=1):
|
for idx, chapter in enumerate(extraction.chapters, start=1):
|
||||||
canceller()
|
canceller()
|
||||||
job.add_log(f"Processing chapter {idx}/{total_chapters}: {chapter.title}")
|
job.add_log(f"Processing chapter {idx}/{total_chapters}: {chapter.title}")
|
||||||
@@ -425,71 +520,54 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
voice_choice = _resolve_voice(pipeline, chapter_voice_spec, job.use_gpu)
|
voice_choice = _resolve_voice(pipeline, chapter_voice_spec, job.use_gpu)
|
||||||
voice_cache[chapter_voice_spec] = voice_choice
|
voice_cache[chapter_voice_spec] = voice_choice
|
||||||
|
|
||||||
chapter_sink_stack = ExitStack()
|
|
||||||
chapter_sink: Optional[AudioSink] = None
|
|
||||||
chapter_audio_path: Optional[Path] = None
|
chapter_audio_path: Optional[Path] = None
|
||||||
|
|
||||||
if chapter_dir is not None:
|
|
||||||
chapter_audio_path = _build_output_path(
|
|
||||||
chapter_dir,
|
|
||||||
f"{Path(job.original_filename).stem}_{_slugify(chapter.title, idx)}",
|
|
||||||
job.separate_chapters_format,
|
|
||||||
)
|
|
||||||
chapter_sink = _open_audio_sink(
|
|
||||||
chapter_audio_path,
|
|
||||||
job,
|
|
||||||
chapter_sink_stack,
|
|
||||||
fmt=job.separate_chapters_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
segments_emitted = 0
|
segments_emitted = 0
|
||||||
tts_input = _normalize_for_pipeline(chapter.text)
|
|
||||||
|
|
||||||
for segment in pipeline(
|
with ExitStack() as chapter_sink_stack:
|
||||||
tts_input,
|
chapter_sink: Optional[AudioSink] = None
|
||||||
voice=voice_choice,
|
|
||||||
speed=job.speed,
|
|
||||||
split_pattern=SPLIT_PATTERN,
|
|
||||||
):
|
|
||||||
canceller()
|
|
||||||
graphemes_raw = getattr(segment, "graphemes", "") or ""
|
|
||||||
graphemes = graphemes_raw.strip()
|
|
||||||
|
|
||||||
audio = _to_float32(getattr(segment, "audio", None))
|
if chapter_dir is not None:
|
||||||
if audio.size == 0:
|
chapter_audio_path = _build_output_path(
|
||||||
continue
|
chapter_dir,
|
||||||
|
f"{Path(job.original_filename).stem}_{_slugify(chapter.title, idx)}",
|
||||||
segments_emitted += 1
|
job.separate_chapters_format,
|
||||||
if chapter_sink:
|
)
|
||||||
chapter_sink.write(audio)
|
chapter_sink = _open_audio_sink(
|
||||||
if audio_sink:
|
chapter_audio_path,
|
||||||
audio_sink.write(audio)
|
job,
|
||||||
|
chapter_sink_stack,
|
||||||
duration = len(audio) / SAMPLE_RATE
|
fmt=job.separate_chapters_format,
|
||||||
processed_chars += len(graphemes)
|
|
||||||
job.processed_characters = processed_chars
|
|
||||||
if job.total_characters:
|
|
||||||
job.progress = min(processed_chars / job.total_characters, 0.999)
|
|
||||||
else:
|
|
||||||
job.progress = 0.0 if processed_chars == 0 else 0.999
|
|
||||||
|
|
||||||
preview_text = graphemes or (graphemes_raw[:80] if graphemes_raw else "[silence]")
|
|
||||||
job.add_log(f"{processed_chars:,}/{job.total_characters or '—'}: {preview_text[:80]}")
|
|
||||||
|
|
||||||
if subtitle_writer and audio_sink and graphemes:
|
|
||||||
subtitle_writer.write_segment(
|
|
||||||
index=subtitle_index,
|
|
||||||
text=graphemes,
|
|
||||||
start=current_time,
|
|
||||||
end=current_time + duration,
|
|
||||||
)
|
)
|
||||||
subtitle_index += 1
|
|
||||||
|
|
||||||
if audio_sink:
|
speak_heading = bool(chapter.title.strip())
|
||||||
current_time += duration
|
if speak_heading:
|
||||||
|
stripped_title = chapter.title.strip()
|
||||||
|
if stripped_title:
|
||||||
|
first_line = next((line.strip() for line in chapter.text.splitlines() if line.strip()), "")
|
||||||
|
if first_line and first_line.casefold() == stripped_title.casefold():
|
||||||
|
speak_heading = False
|
||||||
|
|
||||||
if chapter_sink:
|
if speak_heading:
|
||||||
chapter_sink_stack.close()
|
heading_segments = emit_text(
|
||||||
|
chapter.title,
|
||||||
|
voice_choice=voice_choice,
|
||||||
|
chapter_sink=chapter_sink,
|
||||||
|
preview_prefix=f"Chapter {idx} title",
|
||||||
|
split_pattern=SPLIT_PATTERN,
|
||||||
|
)
|
||||||
|
segments_emitted += heading_segments
|
||||||
|
if heading_segments > 0 and job.chapter_intro_delay > 0:
|
||||||
|
append_silence(
|
||||||
|
job.chapter_intro_delay,
|
||||||
|
include_in_chapter=True,
|
||||||
|
chapter_sink=chapter_sink,
|
||||||
|
)
|
||||||
|
|
||||||
|
segments_emitted += emit_text(
|
||||||
|
chapter.text,
|
||||||
|
voice_choice=voice_choice,
|
||||||
|
chapter_sink=chapter_sink,
|
||||||
|
)
|
||||||
|
|
||||||
chapter_end_time = current_time
|
chapter_end_time = current_time
|
||||||
|
|
||||||
@@ -511,12 +589,12 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
and idx < total_chapters
|
and idx < total_chapters
|
||||||
and job.silence_between_chapters > 0
|
and job.silence_between_chapters > 0
|
||||||
):
|
):
|
||||||
silence_samples = int(job.silence_between_chapters * SAMPLE_RATE)
|
append_silence(
|
||||||
if silence_samples > 0:
|
job.silence_between_chapters,
|
||||||
silence = np.zeros(silence_samples, dtype="float32")
|
include_in_chapter=False,
|
||||||
audio_sink.write(silence)
|
chapter_sink=None,
|
||||||
current_time += job.silence_between_chapters
|
)
|
||||||
chapter_end_time = current_time
|
chapter_end_time = current_time
|
||||||
|
|
||||||
chapter_markers.append(
|
chapter_markers.append(
|
||||||
{
|
{
|
||||||
@@ -730,9 +808,7 @@ def _build_ffmpeg_command(path: Path, fmt: str, metadata: Optional[Dict[str, str
|
|||||||
base += ["-c:a", "copy"]
|
base += ["-c:a", "copy"]
|
||||||
|
|
||||||
if metadata:
|
if metadata:
|
||||||
for key, value in metadata.items():
|
base.extend(_metadata_to_ffmpeg_args(metadata))
|
||||||
if value:
|
|
||||||
base += ["-metadata", f"{key}={value}"]
|
|
||||||
base.append(str(path))
|
base.append(str(path))
|
||||||
return base
|
return base
|
||||||
|
|
||||||
|
|||||||
+21
-1
@@ -135,7 +135,7 @@ BOOLEAN_SETTINGS = {
|
|||||||
"save_as_project",
|
"save_as_project",
|
||||||
}
|
}
|
||||||
|
|
||||||
FLOAT_SETTINGS = {"silence_between_chapters"}
|
FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay"}
|
||||||
INT_SETTINGS = {"max_subtitle_words"}
|
INT_SETTINGS = {"max_subtitle_words"}
|
||||||
|
|
||||||
|
|
||||||
@@ -156,6 +156,7 @@ def _settings_defaults() -> Dict[str, Any]:
|
|||||||
"save_as_project": False,
|
"save_as_project": False,
|
||||||
"separate_chapters_format": "wav",
|
"separate_chapters_format": "wav",
|
||||||
"silence_between_chapters": 2.0,
|
"silence_between_chapters": 2.0,
|
||||||
|
"chapter_intro_delay": 0.5,
|
||||||
"max_subtitle_words": 50,
|
"max_subtitle_words": 50,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +421,9 @@ def settings_page() -> Response | str:
|
|||||||
updated["silence_between_chapters"] = _coerce_float(
|
updated["silence_between_chapters"] = _coerce_float(
|
||||||
form.get("silence_between_chapters"), defaults["silence_between_chapters"]
|
form.get("silence_between_chapters"), defaults["silence_between_chapters"]
|
||||||
)
|
)
|
||||||
|
updated["chapter_intro_delay"] = _coerce_float(
|
||||||
|
form.get("chapter_intro_delay"), defaults["chapter_intro_delay"]
|
||||||
|
)
|
||||||
updated["max_subtitle_words"] = _coerce_int(
|
updated["max_subtitle_words"] = _coerce_int(
|
||||||
form.get("max_subtitle_words"), defaults["max_subtitle_words"]
|
form.get("max_subtitle_words"), defaults["max_subtitle_words"]
|
||||||
)
|
)
|
||||||
@@ -801,6 +805,7 @@ def enqueue_job() -> Response:
|
|||||||
save_as_project = settings["save_as_project"]
|
save_as_project = settings["save_as_project"]
|
||||||
separate_chapters_format = settings["separate_chapters_format"]
|
separate_chapters_format = settings["separate_chapters_format"]
|
||||||
silence_between_chapters = settings["silence_between_chapters"]
|
silence_between_chapters = settings["silence_between_chapters"]
|
||||||
|
chapter_intro_delay = settings["chapter_intro_delay"]
|
||||||
max_subtitle_words = settings["max_subtitle_words"]
|
max_subtitle_words = settings["max_subtitle_words"]
|
||||||
|
|
||||||
pending = PendingJob(
|
pending = PendingJob(
|
||||||
@@ -830,6 +835,7 @@ def enqueue_job() -> Response:
|
|||||||
created_at=time.time(),
|
created_at=time.time(),
|
||||||
cover_image_path=cover_path,
|
cover_image_path=cover_path,
|
||||||
cover_image_mime=cover_mime,
|
cover_image_mime=cover_mime,
|
||||||
|
chapter_intro_delay=chapter_intro_delay,
|
||||||
)
|
)
|
||||||
|
|
||||||
service.store_pending_job(pending)
|
service.store_pending_job(pending)
|
||||||
@@ -854,6 +860,19 @@ def finalize_job(pending_id: str) -> Response:
|
|||||||
pending = cast(PendingJob, pending)
|
pending = cast(PendingJob, pending)
|
||||||
|
|
||||||
profiles = serialize_profiles()
|
profiles = serialize_profiles()
|
||||||
|
delay_value = pending.chapter_intro_delay
|
||||||
|
raw_delay = request.form.get("chapter_intro_delay")
|
||||||
|
if raw_delay is not None:
|
||||||
|
raw_normalized = raw_delay.strip()
|
||||||
|
if raw_normalized:
|
||||||
|
try:
|
||||||
|
delay_value = max(0.0, float(raw_normalized))
|
||||||
|
except ValueError:
|
||||||
|
return _render_prepare_page(pending, error="Enter a valid number for the chapter intro delay.")
|
||||||
|
else:
|
||||||
|
delay_value = 0.0
|
||||||
|
pending.chapter_intro_delay = delay_value
|
||||||
|
|
||||||
overrides: List[Dict[str, Any]] = []
|
overrides: List[Dict[str, Any]] = []
|
||||||
selected_total = 0
|
selected_total = 0
|
||||||
errors: List[str] = []
|
errors: List[str] = []
|
||||||
@@ -941,6 +960,7 @@ def finalize_job(pending_id: str) -> Response:
|
|||||||
max_subtitle_words=pending.max_subtitle_words,
|
max_subtitle_words=pending.max_subtitle_words,
|
||||||
cover_image_path=pending.cover_image_path,
|
cover_image_path=pending.cover_image_path,
|
||||||
cover_image_mime=pending.cover_image_mime,
|
cover_image_mime=pending.cover_image_mime,
|
||||||
|
chapter_intro_delay=pending.chapter_intro_delay,
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect(url_for("web.job_detail", job_id=job.id))
|
return redirect(url_for("web.job_detail", job_id=job.id))
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def _create_set_event() -> threading.Event:
|
|||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
STATE_VERSION = 2
|
STATE_VERSION = 3
|
||||||
|
|
||||||
|
|
||||||
class JobStatus(str, Enum):
|
class JobStatus(str, Enum):
|
||||||
@@ -69,6 +69,7 @@ class Job:
|
|||||||
voice_profile: Optional[str] = None
|
voice_profile: Optional[str] = None
|
||||||
metadata_tags: Dict[str, str] = field(default_factory=dict)
|
metadata_tags: Dict[str, str] = field(default_factory=dict)
|
||||||
max_subtitle_words: int = 50
|
max_subtitle_words: int = 50
|
||||||
|
chapter_intro_delay: float = 0.5
|
||||||
status: JobStatus = JobStatus.PENDING
|
status: JobStatus = JobStatus.PENDING
|
||||||
started_at: Optional[float] = None
|
started_at: Optional[float] = None
|
||||||
finished_at: Optional[float] = None
|
finished_at: Optional[float] = None
|
||||||
@@ -119,6 +120,7 @@ class Job:
|
|||||||
"save_as_project": self.save_as_project,
|
"save_as_project": self.save_as_project,
|
||||||
"voice_profile": self.voice_profile,
|
"voice_profile": self.voice_profile,
|
||||||
"max_subtitle_words": self.max_subtitle_words,
|
"max_subtitle_words": self.max_subtitle_words,
|
||||||
|
"chapter_intro_delay": self.chapter_intro_delay,
|
||||||
},
|
},
|
||||||
"metadata_tags": dict(self.metadata_tags),
|
"metadata_tags": dict(self.metadata_tags),
|
||||||
"chapters": [
|
"chapters": [
|
||||||
@@ -167,6 +169,7 @@ class PendingJob:
|
|||||||
created_at: float
|
created_at: float
|
||||||
cover_image_path: Optional[Path] = None
|
cover_image_path: Optional[Path] = None
|
||||||
cover_image_mime: Optional[str] = None
|
cover_image_mime: Optional[str] = None
|
||||||
|
chapter_intro_delay: float = 0.5
|
||||||
|
|
||||||
|
|
||||||
class ConversionService:
|
class ConversionService:
|
||||||
@@ -230,6 +233,7 @@ class ConversionService:
|
|||||||
metadata_tags: Optional[Mapping[str, Any]] = None,
|
metadata_tags: Optional[Mapping[str, Any]] = None,
|
||||||
cover_image_path: Optional[Path] = None,
|
cover_image_path: Optional[Path] = None,
|
||||||
cover_image_mime: Optional[str] = None,
|
cover_image_mime: Optional[str] = None,
|
||||||
|
chapter_intro_delay: float = 0.5,
|
||||||
) -> Job:
|
) -> Job:
|
||||||
job_id = uuid.uuid4().hex
|
job_id = uuid.uuid4().hex
|
||||||
normalized_metadata = self._normalize_metadata_tags(metadata_tags)
|
normalized_metadata = self._normalize_metadata_tags(metadata_tags)
|
||||||
@@ -263,6 +267,7 @@ class ConversionService:
|
|||||||
chapters=normalized_chapters,
|
chapters=normalized_chapters,
|
||||||
cover_image_path=cover_image_path,
|
cover_image_path=cover_image_path,
|
||||||
cover_image_mime=cover_image_mime,
|
cover_image_mime=cover_image_mime,
|
||||||
|
chapter_intro_delay=chapter_intro_delay,
|
||||||
)
|
)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._jobs[job_id] = job
|
self._jobs[job_id] = job
|
||||||
@@ -528,6 +533,7 @@ class ConversionService:
|
|||||||
"resume_token": job.resume_token,
|
"resume_token": job.resume_token,
|
||||||
"cover_image_path": str(job.cover_image_path) if job.cover_image_path else None,
|
"cover_image_path": str(job.cover_image_path) if job.cover_image_path else None,
|
||||||
"cover_image_mime": job.cover_image_mime,
|
"cover_image_mime": job.cover_image_mime,
|
||||||
|
"chapter_intro_delay": job.chapter_intro_delay,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _persist_state(self) -> None:
|
def _persist_state(self) -> None:
|
||||||
@@ -573,6 +579,7 @@ class ConversionService:
|
|||||||
voice_profile=payload.get("voice_profile"),
|
voice_profile=payload.get("voice_profile"),
|
||||||
metadata_tags=payload.get("metadata_tags", {}),
|
metadata_tags=payload.get("metadata_tags", {}),
|
||||||
max_subtitle_words=int(payload.get("max_subtitle_words", 50)),
|
max_subtitle_words=int(payload.get("max_subtitle_words", 50)),
|
||||||
|
chapter_intro_delay=float(payload.get("chapter_intro_delay", 0.5)),
|
||||||
)
|
)
|
||||||
job.status = JobStatus(payload.get("status", job.status.value))
|
job.status = JobStatus(payload.get("status", job.status.value))
|
||||||
job.started_at = payload.get("started_at")
|
job.started_at = payload.get("started_at")
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<li><strong>Merge at end:</strong> {{ 'Yes' if job.merge_chapters_at_end else 'No' }}</li>
|
<li><strong>Merge at end:</strong> {{ 'Yes' if job.merge_chapters_at_end else 'No' }}</li>
|
||||||
<li><strong>Separate chapter format:</strong> {{ job.separate_chapters_format|upper }}</li>
|
<li><strong>Separate chapter format:</strong> {{ job.separate_chapters_format|upper }}</li>
|
||||||
<li><strong>Silence between chapters:</strong> {{ '%.1f'|format(job.silence_between_chapters) }}s</li>
|
<li><strong>Silence between chapters:</strong> {{ '%.1f'|format(job.silence_between_chapters) }}s</li>
|
||||||
|
<li><strong>Chapter intro delay:</strong> {{ '%.1f'|format(job.chapter_intro_delay) }}s</li>
|
||||||
<li><strong>Max words per subtitle:</strong> {{ job.max_subtitle_words }}</li>
|
<li><strong>Max words per subtitle:</strong> {{ job.max_subtitle_words }}</li>
|
||||||
<li><strong>Project folder:</strong> {{ 'Yes' if job.save_as_project else 'No' }}</li>
|
<li><strong>Project folder:</strong> {{ 'Yes' if job.save_as_project else 'No' }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
<dt>Characters</dt>
|
<dt>Characters</dt>
|
||||||
<dd>{{ pending.total_characters|default(0) }}</dd>
|
<dd>{{ pending.total_characters|default(0) }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Chapter intro delay</dt>
|
||||||
|
<dd>{{ '%.1f'|format(pending.chapter_intro_delay) }} seconds</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
{% if pending.metadata_tags %}
|
{% if pending.metadata_tags %}
|
||||||
<div class="prepare-metadata">
|
<div class="prepare-metadata">
|
||||||
@@ -105,6 +109,13 @@
|
|||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="prepare-options">
|
||||||
|
<div class="field">
|
||||||
|
<label for="chapter_intro_delay">Pause after chapter titles (seconds)</label>
|
||||||
|
<input type="number" step="0.1" min="0" id="chapter_intro_delay" name="chapter_intro_delay" value="{{ '%.2f'|format(pending.chapter_intro_delay) }}">
|
||||||
|
<p class="hint">Set to 0 to disable the pause after speaking each chapter title.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="prepare-actions">
|
<div class="prepare-actions">
|
||||||
<button type="submit" class="button">Queue conversion</button>
|
<button type="submit" class="button">Queue conversion</button>
|
||||||
<button type="submit" class="button button--ghost" form="cancel-form">Cancel</button>
|
<button type="submit" class="button button--ghost" form="cancel-form">Cancel</button>
|
||||||
|
|||||||
@@ -86,6 +86,11 @@
|
|||||||
<label for="silence_between_chapters">Silence Between Chapters (Seconds)</label>
|
<label for="silence_between_chapters">Silence Between Chapters (Seconds)</label>
|
||||||
<input type="number" step="0.5" min="0" id="silence_between_chapters" name="silence_between_chapters" value="{{ settings.silence_between_chapters }}">
|
<input type="number" step="0.5" min="0" id="silence_between_chapters" name="silence_between_chapters" value="{{ settings.silence_between_chapters }}">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="chapter_intro_delay">Pause After Chapter Titles (Seconds)</label>
|
||||||
|
<input type="number" step="0.1" min="0" id="chapter_intro_delay" name="chapter_intro_delay" value="{{ '%.2f'|format(settings.chapter_intro_delay) }}">
|
||||||
|
<p class="hint">Inserted between the spoken chapter title and the chapter content. Set to 0 to disable.</p>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="max_subtitle_words">Max Words Per Subtitle Entry</label>
|
<label for="max_subtitle_words">Max Words Per Subtitle Entry</label>
|
||||||
<input type="number" min="1" max="200" id="max_subtitle_words" name="max_subtitle_words" value="{{ settings.max_subtitle_words }}">
|
<input type="number" min="1" max="200" id="max_subtitle_words" name="max_subtitle_words" value="{{ settings.max_subtitle_words }}">
|
||||||
|
|||||||
Reference in New Issue
Block a user