mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Add support for reading book title and authors before narration
This commit is contained in:
@@ -136,6 +136,93 @@ def _strip_duplicate_heading_line(text: str, heading: str) -> tuple[str, bool]:
|
|||||||
new_lines.pop(0)
|
new_lines.pop(0)
|
||||||
return "\n".join(new_lines), True
|
return "\n".join(new_lines), True
|
||||||
|
|
||||||
|
def _normalize_metadata_map(values: Optional[Mapping[str, Any]]) -> Dict[str, str]:
|
||||||
|
normalized: Dict[str, str] = {}
|
||||||
|
if not values:
|
||||||
|
return normalized
|
||||||
|
for key, value in values.items():
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
text = str(value).strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
normalized[str(key).casefold()] = text
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _format_author_sentence(raw: Optional[str]) -> str:
|
||||||
|
if raw is None:
|
||||||
|
return ""
|
||||||
|
normalized = str(raw).strip()
|
||||||
|
if not normalized:
|
||||||
|
return ""
|
||||||
|
lowered = normalized.casefold()
|
||||||
|
if lowered in {"unknown", "various"}:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
working = normalized.replace("&", " and ")
|
||||||
|
segments = [segment.strip() for segment in working.split(",") if segment.strip()]
|
||||||
|
tokens: List[str] = []
|
||||||
|
|
||||||
|
if segments:
|
||||||
|
for segment in segments:
|
||||||
|
parts = [part.strip() for part in re.split(r"\band\b", segment, flags=re.IGNORECASE) if part.strip()]
|
||||||
|
if parts:
|
||||||
|
tokens.extend(parts)
|
||||||
|
else:
|
||||||
|
tokens.append(segment)
|
||||||
|
else:
|
||||||
|
parts = [part.strip() for part in re.split(r"\band\b", working, flags=re.IGNORECASE) if part.strip()]
|
||||||
|
tokens.extend(parts or [normalized])
|
||||||
|
|
||||||
|
cleaned = [token for token in tokens if token and token.casefold() not in {"unknown", "various"}]
|
||||||
|
if not cleaned:
|
||||||
|
return ""
|
||||||
|
if len(cleaned) == 1:
|
||||||
|
return f"By {cleaned[0]}"
|
||||||
|
if len(cleaned) == 2:
|
||||||
|
return f"By {cleaned[0]} and {cleaned[1]}"
|
||||||
|
return f"By {', '.join(cleaned[:-1])}, and {cleaned[-1]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_sentence(text: str) -> str:
|
||||||
|
cleaned = text.strip()
|
||||||
|
if not cleaned:
|
||||||
|
return ""
|
||||||
|
if cleaned[-1] in ".!?":
|
||||||
|
return cleaned
|
||||||
|
return f"{cleaned}."
|
||||||
|
|
||||||
|
|
||||||
|
def _build_title_intro_text(
|
||||||
|
metadata: Optional[Mapping[str, Any]],
|
||||||
|
fallback_basename: str,
|
||||||
|
) -> str:
|
||||||
|
normalized = _normalize_metadata_map(metadata)
|
||||||
|
fallback_title = Path(fallback_basename).stem if fallback_basename else ""
|
||||||
|
title = normalized.get("title") or normalized.get("book_title") or normalized.get("album") or fallback_title
|
||||||
|
if not title:
|
||||||
|
title = fallback_title
|
||||||
|
subtitle = normalized.get("subtitle") or normalized.get("sub_title")
|
||||||
|
if subtitle and title and subtitle.casefold() == title.casefold():
|
||||||
|
subtitle = ""
|
||||||
|
author_value = ""
|
||||||
|
for candidate in ("artist", "album_artist", "author", "authors", "writer", "composer"):
|
||||||
|
value = normalized.get(candidate)
|
||||||
|
if value:
|
||||||
|
author_value = value
|
||||||
|
break
|
||||||
|
|
||||||
|
sentences: List[str] = []
|
||||||
|
if title:
|
||||||
|
sentences.append(_ensure_sentence(title))
|
||||||
|
if subtitle:
|
||||||
|
sentences.append(_ensure_sentence(subtitle))
|
||||||
|
author_sentence = _format_author_sentence(author_value)
|
||||||
|
if author_sentence:
|
||||||
|
sentences.append(_ensure_sentence(author_sentence))
|
||||||
|
return " ".join(sentences).strip()
|
||||||
|
|
||||||
|
|
||||||
def _spec_to_voice_ids(spec: Any) -> Set[str]:
|
def _spec_to_voice_ids(spec: Any) -> Set[str]:
|
||||||
text = str(spec or "").strip()
|
text = str(spec or "").strip()
|
||||||
@@ -1050,6 +1137,16 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
}
|
}
|
||||||
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 ''}")
|
||||||
auto_prefix_titles = getattr(job, "auto_prefix_chapter_titles", True)
|
auto_prefix_titles = getattr(job, "auto_prefix_chapter_titles", True)
|
||||||
|
read_title_intro = getattr(job, "read_title_intro", False)
|
||||||
|
book_intro_text = ""
|
||||||
|
if read_title_intro:
|
||||||
|
book_intro_text = _build_title_intro_text(job.metadata_tags, job.original_filename)
|
||||||
|
if book_intro_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")
|
||||||
|
else:
|
||||||
|
job.add_log("Title intro enabled but no usable metadata was found.", level="debug")
|
||||||
|
intro_emitted = False
|
||||||
|
|
||||||
def emit_text(
|
def emit_text(
|
||||||
text: str,
|
text: str,
|
||||||
@@ -1192,6 +1289,21 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
if _headings_equivalent(first_line, heading_text) or (raw_title and _headings_equivalent(first_line, raw_title)):
|
if _headings_equivalent(first_line, heading_text) or (raw_title and _headings_equivalent(first_line, raw_title)):
|
||||||
remove_heading_from_body = True
|
remove_heading_from_body = True
|
||||||
|
|
||||||
|
if not intro_emitted and book_intro_text:
|
||||||
|
intro_segments = emit_text(
|
||||||
|
book_intro_text,
|
||||||
|
voice_choice=voice_choice,
|
||||||
|
chapter_sink=chapter_sink,
|
||||||
|
preview_prefix="Book intro",
|
||||||
|
)
|
||||||
|
intro_emitted = True
|
||||||
|
if intro_segments > 0 and job.chapter_intro_delay > 0:
|
||||||
|
append_silence(
|
||||||
|
job.chapter_intro_delay,
|
||||||
|
include_in_chapter=True,
|
||||||
|
chapter_sink=chapter_sink,
|
||||||
|
)
|
||||||
|
|
||||||
if speak_heading:
|
if speak_heading:
|
||||||
heading_segments = emit_text(
|
heading_segments = emit_text(
|
||||||
heading_text,
|
heading_text,
|
||||||
|
|||||||
@@ -1528,6 +1528,21 @@ def _apply_prepare_form(
|
|||||||
else:
|
else:
|
||||||
pending.chapter_intro_delay = 0.0
|
pending.chapter_intro_delay = 0.0
|
||||||
|
|
||||||
|
intro_values: List[str] = []
|
||||||
|
getter = getattr(form, "getlist", None)
|
||||||
|
if callable(getter):
|
||||||
|
raw_intro_values = getter("read_title_intro")
|
||||||
|
if raw_intro_values:
|
||||||
|
intro_values = list(cast(Iterable[str], raw_intro_values))
|
||||||
|
else:
|
||||||
|
raw_intro = form.get("read_title_intro")
|
||||||
|
if raw_intro is not None:
|
||||||
|
intro_values = [raw_intro]
|
||||||
|
if intro_values:
|
||||||
|
pending.read_title_intro = _coerce_bool(intro_values[-1], pending.read_title_intro)
|
||||||
|
elif hasattr(form, "__contains__") and "read_title_intro" in form:
|
||||||
|
pending.read_title_intro = False
|
||||||
|
|
||||||
overrides: List[Dict[str, Any]] = []
|
overrides: List[Dict[str, Any]] = []
|
||||||
selected_total = 0
|
selected_total = 0
|
||||||
|
|
||||||
@@ -1636,6 +1651,24 @@ def _apply_book_step_form(
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
intro_default = pending.read_title_intro if isinstance(pending.read_title_intro, bool) else bool(settings.get("read_title_intro", False))
|
||||||
|
intro_values: List[str] = []
|
||||||
|
getter = getattr(form, "getlist", None)
|
||||||
|
if callable(getter):
|
||||||
|
raw_intro_values = getter("read_title_intro")
|
||||||
|
if raw_intro_values:
|
||||||
|
intro_values = list(cast(Iterable[str], raw_intro_values))
|
||||||
|
else:
|
||||||
|
raw_intro_flag = form.get("read_title_intro")
|
||||||
|
if raw_intro_flag is not None:
|
||||||
|
intro_values = [raw_intro_flag]
|
||||||
|
if intro_values:
|
||||||
|
pending.read_title_intro = _coerce_bool(intro_values[-1], intro_default)
|
||||||
|
elif hasattr(form, "__contains__") and "read_title_intro" in form:
|
||||||
|
pending.read_title_intro = False
|
||||||
|
else:
|
||||||
|
pending.read_title_intro = intro_default
|
||||||
|
|
||||||
speed_value = form.get("speed")
|
speed_value = form.get("speed")
|
||||||
if speed_value is not None:
|
if speed_value is not None:
|
||||||
try:
|
try:
|
||||||
@@ -1883,6 +1916,7 @@ BOOLEAN_SETTINGS = {
|
|||||||
"save_as_project",
|
"save_as_project",
|
||||||
"generate_epub3",
|
"generate_epub3",
|
||||||
"enable_entity_recognition",
|
"enable_entity_recognition",
|
||||||
|
"read_title_intro",
|
||||||
"auto_prefix_chapter_titles",
|
"auto_prefix_chapter_titles",
|
||||||
"normalization_numbers",
|
"normalization_numbers",
|
||||||
"normalization_titles",
|
"normalization_titles",
|
||||||
@@ -1913,6 +1947,7 @@ def _settings_defaults() -> Dict[str, Any]:
|
|||||||
"separate_chapters_format": "wav",
|
"separate_chapters_format": "wav",
|
||||||
"silence_between_chapters": 2.0,
|
"silence_between_chapters": 2.0,
|
||||||
"chapter_intro_delay": 0.5,
|
"chapter_intro_delay": 0.5,
|
||||||
|
"read_title_intro": False,
|
||||||
"max_subtitle_words": 50,
|
"max_subtitle_words": 50,
|
||||||
"chunk_level": "paragraph",
|
"chunk_level": "paragraph",
|
||||||
"enable_entity_recognition": True,
|
"enable_entity_recognition": True,
|
||||||
@@ -3375,6 +3410,7 @@ def enqueue_job() -> ResponseReturnValue:
|
|||||||
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"]
|
chapter_intro_delay = settings["chapter_intro_delay"]
|
||||||
|
read_title_intro = settings["read_title_intro"]
|
||||||
max_subtitle_words = settings["max_subtitle_words"]
|
max_subtitle_words = settings["max_subtitle_words"]
|
||||||
auto_prefix_chapter_titles = settings["auto_prefix_chapter_titles"]
|
auto_prefix_chapter_titles = settings["auto_prefix_chapter_titles"]
|
||||||
|
|
||||||
@@ -3442,6 +3478,7 @@ def enqueue_job() -> ResponseReturnValue:
|
|||||||
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,
|
chapter_intro_delay=chapter_intro_delay,
|
||||||
|
read_title_intro=bool(read_title_intro),
|
||||||
auto_prefix_chapter_titles=bool(auto_prefix_chapter_titles),
|
auto_prefix_chapter_titles=bool(auto_prefix_chapter_titles),
|
||||||
chunk_level=chunk_level_value,
|
chunk_level=chunk_level_value,
|
||||||
speaker_mode=speaker_mode_value,
|
speaker_mode=speaker_mode_value,
|
||||||
@@ -3722,6 +3759,7 @@ def finalize_job(pending_id: str) -> ResponseReturnValue:
|
|||||||
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,
|
chapter_intro_delay=pending.chapter_intro_delay,
|
||||||
|
read_title_intro=pending.read_title_intro,
|
||||||
auto_prefix_chapter_titles=getattr(pending, "auto_prefix_chapter_titles", True),
|
auto_prefix_chapter_titles=getattr(pending, "auto_prefix_chapter_titles", True),
|
||||||
chunk_level=pending.chunk_level,
|
chunk_level=pending.chunk_level,
|
||||||
chunks=processed_chunks,
|
chunks=processed_chunks,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def _create_set_event() -> threading.Event:
|
|||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
STATE_VERSION = 6
|
STATE_VERSION = 7
|
||||||
|
|
||||||
|
|
||||||
_JOB_LOGGER = logging.getLogger("abogen.jobs")
|
_JOB_LOGGER = logging.getLogger("abogen.jobs")
|
||||||
@@ -105,6 +105,7 @@ class Job:
|
|||||||
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
|
chapter_intro_delay: float = 0.5
|
||||||
|
read_title_intro: bool = False
|
||||||
auto_prefix_chapter_titles: bool = True
|
auto_prefix_chapter_titles: bool = True
|
||||||
status: JobStatus = JobStatus.PENDING
|
status: JobStatus = JobStatus.PENDING
|
||||||
started_at: Optional[float] = None
|
started_at: Optional[float] = None
|
||||||
@@ -172,6 +173,7 @@ class Job:
|
|||||||
"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,
|
"chapter_intro_delay": self.chapter_intro_delay,
|
||||||
|
"read_title_intro": getattr(self, "read_title_intro", False),
|
||||||
"auto_prefix_chapter_titles": getattr(self, "auto_prefix_chapter_titles", True),
|
"auto_prefix_chapter_titles": getattr(self, "auto_prefix_chapter_titles", True),
|
||||||
},
|
},
|
||||||
"metadata_tags": dict(self.metadata_tags),
|
"metadata_tags": dict(self.metadata_tags),
|
||||||
@@ -235,6 +237,7 @@ class PendingJob:
|
|||||||
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
|
chapter_intro_delay: float = 0.5
|
||||||
|
read_title_intro: bool = False
|
||||||
auto_prefix_chapter_titles: bool = True
|
auto_prefix_chapter_titles: bool = True
|
||||||
chunk_level: str = "paragraph"
|
chunk_level: str = "paragraph"
|
||||||
chunks: List[Dict[str, Any]] = field(default_factory=list)
|
chunks: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
@@ -315,6 +318,7 @@ class ConversionService:
|
|||||||
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,
|
chapter_intro_delay: float = 0.5,
|
||||||
|
read_title_intro: bool = False,
|
||||||
auto_prefix_chapter_titles: bool = True,
|
auto_prefix_chapter_titles: bool = True,
|
||||||
chunk_level: str = "paragraph",
|
chunk_level: str = "paragraph",
|
||||||
chunks: Optional[Iterable[Any]] = None,
|
chunks: Optional[Iterable[Any]] = None,
|
||||||
@@ -362,6 +366,7 @@ class ConversionService:
|
|||||||
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,
|
chapter_intro_delay=chapter_intro_delay,
|
||||||
|
read_title_intro=bool(read_title_intro),
|
||||||
auto_prefix_chapter_titles=bool(auto_prefix_chapter_titles),
|
auto_prefix_chapter_titles=bool(auto_prefix_chapter_titles),
|
||||||
chunk_level=chunk_level,
|
chunk_level=chunk_level,
|
||||||
chunks=normalized_chunks,
|
chunks=normalized_chunks,
|
||||||
@@ -517,6 +522,7 @@ class ConversionService:
|
|||||||
cover_image_path=job.cover_image_path,
|
cover_image_path=job.cover_image_path,
|
||||||
cover_image_mime=job.cover_image_mime,
|
cover_image_mime=job.cover_image_mime,
|
||||||
chapter_intro_delay=job.chapter_intro_delay,
|
chapter_intro_delay=job.chapter_intro_delay,
|
||||||
|
read_title_intro=job.read_title_intro,
|
||||||
auto_prefix_chapter_titles=job.auto_prefix_chapter_titles,
|
auto_prefix_chapter_titles=job.auto_prefix_chapter_titles,
|
||||||
chunk_level=job.chunk_level,
|
chunk_level=job.chunk_level,
|
||||||
chunks=job.chunks,
|
chunks=job.chunks,
|
||||||
@@ -743,6 +749,7 @@ class ConversionService:
|
|||||||
"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,
|
"chapter_intro_delay": job.chapter_intro_delay,
|
||||||
|
"read_title_intro": job.read_title_intro,
|
||||||
"auto_prefix_chapter_titles": job.auto_prefix_chapter_titles,
|
"auto_prefix_chapter_titles": job.auto_prefix_chapter_titles,
|
||||||
"chunk_level": job.chunk_level,
|
"chunk_level": job.chunk_level,
|
||||||
"chunks": [dict(entry) for entry in job.chunks],
|
"chunks": [dict(entry) for entry in job.chunks],
|
||||||
@@ -835,6 +842,7 @@ class ConversionService:
|
|||||||
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)),
|
chapter_intro_delay=float(payload.get("chapter_intro_delay", 0.5)),
|
||||||
|
read_title_intro=bool(payload.get("read_title_intro", False)),
|
||||||
auto_prefix_chapter_titles=bool(payload.get("auto_prefix_chapter_titles", True)),
|
auto_prefix_chapter_titles=bool(payload.get("auto_prefix_chapter_titles", True)),
|
||||||
)
|
)
|
||||||
job.status = JobStatus(payload.get("status", job.status.value))
|
job.status = JobStatus(payload.get("status", job.status.value))
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<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>Chapter intro delay:</strong> {{ '%.1f'|format(job.chapter_intro_delay) }}s</li>
|
||||||
|
<li><strong>Title intro:</strong> {{ 'Yes' if job.read_title_intro else 'No' }}</li>
|
||||||
<li><strong>Prefix chapter titles:</strong> {{ 'Yes' if job.auto_prefix_chapter_titles else 'No' }}</li>
|
<li><strong>Prefix chapter titles:</strong> {{ 'Yes' if job.auto_prefix_chapter_titles else 'No' }}</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>
|
||||||
|
|||||||
@@ -55,6 +55,16 @@
|
|||||||
{% set chapter_delay_value = settings_dict.get('chapter_intro_delay', 0.5) %}
|
{% set chapter_delay_value = settings_dict.get('chapter_intro_delay', 0.5) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% set read_intro_value = form_values.get('read_title_intro') if form_values else None %}
|
||||||
|
{% if read_intro_value is not none %}
|
||||||
|
{% set read_title_intro = ((read_intro_value|string)|lower) in ['true', '1', 'yes', 'on'] %}
|
||||||
|
{% else %}
|
||||||
|
{% if pending is not none %}
|
||||||
|
{% set read_title_intro = pending.read_title_intro %}
|
||||||
|
{% else %}
|
||||||
|
{% set read_title_intro = settings_dict.get('read_title_intro', False) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% set selected_config = form_values.get('speaker_config') if form_values else None %}
|
{% set selected_config = form_values.get('speaker_config') if form_values else None %}
|
||||||
{% if selected_config is none %}
|
{% if selected_config is none %}
|
||||||
{% if pending and pending.applied_speaker_config %}
|
{% if pending and pending.applied_speaker_config %}
|
||||||
@@ -265,6 +275,14 @@
|
|||||||
<input type="number" step="0.1" min="0" id="chapter_intro_delay" name="chapter_intro_delay" value="{{ '%.2f'|format(chapter_delay_value|float if chapter_delay_value is not none else 0.5) }}" {{ 'disabled' if readonly else '' }}>
|
<input type="number" step="0.1" min="0" id="chapter_intro_delay" name="chapter_intro_delay" value="{{ '%.2f'|format(chapter_delay_value|float if chapter_delay_value is not none else 0.5) }}" {{ 'disabled' if readonly else '' }}>
|
||||||
<p class="hint">Set to 0 to disable the pause after speaking each chapter title.</p>
|
<p class="hint">Set to 0 to disable the pause after speaking each chapter title.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field field--stack">
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="hidden" name="read_title_intro" value="false">
|
||||||
|
<input type="checkbox" name="read_title_intro" value="true" {% if read_title_intro %}checked{% endif %} {{ 'disabled' if readonly else '' }}>
|
||||||
|
<span>Read title and authors before narration</span>
|
||||||
|
</label>
|
||||||
|
<p class="hint">When enabled, the narrator speaks the book title and author list before chapter one begins.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<input type="hidden" name="chunk_level" value="{{ pending.chunk_level }}">
|
<input type="hidden" name="chunk_level" value="{{ pending.chunk_level }}">
|
||||||
<input type="hidden" name="speaker_analysis_threshold" value="{{ pending.speaker_analysis_threshold }}">
|
<input type="hidden" name="speaker_analysis_threshold" value="{{ pending.speaker_analysis_threshold }}">
|
||||||
<input type="hidden" name="chapter_intro_delay" value="{{ '%.2f'|format(pending.chapter_intro_delay) }}">
|
<input type="hidden" name="chapter_intro_delay" value="{{ '%.2f'|format(pending.chapter_intro_delay) }}">
|
||||||
|
<input type="hidden" name="read_title_intro" value="{{ 'true' if pending.read_title_intro else 'false' }}">
|
||||||
{% if pending.generate_epub3 %}
|
{% if pending.generate_epub3 %}
|
||||||
<input type="hidden" name="generate_epub3" value="true">
|
<input type="hidden" name="generate_epub3" value="true">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<input type="hidden" name="chunk_level" value="{{ pending.chunk_level }}">
|
<input type="hidden" name="chunk_level" value="{{ pending.chunk_level }}">
|
||||||
<input type="hidden" name="speaker_analysis_threshold" value="{{ pending.speaker_analysis_threshold }}">
|
<input type="hidden" name="speaker_analysis_threshold" value="{{ pending.speaker_analysis_threshold }}">
|
||||||
<input type="hidden" name="chapter_intro_delay" value="{{ '%.2f'|format(pending.chapter_intro_delay) }}">
|
<input type="hidden" name="chapter_intro_delay" value="{{ '%.2f'|format(pending.chapter_intro_delay) }}">
|
||||||
|
<input type="hidden" name="read_title_intro" value="{{ 'true' if pending.read_title_intro else 'false' }}">
|
||||||
{% if pending.generate_epub3 %}
|
{% if pending.generate_epub3 %}
|
||||||
<input type="hidden" name="generate_epub3" value="true">
|
<input type="hidden" name="generate_epub3" value="true">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<input type="hidden" name="speaker_mode" value="{{ pending.speaker_mode }}">
|
<input type="hidden" name="speaker_mode" value="{{ pending.speaker_mode }}">
|
||||||
<input type="hidden" name="speaker_analysis_threshold" value="{{ pending.speaker_analysis_threshold }}">
|
<input type="hidden" name="speaker_analysis_threshold" value="{{ pending.speaker_analysis_threshold }}">
|
||||||
<input type="hidden" name="chapter_intro_delay" value="{{ '%.2f'|format(pending.chapter_intro_delay) }}">
|
<input type="hidden" name="chapter_intro_delay" value="{{ '%.2f'|format(pending.chapter_intro_delay) }}">
|
||||||
|
<input type="hidden" name="read_title_intro" value="{{ 'true' if pending.read_title_intro else 'false' }}">
|
||||||
{% if pending.generate_epub3 %}
|
{% if pending.generate_epub3 %}
|
||||||
<input type="hidden" name="generate_epub3" value="true">
|
<input type="hidden" name="generate_epub3" value="true">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -123,6 +123,13 @@
|
|||||||
<input type="number" step="0.1" min="0" id="chapter_intro_delay" name="chapter_intro_delay" value="{{ '%.2f'|format(settings.chapter_intro_delay) }}">
|
<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>
|
<p class="hint">Inserted between the spoken chapter title and the chapter content. Set to 0 to disable.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field field--stack">
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="checkbox" name="read_title_intro" value="true" {% if settings.read_title_intro %}checked{% endif %}>
|
||||||
|
<span>Read Book Title Before Narration</span>
|
||||||
|
</label>
|
||||||
|
<p class="hint">When enabled, the narrator speaks the title, optional subtitle, and author names before chapter one.</p>
|
||||||
|
</div>
|
||||||
<div class="field field--stack">
|
<div class="field field--stack">
|
||||||
<label class="toggle-pill">
|
<label class="toggle-pill">
|
||||||
<input type="checkbox" name="auto_prefix_chapter_titles" value="true" {% if settings.auto_prefix_chapter_titles %}checked{% endif %}>
|
<input type="checkbox" name="auto_prefix_chapter_titles" value="true" {% if settings.auto_prefix_chapter_titles %}checked{% endif %}>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ def _make_pending_job() -> PendingJob:
|
|||||||
metadata_tags={},
|
metadata_tags={},
|
||||||
chapters=[],
|
chapters=[],
|
||||||
created_at=0.0,
|
created_at=0.0,
|
||||||
|
read_title_intro=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user