mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
391 lines
12 KiB
Python
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
|