docs(prototype): добавлен макет транскрипта со спикерами
- Зачем: - требовалось выбрать компактный Markdown-формат реплик до реализации диаризации. - Что: - добавлены три переключаемых варианта на реальном обезличенном фрагменте. - выбранный линейный вариант обновлён до формата `[MM:SS] Speaker N: текст`. - Проверка: - файл открыт локально, варианты A–C переключаются стрелками и параметром `variant`.
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PROTOTYPE — формат транскрипта со спикерами</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--paper: #fbfaf7;
|
||||
--ink: #25221f;
|
||||
--muted: #746e67;
|
||||
--line: #ddd7ce;
|
||||
--accent: #9b3f2f;
|
||||
--code: #f0ece5;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #e9e4dc;
|
||||
color: var(--ink);
|
||||
font: 16px/1.58 system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
main {
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 28px auto 100px;
|
||||
}
|
||||
.prototype-note {
|
||||
margin-bottom: 18px;
|
||||
padding: 12px 16px;
|
||||
border: 1px dashed #a59d92;
|
||||
background: #fffdf8;
|
||||
color: #5f5850;
|
||||
}
|
||||
.prototype-note strong { color: var(--accent); }
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(340px, .72fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
.paper, .source-panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--paper);
|
||||
box-shadow: 0 8px 28px rgb(49 40 31 / 9%);
|
||||
}
|
||||
.paper { padding: clamp(22px, 4vw, 54px); }
|
||||
.source-panel { position: sticky; top: 18px; overflow: hidden; }
|
||||
.source-panel h2 {
|
||||
margin: 0;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
pre {
|
||||
max-height: calc(100vh - 120px);
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
overflow: auto;
|
||||
background: var(--code);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font: 13px/1.55 ui-monospace, "Cascadia Code", Consolas, monospace;
|
||||
}
|
||||
h1 { margin: 0 0 22px; font: 700 clamp(26px, 3vw, 38px)/1.15 Georgia, serif; }
|
||||
h2 { margin: 28px 0 12px; font-size: 20px; }
|
||||
h3 { margin: 25px 0 7px; font-size: 17px; }
|
||||
ul { padding-left: 21px; }
|
||||
hr { margin: 28px 0; border: 0; border-top: 1px solid var(--line); }
|
||||
.turn { margin: 18px 0; }
|
||||
.time { color: var(--muted); font: 13px ui-monospace, "Cascadia Code", monospace; }
|
||||
.speaker { color: #783427; }
|
||||
blockquote {
|
||||
margin: 8px 0 22px;
|
||||
padding: 10px 16px;
|
||||
border-left: 4px solid #b88772;
|
||||
background: #f5f0e9;
|
||||
}
|
||||
blockquote p { margin: 0; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||||
th, td { padding: 9px 8px; border: 1px solid var(--line); vertical-align: top; text-align: left; }
|
||||
th { background: #eee8de; }
|
||||
.warning {
|
||||
margin: 17px 0;
|
||||
padding: 10px 13px;
|
||||
border-left: 4px solid #c47b23;
|
||||
background: #fff2dc;
|
||||
}
|
||||
.overlap { background: #f7e9e4; }
|
||||
.switcher {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 22px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transform: translateX(-50%);
|
||||
padding: 7px;
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
border-radius: 999px;
|
||||
background: #201d1a;
|
||||
box-shadow: 0 8px 32px rgb(0 0 0 / 25%);
|
||||
color: white;
|
||||
}
|
||||
.switcher button {
|
||||
width: 38px;
|
||||
height: 34px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: #39332e;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
.switcher button:hover { background: #554b43; }
|
||||
#variant-label { min-width: 230px; text-align: center; font-size: 14px; }
|
||||
@media (max-width: 820px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.source-panel { position: static; }
|
||||
pre { max-height: none; }
|
||||
#variant-label { min-width: 190px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Три варианта markdown-транскрипта, переключаемые через ?variant=, в отдельном throwaway-прототипе. -->
|
||||
<main>
|
||||
<div class="prototype-note">
|
||||
<strong>PROTOTYPE — не часть продукта.</strong>
|
||||
Реальный фрагмент рабочей встречи слегка сокращён и обезличен; интервалы
|
||||
и статистика взяты из уже выполненного замера. Слева — вид документа,
|
||||
справа — буквальный Markdown для оценки последующей обработки ИИ.
|
||||
</div>
|
||||
<div class="layout">
|
||||
<article class="paper" id="preview"></article>
|
||||
<section class="source-panel">
|
||||
<h2>Markdown-источник</h2>
|
||||
<pre id="source"></pre>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<nav class="switcher" aria-label="Переключение вариантов">
|
||||
<button id="previous" aria-label="Предыдущий вариант">←</button>
|
||||
<span id="variant-label"></span>
|
||||
<button id="next" aria-label="Следующий вариант">→</button>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
const variants = {
|
||||
A: {
|
||||
name: "Линейные реплики",
|
||||
source: `# Транскрипт: пример-встречи.mp4
|
||||
|
||||
- **Дата транскрипции**: 2026-08-14 12:00:00
|
||||
- **Модель**: gigaam-v3-e2e-rnnt
|
||||
- **Язык**: ru (задан явно)
|
||||
- **Длительность**: 25:59
|
||||
- **Устройство**: ONNX (CPU)
|
||||
- **Диаризация**: 4 голосовых кластера
|
||||
- **Внимание**: Speaker 4 — малый кластер (00:19; 1,3% речи), возможна ошибка разделения
|
||||
|
||||
---
|
||||
|
||||
[08:50] Speaker 3: А показываем, получается, в их контуре, не в нашем?
|
||||
|
||||
[08:54] Speaker 2: В нашем, по-моему.
|
||||
|
||||
[08:55] Speaker 3: В нашем. А, отлично. У нас просто есть тестовый контур, который на самом деле…
|
||||
|
||||
[09:07] Speaker 1: Мы же у них не разворачиваемся. Мне гораздо проще накатывать обновления на наш контур.
|
||||
|
||||
[11:34] Speaker 2: Результаты проверок пишутся туда же.
|
||||
|
||||
[11:39] Speaker 1: По сути, нам нужно переписать только ту часть, которая обрабатывает файл.
|
||||
|
||||
[11:43] Speaker 3: Да, а дальше переиспользовать существующую запись результатов.`,
|
||||
preview: `
|
||||
<h1>Транскрипт: пример-встречи.mp4</h1>
|
||||
<ul>
|
||||
<li><strong>Дата транскрипции</strong>: 2026-08-14 12:00:00</li>
|
||||
<li><strong>Модель</strong>: gigaam-v3-e2e-rnnt</li>
|
||||
<li><strong>Язык</strong>: ru (задан явно)</li>
|
||||
<li><strong>Длительность</strong>: 25:59</li>
|
||||
<li><strong>Устройство</strong>: ONNX (CPU)</li>
|
||||
<li><strong>Диаризация</strong>: 4 голосовых кластера</li>
|
||||
</ul>
|
||||
<div class="warning"><strong>Внимание:</strong> Speaker 4 — малый кластер (00:19; 1,3% речи), возможна ошибка разделения.</div>
|
||||
<hr>
|
||||
<p class="turn"><span class="time">[08:50]</span> Speaker 3: А показываем, получается, в их контуре, не в нашем?</p>
|
||||
<p class="turn"><span class="time">[08:54]</span> Speaker 2: В нашем, по-моему.</p>
|
||||
<p class="turn"><span class="time">[08:55]</span> Speaker 3: В нашем. А, отлично. У нас просто есть тестовый контур, который на самом деле…</p>
|
||||
<p class="turn"><span class="time">[09:07]</span> Speaker 1: Мы же у них не разворачиваемся. Мне гораздо проще накатывать обновления на наш контур.</p>
|
||||
<p class="turn"><span class="time">[11:34]</span> Speaker 2: Результаты проверок пишутся туда же.</p>
|
||||
<p class="turn"><span class="time">[11:39]</span> Speaker 1: По сути, нам нужно переписать только ту часть, которая обрабатывает файл.</p>
|
||||
<p class="turn overlap"><span class="time">[11:43]</span> Speaker 3: Да, а дальше переиспользовать существующую запись результатов.</p>`
|
||||
},
|
||||
B: {
|
||||
name: "Сценарий",
|
||||
source: `# Транскрипт: пример-встречи.mp4
|
||||
|
||||
> Диаризация нашла четыре голосовых кластера. Speaker 4 занимает 19 секунд и может быть ошибкой разделения.
|
||||
|
||||
---
|
||||
|
||||
### Speaker 3 · [08:50.75 - 08:54.73]
|
||||
|
||||
> А показываем, получается, в их контуре, не в нашем?
|
||||
|
||||
### Speaker 2 · [08:54.73 - 08:55.91]
|
||||
|
||||
> В нашем, по-моему.
|
||||
|
||||
### Speaker 3 · [08:55.81 - 09:07.96]
|
||||
|
||||
> В нашем. А, отлично. У нас просто есть тестовый контур, который на самом деле…
|
||||
|
||||
### Speaker 1 · [09:07.96 - 09:18.80]
|
||||
|
||||
> Мы же у них не разворачиваемся. Мне гораздо проще накатывать обновления на наш контур.
|
||||
|
||||
## Перекрывающаяся речь · [11:43.57 - 11:44.82]
|
||||
|
||||
> **Speaker 1:** По сути, нам нужно переписать только ту часть, которая обрабатывает файл.
|
||||
>
|
||||
> **Speaker 3:** Да, а дальше переиспользовать существующую запись результатов.`,
|
||||
preview: `
|
||||
<h1>Транскрипт: пример-встречи.mp4</h1>
|
||||
<blockquote><p>Диаризация нашла четыре голосовых кластера. Speaker 4 занимает 19 секунд и может быть ошибкой разделения.</p></blockquote>
|
||||
<hr>
|
||||
<h3>Speaker 3 · <span class="time">[08:50.75 - 08:54.73]</span></h3>
|
||||
<blockquote><p>А показываем, получается, в их контуре, не в нашем?</p></blockquote>
|
||||
<h3>Speaker 2 · <span class="time">[08:54.73 - 08:55.91]</span></h3>
|
||||
<blockquote><p>В нашем, по-моему.</p></blockquote>
|
||||
<h3>Speaker 3 · <span class="time">[08:55.81 - 09:07.96]</span></h3>
|
||||
<blockquote><p>В нашем. А, отлично. У нас просто есть тестовый контур, который на самом деле…</p></blockquote>
|
||||
<h3>Speaker 1 · <span class="time">[09:07.96 - 09:18.80]</span></h3>
|
||||
<blockquote><p>Мы же у них не разворачиваемся. Мне гораздо проще накатывать обновления на наш контур.</p></blockquote>
|
||||
<h2>Перекрывающаяся речь · <span class="time">[11:43.57 - 11:44.82]</span></h2>
|
||||
<blockquote class="overlap"><p><strong>Speaker 1:</strong> По сути, нам нужно переписать только ту часть, которая обрабатывает файл.<br><br><strong>Speaker 3:</strong> Да, а дальше переиспользовать существующую запись результатов.</p></blockquote>`
|
||||
},
|
||||
C: {
|
||||
name: "Таблица событий",
|
||||
source: `# Транскрипт: пример-встречи.mp4
|
||||
|
||||
| Speaker | Речь | Доля | Примечание |
|
||||
|---|---:|---:|---|
|
||||
| Speaker 1 | 09:33 | 40,3% | основной кластер |
|
||||
| Speaker 2 | 08:07 | 34,3% | основной кластер |
|
||||
| Speaker 3 | 05:42 | 24,1% | основной кластер |
|
||||
| Speaker 4 | 00:19 | 1,3% | возможная ошибка разделения |
|
||||
|
||||
| Начало | Конец | Кто | Текст | Событие |
|
||||
|---:|---:|---|---|---|
|
||||
| 08:50.75 | 08:54.73 | S3 | А показываем, получается, в их контуре, не в нашем? | — |
|
||||
| 08:54.73 | 08:55.91 | S2 | В нашем, по-моему. | — |
|
||||
| 08:55.81 | 09:07.96 | S3 | В нашем. А, отлично. У нас просто есть тестовый контур… | — |
|
||||
| 09:07.96 | 09:18.80 | S1 | Мы же у них не разворачиваемся. Мне проще накатывать обновления на наш контур. | — |
|
||||
| 11:39.91 | 11:44.82 | S1 | Нам нужно переписать только ту часть, которая обрабатывает файл. | overlap:S3 |
|
||||
| 11:43.57 | 11:46.47 | S3 | Да, а дальше переиспользовать существующую запись результатов. | overlap:S1 |`,
|
||||
preview: `
|
||||
<h1>Транскрипт: пример-встречи.mp4</h1>
|
||||
<table>
|
||||
<thead><tr><th>Speaker</th><th>Речь</th><th>Доля</th><th>Примечание</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Speaker 1</td><td>09:33</td><td>40,3%</td><td>основной кластер</td></tr>
|
||||
<tr><td>Speaker 2</td><td>08:07</td><td>34,3%</td><td>основной кластер</td></tr>
|
||||
<tr><td>Speaker 3</td><td>05:42</td><td>24,1%</td><td>основной кластер</td></tr>
|
||||
<tr class="warning"><td>Speaker 4</td><td>00:19</td><td>1,3%</td><td>возможная ошибка разделения</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>События</h2>
|
||||
<table>
|
||||
<thead><tr><th>Начало</th><th>Конец</th><th>Кто</th><th>Текст</th><th>Событие</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>08:50.75</td><td>08:54.73</td><td>S3</td><td>А показываем, получается, в их контуре, не в нашем?</td><td>—</td></tr>
|
||||
<tr><td>08:54.73</td><td>08:55.91</td><td>S2</td><td>В нашем, по-моему.</td><td>—</td></tr>
|
||||
<tr><td>08:55.81</td><td>09:07.96</td><td>S3</td><td>В нашем. А, отлично. У нас просто есть тестовый контур…</td><td>—</td></tr>
|
||||
<tr><td>09:07.96</td><td>09:18.80</td><td>S1</td><td>Мы же у них не разворачиваемся. Мне проще накатывать обновления на наш контур.</td><td>—</td></tr>
|
||||
<tr class="overlap"><td>11:39.91</td><td>11:44.82</td><td>S1</td><td>Нам нужно переписать только ту часть, которая обрабатывает файл.</td><td>overlap:S3</td></tr>
|
||||
<tr class="overlap"><td>11:43.57</td><td>11:46.47</td><td>S3</td><td>Да, а дальше переиспользовать существующую запись результатов.</td><td>overlap:S1</td></tr>
|
||||
</tbody>
|
||||
</table>`
|
||||
}
|
||||
};
|
||||
|
||||
const keys = Object.keys(variants);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let current = (params.get("variant") || "A").toUpperCase();
|
||||
if (!variants[current]) current = "A";
|
||||
|
||||
function render() {
|
||||
const variant = variants[current];
|
||||
document.getElementById("preview").innerHTML = variant.preview;
|
||||
document.getElementById("source").textContent = variant.source;
|
||||
document.getElementById("variant-label").textContent = `${current} — ${variant.name}`;
|
||||
document.title = `${current} — ${variant.name} · PROTOTYPE`;
|
||||
}
|
||||
|
||||
function move(offset) {
|
||||
const index = keys.indexOf(current);
|
||||
current = keys[(index + offset + keys.length) % keys.length];
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("variant", current);
|
||||
window.history.replaceState({}, "", url);
|
||||
render();
|
||||
}
|
||||
|
||||
document.getElementById("previous").addEventListener("click", () => move(-1));
|
||||
document.getElementById("next").addEventListener("click", () => move(1));
|
||||
window.addEventListener("keydown", (event) => {
|
||||
const target = event.target;
|
||||
if (target.matches("input, textarea, [contenteditable]")) return;
|
||||
if (event.key === "ArrowLeft") move(-1);
|
||||
if (event.key === "ArrowRight") move(1);
|
||||
});
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user