Копирование в Word, длительность в списке, хранение две недели

Длительность в списке всегда была нулевой: при загрузке файл ещё не
декодирован, и mark_done её не записывал.

Кнопка копирования кладёт в буфер и разметку, и простой текст: Word берёт
первое и сохраняет полужирные имена говорящих, простые редакторы - второе.

Срок хранения поднят с 3 суток до 336 часов, после чистки выполняется
VACUUM: SQLite помечает страницы свободными, но файл не уменьшает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vladimir Bryzgalov
2026-08-17 19:20:23 +05:00
co-authored by Claude Opus 5
parent 2382c7a700
commit 7dd49a11ec
5 changed files with 123 additions and 6 deletions
+45 -1
View File
@@ -174,11 +174,15 @@ 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; }}
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; }}
button.copy:hover {{ border-color:var(--dim); }}
</style></head><body><div class="wrap">
<h1>Расшифровка {html.escape(str(job.get("job_id", ""))[:8])} - {html.escape(str(job.get("filename", "")))}</h1>
<div class="stats">{_stats_row(job)}</div>
<h2>Что отдал сервис</h2>
<h2>Что отдал сервис <button class="copy" data-src="plain">скопировать для Word</button></h2>
<div class="hint">Реплики так, как их нарезал сервис. Г0 значит, что разделение
по голосам выключено и говорящий неизвестен - это норма.</div>
{"".join(rows) or "<div class=hint>реплик нет</div>"}
@@ -187,6 +191,7 @@ def render_job(job: dict) -> str:
{roles_block}
<div id="check" class="hint"></div>
<div id="out" class="roles"></div>
<button class="copy" id="copyroles" data-src="roles" hidden>скопировать диалог для Word</button>
</div>
<script>
const S = {payload};
@@ -212,6 +217,45 @@ function draw() {{
if (r !== prev) {{ flush(); prev = r; start = s.start; }}
buf.push(s.text.replace(/[&<>]/g, c => ({{'&':'&amp;','<':'&lt;','>':'&gt;'}})[c])); }});
flush(); out.innerHTML = html;
const rolesButton = document.getElementById('copyroles');
if (rolesButton) rolesButton.hidden = !html;
}}
if (box) box.addEventListener('input', draw);
// Word вставляет из буфера разметку, если она есть: имена говорящих
// останутся полужирными, а абзацы - абзацами. Простой текст кладём рядом,
// для редакторов, которые разметку не берут.
function collect(src) {{
if (src === 'roles') {{
const lines = [...out.querySelectorAll('.line')].map(el => [
el.querySelector('.who2').textContent.trim(),
el.lastElementChild.textContent.trim()]);
return lines;
}}
return [...document.querySelectorAll('.turn')].map(el => [
el.querySelector('.tc').textContent.trim() + ' ' +
el.querySelector('.who').textContent.trim(),
el.querySelector('.text').textContent.trim()]);
}}
function esc(t) {{ return t.replace(/[&<>]/g, c => ({{'&':'&amp;','<':'&lt;','>':'&gt;'}}[c])); }}
async function copyAs(src, button) {{
const rows = collect(src);
if (!rows.length) return;
const plain = rows.map(([who, text]) => who + ': ' + text).join('\\n\\n');
const rich = '<div>' + rows.map(([who, text]) =>
'<p><b>' + esc(who) + ':</b> ' + esc(text) + '</p>').join('') + '</div>';
try {{
await navigator.clipboard.write([new ClipboardItem({{
'text/html': new Blob([rich], {{type: 'text/html'}}),
'text/plain': new Blob([plain], {{type: 'text/plain'}})}})]);
}} catch (e) {{
// Старые браузеры и небезопасное соединение: кладём хотя бы текст.
await navigator.clipboard.writeText(plain);
}}
const was = button.textContent;
button.textContent = 'скопировано';
setTimeout(() => {{ button.textContent = was; }}, 1500);
}}
document.querySelectorAll('button.copy').forEach(b =>
b.addEventListener('click', () => copyAs(b.dataset.src, b)));
</script></body></html>"""