init
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.speech import SpeechRequest, router
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["loud", "nope", "x-slower", "1.5"])
|
||||
def test_rate_invalid_rejected_by_model(bad):
|
||||
with pytest.raises(ValidationError):
|
||||
SpeechRequest(voice="a", input="текст", rate=bad)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", ["loud", "very-high"])
|
||||
def test_pitch_invalid_rejected_by_model(bad):
|
||||
with pytest.raises(ValidationError):
|
||||
SpeechRequest(voice="a", input="текст", pitch=bad)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("good", ["x-slow", "slow", "medium", "fast", "x-fast"])
|
||||
def test_rate_valid_accepted(good):
|
||||
assert SpeechRequest(voice="a", input="текст", rate=good).rate == good
|
||||
|
||||
|
||||
@pytest.mark.parametrize("good", ["x-low", "low", "medium", "high", "x-high"])
|
||||
def test_pitch_valid_accepted(good):
|
||||
assert SpeechRequest(voice="a", input="текст", pitch=good).pitch == good
|
||||
|
||||
|
||||
def test_endpoint_returns_422_for_invalid_rate(client):
|
||||
r = client.post(
|
||||
"/v1/audio/speech",
|
||||
json={"model": "UniosTTS", "voice": "baya", "input": "привет", "rate": "loud"},
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_endpoint_returns_422_for_invalid_pitch(client):
|
||||
r = client.post(
|
||||
"/v1/audio/speech",
|
||||
json={"model": "UniosTTS", "voice": "baya", "input": "привет", "pitch": "loud"},
|
||||
)
|
||||
assert r.status_code == 422
|
||||
@@ -0,0 +1,132 @@
|
||||
import pytest
|
||||
|
||||
from app.normalize import mark_pause_delimiters, normalize_common
|
||||
from app.segmentation import prepare_segments
|
||||
from app.ssml import PAUSE_DELIM as P, build_ssml, strip_sentinels
|
||||
|
||||
DELIMS = "([{«“<)]}»>\"'`*_~"
|
||||
|
||||
|
||||
# 5.1/5.2: разделительные символы удаляются, в паузообразующих позициях
|
||||
# остаётся sentinel (НЕ запятая). Проверяем инварианты, а не литеральные запятые.
|
||||
@pytest.mark.parametrize("text, expected_pauses", [
|
||||
("скажи, что именно интересует (мир, техника, курсы валют), и я поищу.", 1),
|
||||
('он сказал "сейчас проверю" и ушёл', 2),
|
||||
('он сказал: "привет"', 0),
|
||||
("мы сходим (надо)", 1),
|
||||
('вот текст (важно)"', 1),
|
||||
("он сказал **важно** слово", 2),
|
||||
("слово *курсив* конец", 2),
|
||||
("это _важно_ для дела", 2),
|
||||
("список ~зачёркнут~ конец", 2),
|
||||
("код `inline` конец", 2),
|
||||
("тема «мир» конец", 2),
|
||||
("сравнение x < 5 > 3 конец", 2),
|
||||
("это {фигурные} и [квадратные] скобки", 4),
|
||||
("слово 'вот это' конец", 2),
|
||||
])
|
||||
def test_delimiters_removed_pause_is_sentinel(text, expected_pauses):
|
||||
result = mark_pause_delimiters(text)
|
||||
for ch in DELIMS:
|
||||
assert ch not in result, f"символ {ch!r} не удалён из {result!r}"
|
||||
assert result.count(P) == expected_pauses
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text, expected", [
|
||||
("мы сходим (надо)", f"мы сходим {P} надо"),
|
||||
('вот текст (важно)"', f"вот текст {P} важно"),
|
||||
("он сказал **важно** слово", f"он сказал {P} важно {P} слово"),
|
||||
('он сказал: "привет"', "он сказал: привет"),
|
||||
("погода в твоём городе?), и я поищу.", "погода в твоём городе?, и я поищу."),
|
||||
])
|
||||
def test_delimiter_pause_positions(text, expected):
|
||||
assert mark_pause_delimiters(text) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", [
|
||||
"д'Катт и O'Neill пришли",
|
||||
])
|
||||
def test_apostrophes_inside_words_untouched(text):
|
||||
assert mark_pause_delimiters(text) == text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", [
|
||||
"текст (важно).",
|
||||
"текст (важно), конец",
|
||||
"текст [важно]!",
|
||||
"(важно)",
|
||||
])
|
||||
def test_no_delimiter_chars_left(text):
|
||||
result = mark_pause_delimiters(text)
|
||||
for ch in DELIMS:
|
||||
assert ch not in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", [
|
||||
"текст (важно).",
|
||||
"текст (важно), конец",
|
||||
"текст (важно)! и ещё (так) конец",
|
||||
])
|
||||
def test_no_dangling_artifacts(text):
|
||||
# Sentinel не добавляет запятых; после удаления артефактов не остаётся.
|
||||
result = mark_pause_delimiters(text)
|
||||
assert ",," not in result
|
||||
assert not result.lstrip().startswith(",")
|
||||
assert ",." not in result
|
||||
|
||||
|
||||
def test_line_boundaries():
|
||||
text = "(в начале строки) текст\nтекст (в конце строки)\n(снова) конец"
|
||||
result = mark_pause_delimiters(text)
|
||||
for ch in "()":
|
||||
assert ch not in result
|
||||
assert P in result
|
||||
clean = strip_sentinels(result)
|
||||
assert "снова" in clean and "конец" in clean and "текст" in clean
|
||||
|
||||
|
||||
def test_multiline_delimiters_processed_in_every_line():
|
||||
text = "первая (строка) тут\nвторая «строка» тут"
|
||||
result = mark_pause_delimiters(text)
|
||||
assert "(" not in result and "«" not in result and "»" not in result
|
||||
clean = strip_sentinels(result)
|
||||
assert "строка" in clean and "тут" in clean
|
||||
assert P in result
|
||||
|
||||
|
||||
def test_english_text_same_rule():
|
||||
segments = prepare_segments("The (parenthetical) works")
|
||||
ssml = build_ssml(segments)
|
||||
assert "<break" in ssml
|
||||
assert "parenthetical" in ssml
|
||||
assert "(" not in ssml and ")" not in ssml
|
||||
|
||||
|
||||
def test_bug_example_pauses_before_and_after_parentheses():
|
||||
text = ("скажи, что именно интересует (мир, техника, курсы валют, "
|
||||
"погода в твоём городе?), и я поищу.")
|
||||
segments = prepare_segments(text)
|
||||
stripped = strip_sentinels(" ".join(s.text for s in segments))
|
||||
assert "(" not in stripped and ")" not in stripped
|
||||
assert ",," not in stripped
|
||||
assert "интересует" in stripped and "город" in stripped
|
||||
# Единственная паузная граница — от открывающей скобки перед «мир»;
|
||||
# закрывающая скобка перед запятой границы не создаёт.
|
||||
ssml = build_ssml(segments)
|
||||
assert ssml.count("<break") == 1
|
||||
assert P not in ssml
|
||||
|
||||
|
||||
def test_pause_delim_becomes_break_in_ssml():
|
||||
segments = prepare_segments("он сказал **важно** слово")
|
||||
ssml = build_ssml(segments)
|
||||
assert ssml.count("<break") == 2
|
||||
assert P not in ssml
|
||||
|
||||
|
||||
def test_underscore_not_verbalized_before_regional_normalization():
|
||||
segments = prepare_segments("это _важно_ для дела")
|
||||
joined = strip_sentinels(" ".join(s.text for s in segments))
|
||||
assert "_" not in joined
|
||||
assert "подчеркивание" not in joined.lower()
|
||||
assert "подчёркивание" not in joined.lower()
|
||||
@@ -0,0 +1,103 @@
|
||||
from app.segmentation import (
|
||||
detect_language,
|
||||
extract_explicit_pauses,
|
||||
prepare_segments,
|
||||
split_paragraphs,
|
||||
)
|
||||
from app.ssml import DEFAULT_PAUSE_MS, explicit_pause_char
|
||||
|
||||
|
||||
def test_detect_language_pure_ru():
|
||||
assert detect_language("Привет мир") == "ru"
|
||||
|
||||
|
||||
def test_detect_language_pure_en():
|
||||
assert detect_language("Hello world") == "en"
|
||||
|
||||
|
||||
def test_detect_language_single_cyrillic_letter():
|
||||
assert detect_language("tshirt 1") == "en"
|
||||
assert detect_language("tshirt 1 е") == "ru"
|
||||
|
||||
|
||||
def test_detect_language_latin_with_digits():
|
||||
assert detect_language("iPhone 15 Pro 24/7") == "en"
|
||||
|
||||
|
||||
def test_ru_request_segments_uniform_and_latin_free():
|
||||
segments = prepare_segments("Закажи на Ozon через Python-скрипт.")
|
||||
assert segments
|
||||
assert all(s.lang == "ru" for s in segments)
|
||||
assert not any(any(ch.isascii() and ch.isalpha() for ch in s.text) for s in segments)
|
||||
joined = " ".join(s.text for s in segments)
|
||||
assert "Озон" in joined or "озон" in joined
|
||||
assert "Пайтон" in joined or "пайтон" in joined
|
||||
|
||||
|
||||
def test_en_request_not_transliterated():
|
||||
segments = prepare_segments("Hello world, this is a test.")
|
||||
assert segments
|
||||
assert all(s.lang == "en" for s in segments)
|
||||
assert any("Hello" in s.text for s in segments)
|
||||
|
||||
|
||||
def test_ru_numbers_normalized():
|
||||
segments = prepare_segments("Купи 5 штук в Shopify.")
|
||||
joined = " ".join(s.text for s in segments)
|
||||
assert "пять" in joined.lower()
|
||||
|
||||
|
||||
# --- 5.3: абзацы и явные паузы ---
|
||||
|
||||
def test_split_paragraphs_on_blank_lines():
|
||||
paras = split_paragraphs("Первый абзац.\n\nВторой абзац.\n \nТретий.")
|
||||
assert paras == ["Первый абзац.", "Второй абзац.", "Третий."]
|
||||
|
||||
|
||||
def test_paragraphs_get_distinct_para_index():
|
||||
segments = prepare_segments("Первое предложение.\n\nВторой абзац тут.")
|
||||
assert segments
|
||||
paras = {s.para for s in segments}
|
||||
assert len(paras) >= 2
|
||||
assert min(paras) == 0
|
||||
|
||||
|
||||
def test_sentences_within_paragraph_share_para():
|
||||
segments = prepare_segments("Раз. Два. Три.")
|
||||
assert len(segments) >= 2
|
||||
assert all(s.para == 0 for s in segments)
|
||||
|
||||
|
||||
def test_delimiter_removal_does_not_break_sentence_split():
|
||||
# Удаление разделителей (без подмены запятыми) не ломает разбиение на предложения.
|
||||
segments = prepare_segments("Он сказал (тихо) и ушёл. Потом сразу спал.")
|
||||
assert len(segments) >= 2
|
||||
joined = " ".join(s.text for s in segments)
|
||||
for ch in "()":
|
||||
assert ch not in joined
|
||||
|
||||
|
||||
def test_explicit_pause_default_marker_to_single_char():
|
||||
out = extract_explicit_pauses("стоп ||| продолжение")
|
||||
assert "|||" not in out
|
||||
assert explicit_pause_char(DEFAULT_PAUSE_MS) in out
|
||||
|
||||
|
||||
def test_explicit_pause_timed_marker_encodes_ms():
|
||||
out = extract_explicit_pauses("а |||1500 б")
|
||||
assert "1500" not in out
|
||||
assert explicit_pause_char(1500) in out
|
||||
|
||||
|
||||
def test_explicit_pause_clamped_to_max():
|
||||
out = extract_explicit_pauses("а |||999999 б")
|
||||
assert "999999" not in out
|
||||
assert explicit_pause_char(5000) in out
|
||||
|
||||
|
||||
def test_explicit_pause_survives_prepare_segments():
|
||||
segments = prepare_segments("первое |||2000 второе")
|
||||
joined = "".join(s.text for s in segments)
|
||||
assert "|||" not in joined
|
||||
assert "2000" not in joined
|
||||
assert explicit_pause_char(2000) in joined
|
||||
@@ -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 "<break" not in out
|
||||
|
||||
|
||||
# --- экранирование текста ---
|
||||
|
||||
def test_text_is_xml_escaped():
|
||||
out = build_ssml([seg("a & b <c> d")])
|
||||
assert "&" in out and "<" in out and ">" 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
|
||||
@@ -0,0 +1,155 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from app.segmentation import Segment
|
||||
from app.synthesis import SynthesisEngine, ModelEntry
|
||||
|
||||
|
||||
class FakeModel:
|
||||
def __init__(self, name, calls):
|
||||
self.name = name
|
||||
self.calls = calls
|
||||
self.speakers = ["spk1", "spk2"]
|
||||
|
||||
def apply_tts(self, *, text=None, ssml_text=None, speaker, sample_rate,
|
||||
put_accent, put_yo):
|
||||
self.calls.append({
|
||||
"name": self.name, "speaker": speaker, "text": text,
|
||||
"ssml_text": ssml_text, "put_accent": put_accent,
|
||||
})
|
||||
return torch.zeros(10)
|
||||
|
||||
|
||||
class FakeTts:
|
||||
def __init__(self, name, calls, put_accent=True):
|
||||
self.sample_rate = 24000
|
||||
self.put_accent = put_accent
|
||||
self.put_yo = True
|
||||
self.tts_model = FakeModel(name, calls)
|
||||
|
||||
|
||||
def make_engine(use_ssml=True, put_accent=True):
|
||||
calls: list[dict] = []
|
||||
engine = SynthesisEngine(
|
||||
models=[
|
||||
ModelEntry(model_id="ru_model", tts=FakeTts("ru", calls, put_accent), languages=["ru"]),
|
||||
ModelEntry(model_id="en_model", tts=FakeTts("en", calls, put_accent), languages=["en"]),
|
||||
],
|
||||
sample_rate=24000,
|
||||
use_ssml=use_ssml,
|
||||
)
|
||||
return engine, calls
|
||||
|
||||
|
||||
def test_single_speaker_for_all_segments():
|
||||
engine, calls = make_engine(use_ssml=False)
|
||||
segments = [Segment(text="Озон", lang="ru"), Segment(text="Пайтон", lang="ru")]
|
||||
pcm = engine.synthesize_segments(segments, "baya", "ru")
|
||||
assert pcm
|
||||
assert len(calls) == 2
|
||||
assert all(c["name"] == "ru" and c["speaker"] == "baya" for c in calls)
|
||||
|
||||
|
||||
def test_en_lang_selects_en_model():
|
||||
engine, calls = make_engine(use_ssml=False)
|
||||
segments = [Segment(text="hello", lang="en")]
|
||||
engine.synthesize_segments(segments, "en_0", "en")
|
||||
assert calls[0]["name"] == "en"
|
||||
assert calls[0]["text"] == "hello"
|
||||
|
||||
|
||||
def test_legacy_path_uses_text_not_ssml():
|
||||
engine, calls = make_engine(use_ssml=False)
|
||||
engine.synthesize_segments([Segment(text="привет", lang="ru")], "baya", "ru")
|
||||
assert calls[0]["text"] == "привет"
|
||||
assert calls[0]["ssml_text"] is None
|
||||
|
||||
|
||||
def test_no_silence_padding_between_segments():
|
||||
engine, calls = make_engine(use_ssml=False)
|
||||
segments = [Segment(text="а", lang="ru"), Segment(text="б", lang="ru")]
|
||||
pcm = engine.synthesize_segments(segments, "baya", "ru")
|
||||
expected = (np.zeros(10) * 32767).astype(np.int16)
|
||||
assert len(pcm) == 2 * len(expected.tobytes())
|
||||
|
||||
|
||||
# --- ssml_text path (task 3) ---
|
||||
|
||||
def test_ssml_path_passes_ssml_text():
|
||||
engine, calls = make_engine()
|
||||
segments = [Segment(text="привет", lang="ru"), Segment(text="мир", lang="ru")]
|
||||
pcm = engine.synthesize_segments(segments, "baya", "ru")
|
||||
assert pcm
|
||||
assert len(calls) == 1 # один чанк на абзац
|
||||
c = calls[0]
|
||||
assert c["text"] is None
|
||||
assert c["ssml_text"].startswith("<speak>")
|
||||
assert "<s>привет</s>" in c["ssml_text"]
|
||||
assert "<s>мир</s>" in c["ssml_text"]
|
||||
|
||||
|
||||
def test_ssml_path_single_call_per_chunk():
|
||||
engine, calls = make_engine()
|
||||
# Два разных абзаца -> два <p> -> (в пределах лимита) один документ/один вызов.
|
||||
segments = [
|
||||
Segment(text="первый", lang="ru", para=0),
|
||||
Segment(text="второй", lang="ru", para=1),
|
||||
]
|
||||
engine.synthesize_segments(segments, "baya", "ru")
|
||||
assert len(calls) == 1
|
||||
assert "<p>" in calls[0]["ssml_text"]
|
||||
|
||||
|
||||
def test_ssml_prosody_rate_and_pitch_passed_to_builder():
|
||||
engine, calls = make_engine()
|
||||
engine.synthesize_segments(
|
||||
[Segment(text="привет", lang="ru")], "baya", "ru",
|
||||
rate="slow", pitch="high",
|
||||
)
|
||||
ssml = calls[0]["ssml_text"]
|
||||
assert '<prosody' in ssml
|
||||
assert 'rate="slow"' in ssml
|
||||
assert 'pitch="high"' in ssml
|
||||
|
||||
|
||||
def test_ssml_medium_prosody_omitted():
|
||||
engine, calls = make_engine()
|
||||
engine.synthesize_segments(
|
||||
[Segment(text="привет", lang="ru")], "baya", "ru",
|
||||
rate="medium", pitch="medium",
|
||||
)
|
||||
assert "<prosody" not in calls[0]["ssml_text"]
|
||||
|
||||
|
||||
def test_ssml_use_ssml_false_overrides_for_call():
|
||||
engine, calls = make_engine(use_ssml=True)
|
||||
engine.synthesize_segments(
|
||||
[Segment(text="привет", lang="ru")], "baya", "ru", use_ssml=False
|
||||
)
|
||||
assert calls[0]["ssml_text"] is None
|
||||
assert calls[0]["text"] == "привет"
|
||||
|
||||
|
||||
def test_put_accent_true_on_ssml_path():
|
||||
# put_accent=True даже если модель по умолчанию False (ручные + работают).
|
||||
engine, calls = make_engine(put_accent=False)
|
||||
engine.synthesize_segments([Segment(text="привет", lang="ru")], "baya", "ru")
|
||||
assert calls[0]["put_accent"] is True
|
||||
|
||||
|
||||
def test_manual_stress_marker_reaches_model_via_ssml():
|
||||
engine, calls = make_engine()
|
||||
engine.synthesize_segments(
|
||||
[Segment(text="втор+ой сл+ово", lang="ru")], "baya", "ru"
|
||||
)
|
||||
assert "втор+ой" in calls[0]["ssml_text"]
|
||||
assert "сл+ово" in calls[0]["ssml_text"]
|
||||
|
||||
|
||||
def test_manual_stress_non_vowel_passes_without_error():
|
||||
engine, calls = make_engine()
|
||||
pcm = engine.synthesize_segments(
|
||||
[Segment(text="т+ест", lang="ru")], "baya", "ru"
|
||||
)
|
||||
assert pcm
|
||||
assert "т+ест" in calls[0]["ssml_text"]
|
||||
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
|
||||
from app.translit import transliterate_latin, transliterate_token
|
||||
|
||||
|
||||
def test_exact_dict_word():
|
||||
assert transliterate_token("Ozon") == "Озон"
|
||||
assert transliterate_token("ozon") == "озон"
|
||||
|
||||
|
||||
def test_rule_based_word():
|
||||
assert transliterate_token("shipping") == "шиппинг"
|
||||
|
||||
|
||||
def test_abbreviation_uppercase():
|
||||
assert transliterate_token("IT") == "айти"
|
||||
assert transliterate_token("DVD") == "ди-вэ-ди"
|
||||
|
||||
|
||||
def test_unknown_abbreviation_falls_back_to_rules():
|
||||
result = transliterate_token("KPI")
|
||||
assert "айти" != result
|
||||
assert result == "КПИ"
|
||||
|
||||
|
||||
def test_mixed_tokens_preserve_non_letters():
|
||||
assert transliterate_latin("Python-скрипт") == "Пайтон-скрипт"
|
||||
assert transliterate_latin("4G") == "4Г"
|
||||
|
||||
|
||||
def test_noop_without_latin():
|
||||
text = "Привет, мир! Числа 123 и знак #."
|
||||
assert transliterate_latin(text) == text
|
||||
|
||||
|
||||
def test_uppercase_rule_output_keeps_case():
|
||||
assert transliterate_token("NETFLIX") == "НЕТФЛИКС"
|
||||
Reference in New Issue
Block a user