This commit is contained in:
Shaman
2026-10-01 07:38:01 +03:00
commit 82c306ff69
33 changed files with 2629 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
from app.segmentation import Segment
from app.ssml import (
PAUSE_DELIM,
build_ssml,
build_ssml_chunks,
explicit_pause_char,
strip_sentinels,
)
def seg(text, lang="ru", para=0):
return Segment(text=text, lang=lang, para=para)
# --- 2.1 структура <speak>/<p>/<s> и <prosody> ---
def test_single_segment_structure():
out = build_ssml([seg("Привет мир")])
assert out == "<speak><p><s>Привет мир</s></p></speak>"
def test_empty_returns_empty():
assert build_ssml([]) == ""
def test_paragraph_grouping_nesting():
out = build_ssml([seg("а", para=0), seg("б", para=0), seg("в", para=1)])
assert out.count("<p>") == 2
assert out.count("<s>") == 3
assert "<p><s>а</s><s>б</s></p>" in out
def test_no_prosody_by_default():
out = build_ssml([seg("привет")])
assert "<prosody" not in out
def test_prosody_rate_present_once():
out = build_ssml([seg("а"), seg("б")], rate="fast")
assert out.count("<prosody") == 1
assert 'rate="fast"' in out
def test_prosody_rate_and_pitch():
out = build_ssml([seg("а")], rate="slow", pitch="high")
assert '<prosody rate="slow" pitch="high">' in out
assert out.endswith("</prosody></speak>")
def test_prosody_medium_omitted():
out = build_ssml([seg("а")], rate="medium", pitch="medium")
assert "<prosody" not in out
# --- 2.2 sentinel -> <break>, вырезание PUA ---
def test_sentinel_becomes_single_break():
out = build_ssml([seg(f"а{PAUSE_DELIM}б")])
assert out.count("<break") == 1
assert 'time="150ms"' in out
assert PAUSE_DELIM not in out
def test_explicit_pause_to_break_ms():
out = build_ssml([seg("а" + explicit_pause_char(800) + "б")])
assert 'time="800ms"' in out
assert "а<break" in out and "б" in out
def test_explicit_pause_clamped_in_builder():
out = build_ssml([seg("а" + explicit_pause_char(9999) + "б")])
assert 'time="5000ms"' in out
def test_break_not_escaped():
out = build_ssml([seg(f"а{PAUSE_DELIM}б")])
assert "&lt;break" not in out
# --- экранирование текста ---
def test_text_is_xml_escaped():
out = build_ssml([seg("a & b <c> d")])
assert "&amp;" in out and "&lt;" in out and "&gt;" in out
assert "<c>" not in out
# --- 2.4 разбиение по абзацам/лимиту ---
def test_chunking_splits_into_valid_documents():
segs = [seg("предложение номер " + str(i) + " достаточно длинное") for i in range(20)]
chunks = build_ssml_chunks(segs, max_chars=200)
assert len(chunks) >= 2
for c in chunks:
assert c.startswith("<speak>") and c.endswith("</speak>")
# ни одно предложение не потеряно и не разорвано
assert sum(c.count("<s>") for c in chunks) == len(segs)
def test_chunking_respects_max_chars():
segs = [seg("с" * 60) for _ in range(10)]
chunks = build_ssml_chunks(segs, max_chars=200)
assert len(chunks) > 1
for c in chunks:
assert c.count("<s>") >= 1
def test_paragraphs_never_split_mid_segment():
segs = [seg("маленький абзац", para=0), seg("другой абзац", para=1)]
chunks = build_ssml_chunks(segs, max_chars=5000)
assert len(chunks) == 1
assert "<p><s>маленький абзац</s></p>" in chunks[0]
assert "<p><s>другой абзац</s></p>" in chunks[0]
def test_chunking_applies_prosody_to_each_document():
segs = [seg("текст " * 30) for _ in range(10)]
chunks = build_ssml_chunks(segs, rate="fast", max_chars=200)
assert len(chunks) >= 2
for c in chunks:
assert 'rate="fast"' in c
# --- strip_sentinels (для webui-отображения) ---
def test_strip_sentinels_removes_all_and_collapses_spaces():
raw = f"он {PAUSE_DELIM} сказал {explicit_pause_char(500)} слово"
clean = strip_sentinels(raw)
assert PAUSE_DELIM not in clean
assert explicit_pause_char(500) not in clean
assert " " not in clean