Files
trav 618f7ffc22 Add optional automatic speaker labels and room cast lists
CPU-only online speaker labelling for caption windows (CAPTION_DIARIZATION=1):
pyannote segmentation-3.0 finds local turns, a TitaNet-small voiceprint via
sherpa-onnx matches them to per-room speakers numbered in arrival order.
/transcribe?speakers=1 returns committed words grouped into segments with a
speaker ID and the name assigned for the show. Models are SHA-256 pinned and
cached with the Whisper weights; audio and voiceprints are never stored.

Rooms gain a persisted cast list and in-memory per-show names through
/api/rooms/<id>/speakers. The capture page offers "Identify speakers
automatically", publishes one relay caption per speaker turn with label and
spk, and shares a speaker-names panel (speaker-names.js) with the editor.

Also adds scripts/diarize_eval.py, deploy.py --speakers, and copies rooms.py
into the GPU image.
2026-10-28 10:54:05 -07:00

105 lines
3.1 KiB
CSS

:root {
color-scheme: dark;
--cc-bg: #0f1420;
--cc-panel: #172338;
--cc-panel-2: #0c111c;
--cc-border: #2c3c58;
--cc-text: #eef3fb;
--cc-muted: #9fb0cb;
--cc-accent: #2498eb;
--cc-accent-strong: #1769aa;
--cc-good: #4fd47f;
--cc-warn: #f2c14e;
--cc-danger: #ff7777;
--cc-radius: 12px;
--cc-radius-sm: 8px;
--cc-font: 16px/1.55 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.cc-dot {
display: inline-block;
flex: 0 0 auto;
width: 16px;
height: 16px;
border-radius: 50%;
background: #6f7d92;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.18);
vertical-align: -3px;
}
.cc-dot[data-state="good"] {
background: var(--cc-good);
box-shadow: 0 0 0 2px rgba(79, 212, 127, 0.18);
}
.cc-dot[data-state="warn"] {
background: var(--cc-warn);
box-shadow: 0 0 0 2px rgba(242, 193, 78, 0.18);
}
.cc-dot[data-state="danger"] {
background: var(--cc-danger);
box-shadow: 0 0 0 2px rgba(255, 119, 119, 0.18);
}
.cc-status-row {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 28px;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
.cc-room-menu { width: min(820px, calc(100% - 32px)); margin: 24px auto 18px; }
.cc-room-menu[hidden] { display: none; }
.cc-room-menu-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin: 0;
padding: 10px;
list-style: none;
background: var(--cc-panel);
border: 1px solid var(--cc-border);
border-radius: var(--cc-radius);
}
.cc-room-menu-list li { display: flex; align-items: center; gap: 8px; min-width: 0; }
.cc-room-menu-list li + li::before { content: "→"; color: var(--cc-muted); }
.cc-room-menu-item {
display: inline-block;
padding: 7px 10px;
border: 1px solid transparent;
border-radius: 7px;
color: var(--cc-text);
text-decoration: none;
white-space: nowrap;
}
.cc-room-menu-item:hover { border-color: var(--cc-accent); }
.cc-room-menu-item.is-current {
border-color: var(--cc-accent);
background: var(--cc-panel-2);
font-weight: 650;
}
.cc-room-menu-item.is-disabled { color: var(--cc-muted); opacity: 0.65; cursor: not-allowed; }
@media (max-width: 600px) {
.cc-room-menu-list li + li::before { display: none; }
}
.cc-speaker-list { display: grid; gap: 8px; margin: 10px 0; }
.cc-speaker-row { display: grid; grid-template-columns: minmax(90px, max-content) minmax(0, 1fr); align-items: center; gap: 10px; }
.cc-speaker-row > span { color: var(--cc-muted); font-weight: 650; }
.cc-speaker-chip { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.cc-speaker-row input, .cc-speaker-chip input, .cc-speaker-list + details textarea {
font: inherit; padding: 8px 10px; border: 1px solid var(--cc-border); border-radius: var(--cc-radius-sm);
background: var(--cc-panel-2); color: var(--cc-text); width: 100%; margin: 0; box-sizing: border-box;
}
.cc-speaker-row input:focus, .cc-speaker-chip input:focus { outline: 2px solid var(--cc-accent); outline-offset: 1px; }
.cc-speaker-list + details textarea { min-height: 0; font-size: 1rem; margin: 8px 0; }