mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Implement apostrophe normalization settings and overrides in the UI and backend
This commit is contained in:
@@ -34,6 +34,11 @@ _SETTINGS_DEFAULTS: Dict[str, Any] = {
|
|||||||
"normalization_titles": True,
|
"normalization_titles": True,
|
||||||
"normalization_terminal": True,
|
"normalization_terminal": True,
|
||||||
"normalization_phoneme_hints": True,
|
"normalization_phoneme_hints": True,
|
||||||
|
"normalization_apostrophes_contractions": True,
|
||||||
|
"normalization_apostrophes_plural_possessives": True,
|
||||||
|
"normalization_apostrophes_sibilant_possessives": True,
|
||||||
|
"normalization_apostrophes_decades": True,
|
||||||
|
"normalization_apostrophes_leading_elisions": True,
|
||||||
"normalization_apostrophe_mode": "spacy",
|
"normalization_apostrophe_mode": "spacy",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +156,17 @@ def build_apostrophe_config(
|
|||||||
config = replace(base or ApostropheConfig())
|
config = replace(base or ApostropheConfig())
|
||||||
config.convert_numbers = bool(settings.get("normalization_numbers", True))
|
config.convert_numbers = bool(settings.get("normalization_numbers", True))
|
||||||
config.add_phoneme_hints = bool(settings.get("normalization_phoneme_hints", True))
|
config.add_phoneme_hints = bool(settings.get("normalization_phoneme_hints", True))
|
||||||
|
config.contraction_mode = "expand" if settings.get("normalization_apostrophes_contractions", True) else "keep"
|
||||||
|
config.plural_possessive_mode = (
|
||||||
|
"collapse" if settings.get("normalization_apostrophes_plural_possessives", True) else "keep"
|
||||||
|
)
|
||||||
|
config.sibilant_possessive_mode = (
|
||||||
|
"mark" if settings.get("normalization_apostrophes_sibilant_possessives", True) else "keep"
|
||||||
|
)
|
||||||
|
config.decades_mode = "expand" if settings.get("normalization_apostrophes_decades", True) else "keep"
|
||||||
|
config.leading_elision_mode = (
|
||||||
|
"expand" if settings.get("normalization_apostrophes_leading_elisions", True) else "keep"
|
||||||
|
)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from abogen.normalization_settings import (
|
|||||||
build_apostrophe_config,
|
build_apostrophe_config,
|
||||||
build_llm_configuration,
|
build_llm_configuration,
|
||||||
get_runtime_settings,
|
get_runtime_settings,
|
||||||
|
apply_overrides as apply_normalization_overrides,
|
||||||
)
|
)
|
||||||
from abogen.entity_analysis import normalize_token as normalize_entity_token
|
from abogen.entity_analysis import normalize_token as normalize_entity_token
|
||||||
from abogen.text_extractor import ExtractedChapter, extract_from_path
|
from abogen.text_extractor import ExtractedChapter, extract_from_path
|
||||||
@@ -669,6 +670,20 @@ def _merge_metadata(
|
|||||||
_APOSTROPHE_CONFIG = ApostropheConfig()
|
_APOSTROPHE_CONFIG = ApostropheConfig()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_for_pipeline(
|
||||||
|
text: str,
|
||||||
|
*,
|
||||||
|
normalization_overrides: Optional[Mapping[str, Any]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Normalize text for tests or utilities with optional overrides."""
|
||||||
|
|
||||||
|
runtime_settings = get_runtime_settings()
|
||||||
|
if normalization_overrides:
|
||||||
|
runtime_settings = apply_normalization_overrides(runtime_settings, normalization_overrides)
|
||||||
|
apostrophe_config = build_apostrophe_config(settings=runtime_settings, base=_APOSTROPHE_CONFIG)
|
||||||
|
return normalize_for_pipeline(text, config=apostrophe_config, settings=runtime_settings)
|
||||||
|
|
||||||
|
|
||||||
def _compile_pronunciation_rules(
|
def _compile_pronunciation_rules(
|
||||||
overrides: Optional[Iterable[Mapping[str, Any]]],
|
overrides: Optional[Iterable[Mapping[str, Any]]],
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
@@ -1101,6 +1116,9 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
canceller = _make_canceller(job)
|
canceller = _make_canceller(job)
|
||||||
|
|
||||||
normalization_settings = get_runtime_settings()
|
normalization_settings = get_runtime_settings()
|
||||||
|
job_overrides = getattr(job, "normalization_overrides", None)
|
||||||
|
if job_overrides:
|
||||||
|
normalization_settings = apply_normalization_overrides(normalization_settings, job_overrides)
|
||||||
apostrophe_config = build_apostrophe_config(
|
apostrophe_config = build_apostrophe_config(
|
||||||
settings=normalization_settings,
|
settings=normalization_settings,
|
||||||
base=_APOSTROPHE_CONFIG,
|
base=_APOSTROPHE_CONFIG,
|
||||||
|
|||||||
@@ -1723,6 +1723,30 @@ def _apply_book_step_form(
|
|||||||
else:
|
else:
|
||||||
pending.normalize_chapter_opening_caps = caps_default
|
pending.normalize_chapter_opening_caps = caps_default
|
||||||
|
|
||||||
|
def _extract_checkbox(name: str, default: bool) -> bool:
|
||||||
|
values: List[str] = []
|
||||||
|
getter = getattr(form, "getlist", None)
|
||||||
|
if callable(getter):
|
||||||
|
raw_values = getter(name)
|
||||||
|
if raw_values:
|
||||||
|
values = list(cast(Iterable[str], raw_values))
|
||||||
|
else:
|
||||||
|
raw_flag = form.get(name)
|
||||||
|
if raw_flag is not None:
|
||||||
|
values = [raw_flag]
|
||||||
|
if values:
|
||||||
|
return _coerce_bool(values[-1], default)
|
||||||
|
if hasattr(form, "__contains__") and name in form:
|
||||||
|
return False
|
||||||
|
return default
|
||||||
|
|
||||||
|
overrides_existing = getattr(pending, "normalization_overrides", None)
|
||||||
|
overrides: Dict[str, bool] = dict(overrides_existing or {})
|
||||||
|
for key in _APOSTROPHE_OVERRIDE_KEYS:
|
||||||
|
default_toggle = overrides.get(key, bool(settings.get(key, True)))
|
||||||
|
overrides[key] = _extract_checkbox(key, default_toggle)
|
||||||
|
pending.normalization_overrides = overrides
|
||||||
|
|
||||||
speed_value = form.get("speed")
|
speed_value = form.get("speed")
|
||||||
if speed_value is not None:
|
if speed_value is not None:
|
||||||
try:
|
try:
|
||||||
@@ -1977,11 +2001,24 @@ BOOLEAN_SETTINGS = {
|
|||||||
"normalization_titles",
|
"normalization_titles",
|
||||||
"normalization_terminal",
|
"normalization_terminal",
|
||||||
"normalization_phoneme_hints",
|
"normalization_phoneme_hints",
|
||||||
|
"normalization_apostrophes_contractions",
|
||||||
|
"normalization_apostrophes_plural_possessives",
|
||||||
|
"normalization_apostrophes_sibilant_possessives",
|
||||||
|
"normalization_apostrophes_decades",
|
||||||
|
"normalization_apostrophes_leading_elisions",
|
||||||
}
|
}
|
||||||
|
|
||||||
FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay", "llm_timeout"}
|
FLOAT_SETTINGS = {"silence_between_chapters", "chapter_intro_delay", "llm_timeout"}
|
||||||
INT_SETTINGS = {"max_subtitle_words", "speaker_analysis_threshold"}
|
INT_SETTINGS = {"max_subtitle_words", "speaker_analysis_threshold"}
|
||||||
|
|
||||||
|
_APOSTROPHE_OVERRIDE_KEYS = (
|
||||||
|
"normalization_apostrophes_contractions",
|
||||||
|
"normalization_apostrophes_plural_possessives",
|
||||||
|
"normalization_apostrophes_sibilant_possessives",
|
||||||
|
"normalization_apostrophes_decades",
|
||||||
|
"normalization_apostrophes_leading_elisions",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _integration_defaults() -> Dict[str, Dict[str, Any]]:
|
def _integration_defaults() -> Dict[str, Dict[str, Any]]:
|
||||||
return {
|
return {
|
||||||
@@ -2047,6 +2084,11 @@ def _settings_defaults() -> Dict[str, Any]:
|
|||||||
"normalization_titles": True,
|
"normalization_titles": True,
|
||||||
"normalization_terminal": True,
|
"normalization_terminal": True,
|
||||||
"normalization_phoneme_hints": True,
|
"normalization_phoneme_hints": True,
|
||||||
|
"normalization_apostrophes_contractions": True,
|
||||||
|
"normalization_apostrophes_plural_possessives": True,
|
||||||
|
"normalization_apostrophes_sibilant_possessives": True,
|
||||||
|
"normalization_apostrophes_decades": True,
|
||||||
|
"normalization_apostrophes_leading_elisions": True,
|
||||||
"normalization_apostrophe_mode": "spacy",
|
"normalization_apostrophe_mode": "spacy",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3125,6 +3167,11 @@ def api_normalization_preview() -> ResponseReturnValue:
|
|||||||
"normalization_titles",
|
"normalization_titles",
|
||||||
"normalization_terminal",
|
"normalization_terminal",
|
||||||
"normalization_phoneme_hints",
|
"normalization_phoneme_hints",
|
||||||
|
"normalization_apostrophes_contractions",
|
||||||
|
"normalization_apostrophes_plural_possessives",
|
||||||
|
"normalization_apostrophes_sibilant_possessives",
|
||||||
|
"normalization_apostrophes_decades",
|
||||||
|
"normalization_apostrophes_leading_elisions",
|
||||||
)
|
)
|
||||||
for key in boolean_keys:
|
for key in boolean_keys:
|
||||||
if key in normalization_payload:
|
if key in normalization_payload:
|
||||||
@@ -4076,6 +4123,9 @@ def _build_pending_job_from_extraction(
|
|||||||
max_subtitle_words=max_subtitle_words,
|
max_subtitle_words=max_subtitle_words,
|
||||||
metadata_tags=metadata_tags,
|
metadata_tags=metadata_tags,
|
||||||
chapters=chapters_payload,
|
chapters=chapters_payload,
|
||||||
|
normalization_overrides={
|
||||||
|
key: bool(settings.get(key, True)) for key in _APOSTROPHE_OVERRIDE_KEYS
|
||||||
|
},
|
||||||
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,
|
||||||
@@ -4455,6 +4505,7 @@ def finalize_job(pending_id: str) -> ResponseReturnValue:
|
|||||||
entity_summary=pending.entity_summary,
|
entity_summary=pending.entity_summary,
|
||||||
manual_overrides=pending.manual_overrides,
|
manual_overrides=pending.manual_overrides,
|
||||||
pronunciation_overrides=pending.pronunciation_overrides,
|
pronunciation_overrides=pending.pronunciation_overrides,
|
||||||
|
normalization_overrides=pending.normalization_overrides,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config_languages:
|
if config_languages:
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ class Job:
|
|||||||
entity_summary: Dict[str, Any] = field(default_factory=dict)
|
entity_summary: Dict[str, Any] = field(default_factory=dict)
|
||||||
manual_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
manual_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
normalization_overrides: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
def add_log(self, message: str, level: str = "info") -> None:
|
def add_log(self, message: str, level: str = "info") -> None:
|
||||||
entry = JobLog(timestamp=time.time(), message=message, level=level)
|
entry = JobLog(timestamp=time.time(), message=message, level=level)
|
||||||
@@ -216,6 +217,7 @@ class Job:
|
|||||||
"entity_summary": dict(self.entity_summary),
|
"entity_summary": dict(self.entity_summary),
|
||||||
"manual_overrides": [dict(entry) for entry in self.manual_overrides],
|
"manual_overrides": [dict(entry) for entry in self.manual_overrides],
|
||||||
"pronunciation_overrides": [dict(entry) for entry in self.pronunciation_overrides],
|
"pronunciation_overrides": [dict(entry) for entry in self.pronunciation_overrides],
|
||||||
|
"normalization_overrides": dict(self.normalization_overrides),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -431,6 +433,7 @@ class PendingJob:
|
|||||||
max_subtitle_words: int
|
max_subtitle_words: int
|
||||||
metadata_tags: Dict[str, Any]
|
metadata_tags: Dict[str, Any]
|
||||||
chapters: List[Dict[str, Any]]
|
chapters: List[Dict[str, Any]]
|
||||||
|
normalization_overrides: Dict[str, Any]
|
||||||
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
|
||||||
@@ -531,6 +534,7 @@ class ConversionService:
|
|||||||
entity_summary: Optional[Mapping[str, Any]] = None,
|
entity_summary: Optional[Mapping[str, Any]] = None,
|
||||||
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
manual_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||||
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
pronunciation_overrides: Optional[Iterable[Mapping[str, Any]]] = None,
|
||||||
|
normalization_overrides: Optional[Mapping[str, Any]] = None,
|
||||||
) -> 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)
|
||||||
@@ -580,6 +584,7 @@ class ConversionService:
|
|||||||
entity_summary=dict(entity_summary or {}),
|
entity_summary=dict(entity_summary or {}),
|
||||||
manual_overrides=[dict(entry) for entry in manual_overrides] if manual_overrides else [],
|
manual_overrides=[dict(entry) for entry in manual_overrides] if manual_overrides else [],
|
||||||
pronunciation_overrides=[dict(entry) for entry in pronunciation_overrides] if pronunciation_overrides else [],
|
pronunciation_overrides=[dict(entry) for entry in pronunciation_overrides] if pronunciation_overrides else [],
|
||||||
|
normalization_overrides=dict(normalization_overrides or {}),
|
||||||
)
|
)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._jobs[job_id] = job
|
self._jobs[job_id] = job
|
||||||
@@ -737,6 +742,7 @@ class ConversionService:
|
|||||||
entity_summary=job.entity_summary,
|
entity_summary=job.entity_summary,
|
||||||
manual_overrides=job.manual_overrides,
|
manual_overrides=job.manual_overrides,
|
||||||
pronunciation_overrides=job.pronunciation_overrides,
|
pronunciation_overrides=job.pronunciation_overrides,
|
||||||
|
normalization_overrides=job.normalization_overrides,
|
||||||
)
|
)
|
||||||
|
|
||||||
new_job.speaker_voice_languages = list(job.speaker_voice_languages)
|
new_job.speaker_voice_languages = list(job.speaker_voice_languages)
|
||||||
@@ -1039,6 +1045,7 @@ class ConversionService:
|
|||||||
"entity_summary": dict(job.entity_summary),
|
"entity_summary": dict(job.entity_summary),
|
||||||
"manual_overrides": [dict(entry) for entry in job.manual_overrides],
|
"manual_overrides": [dict(entry) for entry in job.manual_overrides],
|
||||||
"pronunciation_overrides": [dict(entry) for entry in job.pronunciation_overrides],
|
"pronunciation_overrides": [dict(entry) for entry in job.pronunciation_overrides],
|
||||||
|
"normalization_overrides": dict(job.normalization_overrides),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _persist_state(self) -> None:
|
def _persist_state(self) -> None:
|
||||||
@@ -1164,6 +1171,7 @@ class ConversionService:
|
|||||||
job.pronunciation_overrides = [
|
job.pronunciation_overrides = [
|
||||||
dict(entry) for entry in payload.get("pronunciation_overrides", []) if isinstance(entry, Mapping)
|
dict(entry) for entry in payload.get("pronunciation_overrides", []) if isinstance(entry, Mapping)
|
||||||
]
|
]
|
||||||
|
job.normalization_overrides = dict(payload.get("normalization_overrides", {}) or {})
|
||||||
job.pause_event.set()
|
job.pause_event.set()
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
|||||||
@@ -264,6 +264,11 @@ function collectNormalizationSettings() {
|
|||||||
normalization_titles: Boolean(form.querySelector('input[name="normalization_titles"]')?.checked),
|
normalization_titles: Boolean(form.querySelector('input[name="normalization_titles"]')?.checked),
|
||||||
normalization_terminal: Boolean(form.querySelector('input[name="normalization_terminal"]')?.checked),
|
normalization_terminal: Boolean(form.querySelector('input[name="normalization_terminal"]')?.checked),
|
||||||
normalization_phoneme_hints: Boolean(form.querySelector('input[name="normalization_phoneme_hints"]')?.checked),
|
normalization_phoneme_hints: Boolean(form.querySelector('input[name="normalization_phoneme_hints"]')?.checked),
|
||||||
|
normalization_apostrophes_contractions: Boolean(form.querySelector('input[name="normalization_apostrophes_contractions"]')?.checked),
|
||||||
|
normalization_apostrophes_plural_possessives: Boolean(form.querySelector('input[name="normalization_apostrophes_plural_possessives"]')?.checked),
|
||||||
|
normalization_apostrophes_sibilant_possessives: Boolean(form.querySelector('input[name="normalization_apostrophes_sibilant_possessives"]')?.checked),
|
||||||
|
normalization_apostrophes_decades: Boolean(form.querySelector('input[name="normalization_apostrophes_decades"]')?.checked),
|
||||||
|
normalization_apostrophes_leading_elisions: Boolean(form.querySelector('input[name="normalization_apostrophes_leading_elisions"]')?.checked),
|
||||||
normalization_apostrophe_mode: form.querySelector('input[name="normalization_apostrophe_mode"]:checked')?.value || 'spacy',
|
normalization_apostrophe_mode: form.querySelector('input[name="normalization_apostrophe_mode"]:checked')?.value || 'spacy',
|
||||||
};
|
};
|
||||||
return normalization;
|
return normalization;
|
||||||
|
|||||||
@@ -112,6 +112,34 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set narrator_voice = settings_dict.get('default_voice', options.voices[0] if options.voices else '') %}
|
{% set narrator_voice = settings_dict.get('default_voice', options.voices[0] if options.voices else '') %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% set normalization_overrides = pending.normalization_overrides if pending and pending.normalization_overrides else {} %}
|
||||||
|
{% set apostrophe_toggles = [
|
||||||
|
{
|
||||||
|
'name': 'normalization_apostrophes_contractions',
|
||||||
|
'label': "Expand contractions (it's -> it is)",
|
||||||
|
'value': normalization_overrides.get('normalization_apostrophes_contractions', settings_dict.get('normalization_apostrophes_contractions', True)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'normalization_apostrophes_plural_possessives',
|
||||||
|
'label': "Collapse plural possessives (dogs' -> dogs)",
|
||||||
|
'value': normalization_overrides.get('normalization_apostrophes_plural_possessives', settings_dict.get('normalization_apostrophes_plural_possessives', True)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'normalization_apostrophes_sibilant_possessives',
|
||||||
|
'label': "Mark sibilant possessives (boss's -> boss + IZ marker)",
|
||||||
|
'value': normalization_overrides.get('normalization_apostrophes_sibilant_possessives', settings_dict.get('normalization_apostrophes_sibilant_possessives', True)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'normalization_apostrophes_decades',
|
||||||
|
'label': "Expand decades ('90s -> 1990s)",
|
||||||
|
'value': normalization_overrides.get('normalization_apostrophes_decades', settings_dict.get('normalization_apostrophes_decades', True)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'normalization_apostrophes_leading_elisions',
|
||||||
|
'label': "Expand leading elisions ('tis -> it is)",
|
||||||
|
'value': normalization_overrides.get('normalization_apostrophes_leading_elisions', settings_dict.get('normalization_apostrophes_leading_elisions', True)),
|
||||||
|
},
|
||||||
|
] %}
|
||||||
{% set voice_formula_value = '' %}
|
{% set voice_formula_value = '' %}
|
||||||
{% set profile_value = narrator_profile if narrator_profile else '__standard' %}
|
{% set profile_value = narrator_profile if narrator_profile else '__standard' %}
|
||||||
{% if profile_value == '__formula' %}
|
{% if profile_value == '__formula' %}
|
||||||
@@ -253,6 +281,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="form-section">
|
||||||
|
<h3 class="form-section__title">Text normalization</h3>
|
||||||
|
<div class="field-grid field-grid--compact">
|
||||||
|
{% for toggle in apostrophe_toggles %}
|
||||||
|
<div class="field field--stack">
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="hidden" name="{{ toggle.name }}" value="false">
|
||||||
|
<input type="checkbox" name="{{ toggle.name }}" value="true" {% if toggle.value %}checked{% endif %} {{ 'disabled' if readonly else '' }}>
|
||||||
|
<span>{{ toggle.label }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="form-section">
|
<section class="form-section">
|
||||||
<h3 class="form-section__title">Entities & casting</h3>
|
<h3 class="form-section__title">Entities & casting</h3>
|
||||||
<div class="field-grid field-grid--compact">
|
<div class="field-grid field-grid--compact">
|
||||||
|
|||||||
@@ -269,6 +269,27 @@
|
|||||||
<input type="checkbox" name="normalization_phoneme_hints" value="true" {% if settings.normalization_phoneme_hints %}checked{% endif %}>
|
<input type="checkbox" name="normalization_phoneme_hints" value="true" {% if settings.normalization_phoneme_hints %}checked{% endif %}>
|
||||||
<span>Add phoneme hints for possessives</span>
|
<span>Add phoneme hints for possessives</span>
|
||||||
</label>
|
</label>
|
||||||
|
<span class="field__caption">Apostrophes</span>
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="checkbox" name="normalization_apostrophes_contractions" value="true" {% if settings.normalization_apostrophes_contractions %}checked{% endif %}>
|
||||||
|
<span>Expand contractions ("it's" -> "it is")</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="checkbox" name="normalization_apostrophes_plural_possessives" value="true" {% if settings.normalization_apostrophes_plural_possessives %}checked{% endif %}>
|
||||||
|
<span>Collapse plural possessives (dogs' -> dogs)</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="checkbox" name="normalization_apostrophes_sibilant_possessives" value="true" {% if settings.normalization_apostrophes_sibilant_possessives %}checked{% endif %}>
|
||||||
|
<span>Add guidance for sibilant possessives (boss's -> boss + IZ marker)</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="checkbox" name="normalization_apostrophes_decades" value="true" {% if settings.normalization_apostrophes_decades %}checked{% endif %}>
|
||||||
|
<span>Expand decades ('90s -> 1990s)</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-pill">
|
||||||
|
<input type="checkbox" name="normalization_apostrophes_leading_elisions" value="true" {% if settings.normalization_apostrophes_leading_elisions %}checked{% endif %}>
|
||||||
|
<span>Expand leading elisions ('tis -> it is)</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="field__label">Apostrophe strategy</span>
|
<span class="field__label">Apostrophe strategy</span>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ def _make_pending_job() -> PendingJob:
|
|||||||
max_subtitle_words=50,
|
max_subtitle_words=50,
|
||||||
metadata_tags={},
|
metadata_tags={},
|
||||||
chapters=[],
|
chapters=[],
|
||||||
|
normalization_overrides={},
|
||||||
created_at=0.0,
|
created_at=0.0,
|
||||||
read_title_intro=False,
|
read_title_intro=False,
|
||||||
normalize_chapter_opening_caps=True,
|
normalize_chapter_opening_caps=True,
|
||||||
|
|||||||
@@ -78,3 +78,28 @@ def test_numeric_ranges_are_spoken_with_to() -> None:
|
|||||||
def test_simple_fractions_are_spoken() -> None:
|
def test_simple_fractions_are_spoken() -> None:
|
||||||
normalized = _normalize_for_pipeline("Add 1/2 cup of sugar")
|
normalized = _normalize_for_pipeline("Add 1/2 cup of sugar")
|
||||||
assert "one half" in normalized.lower()
|
assert "one half" in normalized.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_contractions_can_be_kept_when_override_disabled() -> None:
|
||||||
|
normalized = _normalize_for_pipeline(
|
||||||
|
"It's a good day.",
|
||||||
|
normalization_overrides={"normalization_apostrophes_contractions": False},
|
||||||
|
)
|
||||||
|
assert "It's" in normalized
|
||||||
|
|
||||||
|
|
||||||
|
def test_sibilant_possessives_remain_when_marking_disabled() -> None:
|
||||||
|
normalized = _normalize_for_pipeline(
|
||||||
|
"The boss's chair wobbled.",
|
||||||
|
normalization_overrides={"normalization_apostrophes_sibilant_possessives": False},
|
||||||
|
)
|
||||||
|
assert "boss's" in normalized
|
||||||
|
assert "boss iz" not in normalized.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def test_decades_can_skip_expansion_when_disabled() -> None:
|
||||||
|
normalized = _normalize_for_pipeline(
|
||||||
|
"Classic hits from the '90s filled the hall.",
|
||||||
|
normalization_overrides={"normalization_apostrophes_decades": False},
|
||||||
|
)
|
||||||
|
assert "'90s" in normalized
|
||||||
|
|||||||
Reference in New Issue
Block a user