lct-hack/backend/tests/test_voice_text.py
Ivan Gerasimov 456f3cd34d lct-06: голосовой контур VAD → STT → звонящий → TTS, с перебиванием
Контур замкнут без LLM: звонящий отвечает заготовками через слот-автомат.
Живой диалог через WebSocket прошёл 5 раз подряд без сбоев: первая
реплика после снятия трубки — 87–154 мс, ход — 1.2–1.4 с, перебивание —
88–90 мс. На LLM до цели 1.5 с остаётся 100–300 мс.

Задача ответа живёт, пока у курсанта доигрывает звук: перебивание —
это отмена одной задачи, и tts.end приходит, когда звонящий
действительно замолчал.

Что нашлось при сборке:
- Silero молча выбрасывает цифры: «улица Ленина, дом… квартира…» без
  номеров. Числа и сокращения разворачиваются в слова до синтеза.
- onnxruntime брал все 16 ядер и вытеснял синтез, первый вызов был
  холодным: распознавание 576 мс вместо ~210. Прогрев на старте и по
  4 потока на модель.
- Весь адрес одним предложением — ~455 мс синтеза до первого звука.
  Длинное предложение режется по запятым, номер от улицы не отрывается:
  ~250 мс.
- Первая реплика «Алло! Помогите!» запускалась мимо задачи ответа и не
  перебивалась вовсе.

Отдельно: сломал make dev — num2words попал в pyproject, а Dockerfile
ставил зависимости ручным списком, и бэкенд в контейнере упал на
импорте. Образ теперь ставит зависимости из pyproject.toml.

Голоса в контейнере нет: под WSL модели работают нативно, make back.
2026-09-17 15:04:17 +03:00

53 lines
2.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Нормализация текста перед синтезом: адрес должен прозвучать целиком."""
import re
from app.voice.text import normalize, sentences, speech_chunks
def test_address_digits_become_words():
spoken = normalize("улица Ленина, 14, квартира 47, 5-й этаж")
assert not re.search(r"\d", spoken), spoken
assert "четырнадцать" in spoken and "сорок семь" in spoken and "пятый" in spoken
def test_abbreviations_are_expanded():
assert normalize("ул. Ленина, д. 14, кв. 47") == "улица Ленина, дом четырнадцать, квартира сорок семь"
def test_ordinal_gender_follows_suffix():
assert normalize("5-я линия") == "пятая линия"
assert normalize("3-е окно") == "третье окно"
assert normalize("2-й подъезд") == "второй подъезд"
def test_building_letter_is_spoken():
spoken = normalize("дом 47B")
assert "сорок семь" in spoken and "бэ" in spoken and "B" not in spoken
def test_nothing_to_say_is_empty_not_an_error():
"""Пустая строка роняет Silero ValueError — нормализация отдаёт пустое,
и синтез просто не вызывается."""
assert normalize(" ") == ""
def test_reply_splits_by_sentence():
assert sentences("Алло! Горим! Улица Ленина, дом четырнадцать.") == [
"Алло!", "Горим!", "Улица Ленина, дом четырнадцать."
]
def test_long_address_is_chunked_for_a_fast_first_sound():
chunks = speech_chunks("Улица Ленина, 14, квартира 47, 5-й этаж! Быстрее!")
assert chunks[0] == "Улица Ленина, 14,", chunks
assert "".join(chunks).replace(" ", "") == "УлицаЛенина,14,квартира47,5-йэтаж!Быстрее!"
def test_short_sentences_are_not_chopped():
assert speech_chunks("Алло! Горим!") == ["Алло!", "Горим!"]
def test_house_number_stays_with_the_street():
chunks = speech_chunks("Я же сказал — улица Ленина, 14, квартира 47, 5-й этаж! Записывайте!")
assert chunks[0].endswith("Ленина, 14,"), chunks