"""Страница для разбора результата в браузере. Когда диалог размечен неверно, по одному 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, по одной на предложение. Диалог соберётся ниже, и станет видно, где ошибка: в тексте от сервиса или в расстановке ролей.
"""