Files
abogen/abogen/debug_tts_samples.py
T
2026-01-09 01:36:14 +03:00

391 lines
12 KiB
Python

from __future__ import annotations
import tempfile
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Sequence
MARKER_PREFIX = "[[ABOGEN-DBG:"
MARKER_SUFFIX = "]]"
@dataclass(frozen=True)
class DebugTTSSample:
code: str
label: str
text: str
DEBUG_TTS_SAMPLES: Sequence[DebugTTSSample] = (
DebugTTSSample(
code="APOS_001",
label="Apostrophes & contractions (1)",
text="It's a beautiful day, isn't it? Let's see what we'll do.",
),
DebugTTSSample(
code="APOS_002",
label="Apostrophes & contractions (2)",
text="I'm sure you're ready; we'd better go before it's too late.",
),
DebugTTSSample(
code="APOS_003",
label="Apostrophes & contractions (3)",
text="He'll say it's fine, but I can't promise it'll work.",
),
DebugTTSSample(
code="APOS_004",
label="Apostrophes & contractions (4)",
text="They've done it, and I'd agree they've earned it.",
),
DebugTTSSample(
code="APOS_005",
label="Apostrophes & contractions (5)",
text="She's here, we're late, they're waiting, and you're right.",
),
DebugTTSSample(
code="POS_001",
label="Plural possessives (1)",
text="The dogs' bowls were empty, but the boss's office was quiet.",
),
DebugTTSSample(
code="POS_002",
label="Plural possessives (2)",
text="The teachers' lounge was closed during the students' exams.",
),
DebugTTSSample(
code="POS_003",
label="Plural possessives (3)",
text="The actresses' roles changed, and the directors' notes piled up.",
),
DebugTTSSample(
code="POS_004",
label="Plural possessives (4)",
text="The Joneses' car was parked by the neighbors' fence.",
),
DebugTTSSample(
code="POS_005",
label="Plural possessives (5)",
text="The bosses' meeting ended before the witnesses' statements began.",
),
DebugTTSSample(
code="NUM_001",
label="Grouped numbers (1)",
text="There are 1,234 apples, 56 oranges, and 7.89 liters of juice.",
),
DebugTTSSample(
code="NUM_002",
label="Grouped numbers (2)",
text="The population is 10,000,000 and the area is 123.45 square miles.",
),
DebugTTSSample(
code="NUM_003",
label="Grouped numbers (3)",
text="Set the timer for 0.5 seconds, then wait 2.0 minutes.",
),
DebugTTSSample(
code="NUM_004",
label="Grouped numbers (4)",
text="We measured 3.1415 radians and wrote down 2,718.28 as well.",
),
DebugTTSSample(
code="NUM_005",
label="Grouped numbers (5)",
text="The sequence is 1, 2, 3, 4, 5, and then 13.",
),
DebugTTSSample(
code="YEAR_001",
label="Years and decades (1)",
text="In 1999, people said the '90s were over.",
),
DebugTTSSample(
code="YEAR_002",
label="Years and decades (2)",
text="In 2001, the show premiered; by 2010 it was everywhere.",
),
DebugTTSSample(
code="YEAR_003",
label="Years and decades (3)",
text="The 1980s were loud, and the 1970s were groovy.",
),
DebugTTSSample(
code="YEAR_004",
label="Years and decades (4)",
text="She loved the '80s, but he preferred the '60s.",
),
DebugTTSSample(
code="YEAR_005",
label="Years and decades (5)",
text="In 2024, we looked back at 2020 and planned for 2030.",
),
DebugTTSSample(
code="DATE_001",
label="Dates (1)",
text="On 2023-01-01, we celebrated the new year.",
),
DebugTTSSample(
code="DATE_002",
label="Dates (2)",
text="The deadline is 1999-12-31 at midnight.",
),
DebugTTSSample(
code="DATE_003",
label="Dates (3)",
text="Leap day happens on 2024-02-29.",
),
DebugTTSSample(
code="DATE_004",
label="Dates (4)",
text="Some formats look like 01/02/2003 and can be ambiguous.",
),
DebugTTSSample(
code="DATE_005",
label="Dates (5)",
text="We met on March 5, 2020 and again on Apr. 7, 2021.",
),
DebugTTSSample(
code="CUR_001",
label="Currency symbols (1)",
text="The price is $10.50, but it was £8.00 yesterday.",
),
DebugTTSSample(
code="CUR_002",
label="Currency symbols (2)",
text="Tickets cost €12, and the fine was $0.99.",
),
DebugTTSSample(
code="CUR_003",
label="Currency symbols (3)",
text="The bill was ¥500 and the refund was $-3.25.",
),
DebugTTSSample(
code="CUR_004",
label="Currency symbols (4)",
text="He paid £1,234.56 for the instrument.",
),
DebugTTSSample(
code="CUR_005",
label="Currency symbols (5)",
text="The subscription is $5 per month, or $50 per year.",
),
DebugTTSSample(
code="TITLE_001",
label="Titles and abbreviations (1)",
text="Dr. Smith lives on Elm St. near the U.S. border.",
),
DebugTTSSample(
code="TITLE_002",
label="Titles and abbreviations (2)",
text="Mr. and Mrs. Doe met Prof. Adams at 5 p.m.",
),
DebugTTSSample(
code="TITLE_003",
label="Titles and abbreviations (3)",
text="Gen. Smith spoke to Sgt. Rivera on Main St.",
),
DebugTTSSample(
code="TITLE_004",
label="Titles and abbreviations (4)",
text="The report came from the U.K. office, not the U.S.A. team.",
),
DebugTTSSample(
code="TITLE_005",
label="Titles and abbreviations (5)",
text="St. John's is different from St. Louis.",
),
DebugTTSSample(
code="PUNC_001",
label="Terminal punctuation (1)",
text="This sentence ends without punctuation",
),
DebugTTSSample(
code="PUNC_002",
label="Terminal punctuation (2)",
text="An ellipsis is already present...",
),
DebugTTSSample(
code="PUNC_003",
label="Terminal punctuation (3)",
text="A question without a mark",
),
DebugTTSSample(
code="PUNC_004",
label="Terminal punctuation (4)",
text="An exclamation without a bang",
),
DebugTTSSample(
code="PUNC_005",
label="Terminal punctuation (5)",
text='A quote ends here"',
),
DebugTTSSample(
code="QUOTE_001",
label="ALL CAPS inside quotes (1)",
text='He shouted, "THIS IS IMPORTANT!" and then whispered, "ok."',
),
DebugTTSSample(
code="QUOTE_002",
label="ALL CAPS inside quotes (2)",
text='She said, "NO WAY", but he replied, "maybe".',
),
DebugTTSSample(
code="QUOTE_003",
label="ALL CAPS inside quotes (3)",
text='The sign read "DO NOT ENTER" and the note read "pls knock".',
),
DebugTTSSample(
code="QUOTE_004",
label="ALL CAPS inside quotes (4)",
text='He muttered, "OK", then yelled, "STOP!"',
),
DebugTTSSample(
code="QUOTE_005",
label="ALL CAPS inside quotes (5)",
text='They chanted, "USA!" and someone wrote "idk".',
),
DebugTTSSample(
code="FOOT_001",
label="Footnote indicators (1)",
text="This is a sentence with a footnote[1] and another[12].",
),
DebugTTSSample(
code="FOOT_002",
label="Footnote indicators (2)",
text="Some books use multiple footnotes like this[2][3] in a row.",
),
DebugTTSSample(
code="FOOT_003",
label="Footnote indicators (3)",
text="A footnote can appear mid-sentence[4] and continue afterward.",
),
DebugTTSSample(
code="FOOT_004",
label="Footnote indicators (4)",
text="Edge cases include [0] or very large indices like [1234].",
),
DebugTTSSample(
code="FOOT_005",
label="Footnote indicators (5)",
text="Sometimes a footnote follows punctuation.[5] Sometimes it doesn't[6]",
),
)
def marker_for(code: str) -> str:
return f"{MARKER_PREFIX}{code}{MARKER_SUFFIX}"
def build_debug_epub(dest_path: Path, *, title: str = "abogen debug samples") -> Path:
"""Create a tiny EPUB containing all debug samples.
The text includes stable marker codes so developers can report failures
precisely.
"""
dest_path = Path(dest_path)
dest_path.parent.mkdir(parents=True, exist_ok=True)
chapter_lines: List[str] = [
'<?xml version="1.0" encoding="utf-8"?>',
"<!DOCTYPE html>",
'<html xmlns="http://www.w3.org/1999/xhtml">',
"<head>",
f" <title>{title}</title>",
' <meta charset="utf-8" />',
"</head>",
"<body>",
f" <h1>{title}</h1>",
" <p>Each paragraph begins with a stable debug code marker.</p>",
]
for sample in DEBUG_TTS_SAMPLES:
safe_label = sample.label.replace("&", "and")
chapter_lines.append(f" <h2>{safe_label}</h2>")
chapter_lines.append(
" <p><strong>"
+ marker_for(sample.code)
+ "</strong> "
+ sample.text
+ "</p>"
)
chapter_lines += ["</body>", "</html>"]
chapter_xhtml = "\n".join(chapter_lines)
container_xml = """<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
"""
content_opf = """<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" version="3.0">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:identifier id="bookid">abogen-debug-samples</dc:identifier>
<dc:title>abogen debug samples</dc:title>
<dc:language>en</dc:language>
</metadata>
<manifest>
<item id="chapter" href="chapter.xhtml" media-type="application/xhtml+xml" />
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav" />
</manifest>
<spine>
<itemref idref="chapter" />
</spine>
</package>
"""
nav_xhtml = """<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Navigation</title>
<meta charset="utf-8" />
</head>
<body>
<nav epub:type="toc" id="toc">
<h2>Table of Contents</h2>
<ol>
<li><a href="chapter.xhtml">Debug samples</a></li>
</ol>
</nav>
</body>
</html>
"""
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
(tmp_path / "mimetype").write_text("application/epub+zip", encoding="utf-8")
meta_inf = tmp_path / "META-INF"
meta_inf.mkdir(parents=True, exist_ok=True)
(meta_inf / "container.xml").write_text(container_xml, encoding="utf-8")
oebps = tmp_path / "OEBPS"
oebps.mkdir(parents=True, exist_ok=True)
(oebps / "content.opf").write_text(content_opf, encoding="utf-8")
(oebps / "chapter.xhtml").write_text(chapter_xhtml, encoding="utf-8")
(oebps / "nav.xhtml").write_text(nav_xhtml, encoding="utf-8")
# Per EPUB spec: mimetype must be the first entry and stored (no compression).
with zipfile.ZipFile(dest_path, "w") as zf:
zf.write(
tmp_path / "mimetype", "mimetype", compress_type=zipfile.ZIP_STORED
)
for source in (
meta_inf / "container.xml",
oebps / "content.opf",
oebps / "chapter.xhtml",
oebps / "nav.xhtml",
):
arcname = str(source.relative_to(tmp_path)).replace("\\", "/")
zf.write(source, arcname, compress_type=zipfile.ZIP_DEFLATED)
return dest_path
def iter_expected_codes() -> Iterable[str]:
for sample in DEBUG_TTS_SAMPLES:
yield sample.code