"""Страница для разбора результата в браузере.
Когда диалог размечен неверно, по одному JSON не понять, чья это ошибка:
распознавание склеило реплики или LLM неверно расставила роли. Страница
показывает обе стороны - что отдал сервис и что получилось после разметки,
- и позволяет вставить ответ модели, чтобы увидеть результат сразу.
"""
import html
import json
__all__ = ["render_job"]
GAP_NOTICE_SEC = 3.0
def timecode(seconds: float) -> str:
return f"{int(seconds) // 60:02d}:{int(seconds) % 60:02d}"
def _stats_row(job: dict) -> str:
result = job.get("result") or job
stats = result.get("stats") or {}
timing = result.get("timing") or {}
duration = result.get("duration_sec", 0)
speech = stats.get("speech_sec", 0)
items = [
("Длительность", timecode(duration)),
("Реплик", str(stats.get("turns_count", len(result.get("turns", []))))),
("Предложений", str(len(result.get("sentences", [])))),
("Речи распознано", f"{speech:.0f} с ({speech / duration * 100:.0f} %)"
if duration else "-"),
("Режим", "диаризация" if stats.get("diarized") else "детектор речи"),
("Обработка", f"{timing.get('diarization_sec', 0):.0f} + "
f"{timing.get('asr_sec', 0):.0f} с"),
]
return "".join(
f'
{html.escape(k)}'
f'{html.escape(v)}
' for k, v in items)
def render_job(job: dict) -> str:
"""Собирает самодостаточную страницу: без внешних файлов и запросов."""
result = job.get("result") or job
turns = result.get("turns") or []
sentences = result.get("sentences") or []
rows = []
previous_end = 0.0
for turn in turns:
gap = turn["start"] - previous_end
if gap >= GAP_NOTICE_SEC:
rows.append(f'пропуск {gap:.0f} с '
f'[{timecode(previous_end)} - {timecode(turn["start"])}]
')
previous_end = turn["end"]
loudness = (turn.get("acoustics") or {}).get("loudness_db")
volume = f"{loudness:.0f} дБ" if loudness is not None else ""
mark = 'второй проход' if turn.get("recovered") else ""
speaker = turn.get("speaker", 0)
who = f"Г{speaker}" if speaker else "Г0"
rows.append(
f''
f'{timecode(turn["start"])}'
f'{who}'
f'{volume}'
f'{html.escape(turn["text"])}{mark}'
f'
')
# Внутри в тексте
# закрыла бы тег и всё, что дальше, браузер прочитал бы как разметку.
payload = json.dumps(
[{"n": s["n"], "text": s["text"], "start": s["start"]} for s in sentences],
ensure_ascii=False).replace("<", "\\u003c").replace("\u2028", "\\u2028")
return f"""
Расшифровка {html.escape(str(job.get("job_id", "")))[:8]}
Расшифровка {html.escape(str(job.get("job_id", ""))[:8])} - {html.escape(str(job.get("filename", "")))}
{_stats_row(job)}
Что отдал сервис
Реплики так, как их нарезал сервис. Г0 значит, что разделение
по голосам выключено и говорящий неизвестен - это норма.
{"".join(rows) or "
реплик нет
"}
Разметка ролей
Вставьте ответ модели - строку из букв M и C, по одной
на предложение. Диалог соберётся ниже, и станет видно, где ошибка:
в тексте от сервиса или в расстановке ролей.
"""