+
Wer hat gesprochen?
+
+
+
+
+
+
+```
+
+**Step 3: Add JS to `frontend/app.js`**
+
+Add after the existing constants at the top:
+
+```javascript
+const speakerCard = document.getElementById('speaker-card');
+const speakerRows = document.getElementById('speaker-rows');
+const speakerConfirmBtn = document.getElementById('speaker-confirm-btn');
+const speakerAnonBtn = document.getElementById('speaker-anon-btn');
+let _speakerData = []; // [{id, excerpts, inputEl, currentIdx}, ...]
+```
+
+Add to the `STATUS_LABELS`:
+```javascript
+ awaiting_speakers: 'Sprecher zuordnen\u2026',
+```
+
+Replace the `ws.onmessage` handler — add handling for `speakers_unknown`:
+
+```javascript
+ ws.onmessage = (e) => {
+ const msg = JSON.parse(e.data);
+ if (msg.event === 'processing') setStatus('processing');
+ if (msg.event === 'saved') {
+ setStatus('idle');
+ hideSpeakerCard();
+ loadTranscripts();
+ }
+ if (msg.event === 'error') setStatus('error');
+ if (msg.event === 'speakers_unknown') showSpeakerCard(msg.speakers);
+ };
+```
+
+Add functions for the speaker card:
+
+```javascript
+function showSpeakerCard(speakers) {
+ _speakerData = [];
+ speakerRows.replaceChildren();
+ speakers.forEach(function(s) {
+ const row = document.createElement('div');
+ row.className = 'speaker-row';
+
+ const excerptEl = document.createElement('div');
+ excerptEl.className = 'speaker-excerpt';
+ excerptEl.textContent = s.excerpts[0] || '';
+
+ const counter = document.createElement('span');
+ counter.className = 'excerpt-counter';
+ counter.textContent = s.excerpts.length > 1 ? '1/' + s.excerpts.length : '';
+
+ let idx = 0;
+ const prev = document.createElement('button');
+ prev.textContent = '\u2039';
+ const next = document.createElement('button');
+ next.textContent = '\u203a';
+
+ function updateExcerpt() {
+ excerptEl.textContent = s.excerpts[idx] || '';
+ counter.textContent = s.excerpts.length > 1 ? (idx + 1) + '/' + s.excerpts.length : '';
+ }
+ prev.addEventListener('click', function() {
+ if (idx > 0) { idx--; updateExcerpt(); }
+ });
+ next.addEventListener('click', function() {
+ if (idx < s.excerpts.length - 1) { idx++; updateExcerpt(); }
+ });
+
+ const nav = document.createElement('div');
+ nav.className = 'excerpt-nav';
+ if (s.excerpts.length > 1) { nav.append(prev, counter, next); }
+
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.className = 'speaker-name-input';
+ input.placeholder = s.id.replace('SPEAKER_0', 'Sprecher ').replace('SPEAKER_', 'Sprecher ');
+
+ row.append(excerptEl, nav, input);
+ speakerRows.appendChild(row);
+ _speakerData.push({ id: s.id, input: input });
+ });
+ speakerCard.classList.remove('hidden');
+ setStatus('awaiting_speakers');
+}
+
+function hideSpeakerCard() {
+ speakerCard.classList.add('hidden');
+ _speakerData = [];
+}
+
+async function submitSpeakers(useNames) {
+ const body = {};
+ _speakerData.forEach(function(s) {
+ body[s.id] = useNames ? s.input.value.trim() : '';
+ });
+ await apiFetch('/speakers', { method: 'POST', body: JSON.stringify(body) });
+}
+
+speakerConfirmBtn.addEventListener('click', function() { submitSpeakers(true); });
+speakerAnonBtn.addEventListener('click', function() { submitSpeakers(false); });
+```
+
+**Step 4: Run full suite (no automated test for UI, visual check at step 5)**
+
+```bash
+.venv/bin/pytest -v 2>&1 | tail -15
+```
+Expected: all PASS (no test for UI JS)
+
+**Step 5: Commit**
+
+```bash
+git add frontend/index.html frontend/app.js
+git commit -m "feat: speaker naming card with excerpt navigator in main UI"
+```
+
+---
+
+### Task 11: Settings page — diarization section
+
+**Files:**
+- Modify: `frontend/settings.html`
+- Modify: `frontend/settings.js`
+
+**Step 1: Add HTML section to `frontend/settings.html`**
+
+After the `