Результаты, обработанные до 0.14.0, не содержат массива sentences. Пустой блок разметки выглядел бы поломкой страницы - теперь он говорит, что нужно отправить запись заново. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
83 lines
3.7 KiB
Python
83 lines
3.7 KiB
Python
"""Страница разбора: по ней смотрят, чья ошибка - распознавания или разметки."""
|
||
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 "Вторая <реплика>." 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())
|