Плеер на странице разбора и хранение записей
Записи складываются в папку records и удаляются вместе с задачей по тому же сроку. На странице разбора появился плеер, а щелчок по времени слева от реплики перематывает запись - можно послушать спорное место, не таская ползунок. Настройка keep_records в config.toml. Если запись не сохранена, вместо пустого плеера показывается объяснение. Черновик с подсветкой расхождений пока остаётся отдельной командой: build/make_draft.py. Ложные расхождения по написанию больше не подсвечиваются. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7b4f4721d0
commit
96c2a27c4c
+33
-2
@@ -111,7 +111,7 @@ def render_index(jobs: list[dict], token: str) -> str:
|
||||
</div></body></html>"""
|
||||
|
||||
|
||||
def render_job(job: dict) -> str:
|
||||
def render_job(job: dict, token: str = "") -> str:
|
||||
"""Собирает самодостаточную страницу: без внешних файлов и запросов."""
|
||||
result = job.get("result") or job
|
||||
turns = result.get("turns") or []
|
||||
@@ -132,7 +132,8 @@ def render_job(job: dict) -> str:
|
||||
who = f"Г{speaker}" if speaker else "Г0"
|
||||
rows.append(
|
||||
f'<div class="turn sp{speaker % 3}">'
|
||||
f'<span class="tc">{timecode(turn["start"])}</span>'
|
||||
f'<span class="tc" data-at="{turn["start"]:.1f}">'
|
||||
f'{timecode(turn["start"])}</span>'
|
||||
f'<span class="who">{who}</span>'
|
||||
f'<span class="db">{volume}</span>'
|
||||
f'<span class="text">{html.escape(turn["text"])}{mark}</span>'
|
||||
@@ -154,6 +155,18 @@ def render_job(job: dict) -> str:
|
||||
'запись обработана версией сервиса старше 0.14.0. Отправьте её заново, '
|
||||
'чтобы собрать диалог по ролям.</div>')
|
||||
|
||||
# Плеер появляется, только если запись сохранена: иначе он показывал бы
|
||||
# пустую полосу и выглядел бы поломкой.
|
||||
if job.get("has_record"):
|
||||
suffix = f"?token={quote(token)}" if token else ""
|
||||
player = (f'<audio id="player" controls preload="none" '
|
||||
f'src="/v1/jobs/{html.escape(str(job.get("job_id", "")))}/audio{suffix}">'
|
||||
'</audio>'
|
||||
'<div class="hint">Щелчок по времени слева от реплики перематывает запись.</div>')
|
||||
else:
|
||||
player = ('<div class="hint">Запись не сохранена: включите keep_records '
|
||||
'в config.toml, чтобы слушать разговор прямо здесь.</div>')
|
||||
|
||||
payload = json.dumps(
|
||||
[{"n": s["n"], "text": s["text"], "start": s["start"]} for s in sentences],
|
||||
ensure_ascii=False).replace("<", "\\u003c").replace("\u2028", "\\u2028")
|
||||
@@ -167,6 +180,9 @@ def render_job(job: dict) -> str:
|
||||
.turn {{ display:grid; grid-template-columns:52px 34px 58px 1fr; gap:8px;
|
||||
padding:5px 0; border-bottom:1px solid var(--line); }}
|
||||
.tc, .db {{ color:var(--dim); font-size:12px; font-variant-numeric:tabular-nums; }}
|
||||
.tc[data-at] {{ cursor:pointer; text-decoration:underline dotted; }}
|
||||
.tc[data-at]:hover {{ color:var(--fg); }}
|
||||
audio {{ width:100%; margin:4px 0 2px; }}
|
||||
.who {{ font-weight:600; font-size:13px; }}
|
||||
.sp1 .who {{ color:var(--a); }} .sp2 .who {{ color:var(--b); }}
|
||||
.gap {{ color:var(--warn); font-size:12px; padding:5px 0 5px 94px; }}
|
||||
@@ -179,6 +195,10 @@ def render_job(job: dict) -> str:
|
||||
.roles .line {{ display:grid; grid-template-columns:76px 1fr; gap:10px;
|
||||
padding:4px 0; border-bottom:1px solid var(--line); }}
|
||||
.who2 {{ font-weight:600; font-size:13px; }}
|
||||
a.button {{ display:inline-block; font-size:13px; color:var(--fg);
|
||||
border:1px solid var(--line); border-radius:4px; padding:5px 12px;
|
||||
text-decoration:none; }}
|
||||
a.button:hover {{ border-color:var(--dim); }}
|
||||
button.copy {{ font:13px inherit; color:var(--fg); background:transparent;
|
||||
border:1px solid var(--line); border-radius:4px; padding:3px 10px;
|
||||
margin-left:10px; cursor:pointer; font-weight:400; }}
|
||||
@@ -187,6 +207,7 @@ def render_job(job: dict) -> str:
|
||||
<h1>Расшифровка {html.escape(str(job.get("job_id", ""))[:8])} - {html.escape(str(job.get("filename", "")))}</h1>
|
||||
<div class="stats">{_stats_row(job)}</div>
|
||||
|
||||
{player}
|
||||
<h2>Что отдал сервис <button class="copy" data-src="plain">скопировать для Word</button></h2>
|
||||
<div class="hint">Реплики так, как их нарезал сервис. Г0 значит, что разделение
|
||||
по голосам выключено и говорящий неизвестен - это норма.</div>
|
||||
@@ -263,4 +284,14 @@ async function copyAs(src, button) {{
|
||||
}}
|
||||
document.querySelectorAll('button.copy').forEach(b =>
|
||||
b.addEventListener('click', () => copyAs(b.dataset.src, b)));
|
||||
|
||||
// Перемотка по щелчку на времени: слушать нужное место, не таская ползунок.
|
||||
const player = document.getElementById('player');
|
||||
if (player) {{
|
||||
document.querySelectorAll('.tc[data-at]').forEach(el =>
|
||||
el.addEventListener('click', () => {{
|
||||
player.currentTime = parseFloat(el.dataset.at);
|
||||
player.play();
|
||||
}}));
|
||||
}}
|
||||
</script></body></html>"""
|
||||
|
||||
Reference in New Issue
Block a user