Files
talkscore-asr/tests/test_view.py
T
Vladimir BryzgalovandClaude Opus 5 f3640f2941 Список задач в браузере и сравнение с эталоном
GET /v1/jobs/view - список последних задач со ссылками на разбор. Токен
подставляется в ссылки, иначе по каждой пришлось бы дописывать его руками.
Отдельный тест следит, чтобы маршрут не перехватывался /v1/jobs/{job_id}.

build/compare_reference.py считает WER относительно эталона, выписанного
на слух. Косвенные метрики ловят крупные сдвиги и слепы к систематическим
мелким потерям - так и пропустили обрыв концов фраз.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:39:21 +05:00

129 lines
5.2 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 json
import re
from app.view import render_job
def job(**over):
base = {
"job_id": "abc12345", "filename": "call.mp3", "duration_sec": 120.0,
"turns": [
{"speaker": 0, "start": 0.0, "end": 4.0, "text": "Первая реплика.",
"acoustics": {"loudness_db": -18.0}},
{"speaker": 0, "start": 30.0, "end": 34.0, "text": "Вторая <реплика>.",
"acoustics": {}, "recovered": True},
],
"sentences": [
{"n": 1, "text": "Первая реплика.", "start": 0.0, "loudness_db": -18.0, "turn": 0},
{"n": 2, "text": "Вторая <реплика>.", "start": 30.0, "loudness_db": None, "turn": 1},
],
"stats": {"turns_count": 2, "speech_sec": 8.0, "diarized": False},
"timing": {"diarization_sec": 1.0, "asr_sec": 2.0},
}
base.update(over)
return base
class TestPage:
def test_is_self_contained(self):
"""Страница открывается локально: внешних файлов и запросов быть не должно."""
page = render_job(job())
assert "<script" in page and "<style" in page
assert "http://" not in page and "https://" not in page
def test_shows_text_of_turns(self):
page = render_job(job())
assert "Первая реплика." in page
def test_escapes_text(self):
"""Расшифровка - это данные, а не разметка."""
page = render_job(job())
assert "Вторая &lt;реплика&gt;." in page
assert "<реплика>" not in page
def test_marks_gap_between_turns(self):
page = render_job(job())
assert "пропуск" in page and "00:04" in page
def test_marks_recovered_turn(self):
assert "второй проход" in render_job(job())
def test_shows_mode(self):
assert "детектор речи" in render_job(job())
assert "диаризация" in render_job(job(stats={"diarized": True, "turns_count": 2}))
def test_sentences_go_into_script(self):
"""По ним страница собирает диалог из вставленной строки ролей."""
page = render_job(job())
payload = re.search(r"const S = (\[.*?\]);", page, re.S).group(1)
assert len(json.loads(payload)) == 2
def test_closing_tag_in_text_cannot_break_out(self):
"""Строка </script> в расшифровке закрыла бы тег и сломала страницу."""
evil = 'а тут </script><img src=x onerror=alert(1)>'
page = render_job(job(
turns=[{"speaker": 0, "start": 0.0, "end": 1.0, "text": evil, "acoustics": {}}],
sentences=[{"n": 1, "text": evil, "start": 0.0, "loudness_db": None, "turn": 0}]))
assert "</script><img" not in page
assert page.count("</script>") == 1
def test_survives_empty_result(self):
page = render_job({"job_id": "x", "turns": [], "sentences": [], "duration_sec": 0})
assert "реплик нет" in page
def test_old_result_explains_missing_sentences(self):
"""Результаты до 0.14.0 не содержат предложений - пустой блок выглядел бы поломкой."""
page = render_job({"job_id": "x", "turns": [], "sentences": [], "duration_sec": 10})
assert "старше 0.14.0" in page
assert "<textarea" not in page
def test_page_with_sentences_has_input(self):
assert "<textarea" in render_job(job())
class TestIndex:
@staticmethod
def jobs():
return [
{"id": "a" * 32, "filename": "call.mp3", "status": "done",
"created_at": 1755300000, "duration_sec": 903.0, "error": None},
{"id": "b" * 32, "filename": "<bad>.mp3", "status": "failed",
"created_at": 1755300500, "duration_sec": 0.0, "error": "нет модели"},
]
def test_done_job_is_a_link(self):
from app.view import render_index
page = render_index(self.jobs(), "tok")
assert f'href="/v1/jobs/{"a" * 32}/view?token=tok"' in page
def test_failed_job_is_not_a_link(self):
"""Разбирать нечего: результата нет."""
from app.view import render_index
page = render_index(self.jobs(), "tok")
assert f'/v1/jobs/{"b" * 32}/view' not in page
def test_error_is_shown(self):
from app.view import render_index
assert "нет модели" in render_index(self.jobs(), "tok")
def test_filename_is_escaped(self):
from app.view import render_index
page = render_index(self.jobs(), "tok")
assert "&lt;bad&gt;.mp3" in page and "<bad>" not in page
def test_token_is_encoded_in_links(self):
from app.view import render_index
page = render_index(self.jobs(), "token s probelom")
assert "token%20s%20probelom" in page
def test_empty_list_says_so(self):
from app.view import render_index
assert "задач пока нет" in render_index([], "tok")