fix(audio): Wiedergabe-MP3 als CBR ohne Xing-Tabelle -- Wortmarkierung war 28 s voraus

Die Markierung im Transkript stand auf einem Wort, gesprochen wurde eins vier
Zeilen weiter oben (gemessen an #85: 24 s Vorlauf).

Nicht die Ursache -- jeweils per Gegenprobe ausgeschlossen: die Wortzeiten
(Ausschnitte bei 600/1240/3400 s einzeln transkribiert, enthalten exakt die
Wörter der DB), MP3- vs WAV-Zeitachse (identisch), ffmpeg-Seek (trifft),
ontimeupdate-Logik (korrekt).

Ursache war der Sprung im Browser: in einer VBR-Datei gibt es keine feste
Byte-pro-Sekunde-Regel, also nutzt er die Xing-Tabelle -- 100 Stützstellen,
bei 63 Minuten ein 38-s-Raster. Er landet auf der Stützstelle davor, meldet
über currentTime aber die angefragte Zeit. Nachgerechnet: Ziel 20:40 spielt
ab 20:11,5 (-28,5 s).

Fix: -b:a 64k -ar 48000 -ac 1 -write_xing 0 statt -qscale:a 3. Byte-Position
proportional zur Zeit. Die Tabelle muss auch bei CBR weg, sie rastert auf
1/256 der Dateigröße (~15 s pro Stunde, Restfehler bis -6,4 s gemessen). Ohne
sie rechnet der Decoder linear: Restfehler 0,02 s an neun Stichpunkten.

lab/recode-cbr.sh schreibt den Altbestand einmalig um (Quelle WAV, sonst die
MP3 selbst), lab/tocerr.py ist das Messwerkzeug dahinter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-08-02 23:40:29 +02:00
co-authored by Claude Opus 5
parent 3496b4a2df
commit d9554c590c
5 changed files with 239 additions and 3 deletions
+35
View File
@@ -75,6 +75,41 @@ stateDiagram-v2
- **GUI-Anzeige „⏱ Analysezeit“:** `analysis_started_at`/`analysis_ms` in der DB; beim Polling auf
`analysis_status` achten, NICHT auf `status` (sonst wirkt die Summary „leer“).
## Wiedergabe-MP3: konstante Bitrate, keine Xing-Tabelle
Die WAV ist die Arbeitskopie (Whisper, Diarization), die MP3 ist nur fürs Abspielen im Browser —
`highpass=f=80` gegen Rumpeln, `loudnorm` für gleichmäßige Lautstärke. Beim Codec sind aber **zwei
Einstellungen nicht verhandelbar**, sonst läuft die Wortmarkierung im Transkript aus dem Takt:
```
-codec:a libmp3lame -b:a 64k -ar 48000 -ac 1 -write_xing 0
```
**Kein VBR** (früher `-qscale:a 3`) und **keine Xing-Tabelle**. Der Grund liegt im Sprung, nicht in der
Wiedergabe:
```mermaid
flowchart LR
A["Klick auf 20:40<br/>audio.currentTime = 1240"] --> B{Wie findet der<br/>Browser das Byte?}
B -->|"VBR: Xing-Tabelle<br/>100 Stützstellen ≈ 38 s Raster"| C["landet bei 20:11,5<br/>meldet aber 20:40"]
B -->|"CBR ohne Tabelle:<br/>Byte = Zeit × Bitrate"| D["landet bei 20:40,0<br/>Fehler 0,02 s"]
C --> E["Ton hinkt 28 s nach,<br/>Markierung läuft voraus"]
D --> F["synchron"]
```
Nachgemessen an #85 (63 min): ein Sprung auf 20:40 spielte in Wahrheit ab **20:11,5 (−28,5 s)** — genau
das Bild, das der Nutzer meldete („markiert ist ein Wort vier Zeilen unter dem, was ich höre"). Die
Wortzeitstempel selbst waren dabei korrekt; per Gegenprobe transkribierte Ausschnitte an t = 600/1240/3400 s
enthielten exakt die Wörter, die die Datenbank dort führt.
Die Tabelle muss auch bei CBR weg: sie speichert Byte-Positionen in 1/256 der Dateigröße, was bei einer
Stunde Audio rund 15 s Rundung sind (gemessen: bis −6,4 s Restfehler). Ohne sie rechnet der Decoder
linear — bei konstanter Bitrate exakt.
64 kbit CBR entspricht praktisch der bisherigen Durchschnittsrate (68 kbit); #85 schrumpfte von 32,3 auf
30,3 MB. Der Altbestand wurde einmalig mit `lab/recode-cbr.sh` umgeschrieben (Quelle: die WAV, wo noch
vorhanden — sonst die MP3 selbst, dann ohne erneute Filter).
## API-Endpunkte (Auszug)
| Endpunkt | Zweck |
+40
View File
@@ -3,6 +3,46 @@
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
Was, Warum, betroffene Dateien/Backups).
## 2026-08-02 — Wortmarkierung lief dem Ton voraus: VBR-MP3 war schuld
**Symptom:** im Transkript war ein Wort markiert, gesprochen wurde aber eins rund vier Zeilen weiter oben.
Gemessen an #85: markiert „auf" (20:40,3), zu hören „OneDrive" (20:16,3) — **24 s Vorlauf**.
**Was es NICHT war** (der Reihe nach ausgeschlossen, jeweils mit Gegenprobe):
| Verdacht | Prüfung | Ergebnis |
|---|---|---|
| Wortzeiten driften (VAD-Trimming im Batch-Modus) | Ausschnitte bei t = 600/1240/3400 s aus der WAV geschnitten und einzeln transkribiert | dort stehen exakt die Wörter, die die DB führt — **Zeitstempel korrekt** |
| MP3 und WAV haben verschiedene Zeitachsen | MP3 komplett dekodiert, dieselben Stellen geprüft | identisch |
| Datei kaputt / ffmpeg-Seek daneben | `ffmpeg -ss` an denselben Stellen | trifft genau |
| Anzeige-Logik (`ontimeupdate`) | Code gelesen: letztes Wort mit `t_start <= t` | korrekt |
**Was es war:** der Sprung im Browser. Die MP3 war VBR, dort gibt es keine feste Byte-pro-Sekunde-Regel,
also nutzt der Browser die **Xing-Tabelle — 100 Stützstellen, bei 63 Minuten ein Raster von 38 s**. Er
landet auf der Stützstelle davor, meldet über `currentTime` aber die angefragte Zeit zurück. Nachgerechnet
(`lab/tocerr.py`, Frames gezählt + Tabelle geparst): Ziel 20:40 → tatsächliche Abspielposition **20:11,5,
also −28,5 s**. Genau die beobachtete Größenordnung, und der Versatz bleibt bis zum nächsten Sprung stehen.
**Fix:** MP3 künftig **CBR mit 64 kbit und ohne Xing-Tabelle**
(`-b:a 64k -ar 48000 -ac 1 -write_xing 0` statt `-qscale:a 3`). Dann ist die Byte-Position proportional
zur Zeit und jeder Sprung sitzt. Die Tabelle muss auch bei CBR weg — sie rastert Byte-Positionen auf
1/256 der Dateigröße, was bei einer Stunde ~15 s sind (Restfehler gemessen: bis −6,4 s). Ohne sie rechnet
der Decoder linear: **Restfehler 0,02 s = ein halber Frame**, an neun Stichpunkten über die ganze Datei
geprüft.
Dateigröße bleibt praktisch gleich (#85: 32,3 → 30,3 MB), 64 kbit CBR ≈ die bisherige Durchschnittsrate
von 68 kbit.
**Altbestand:** `lab/recode-cbr.sh` schreibt alle vorhandenen Aufnahmen einmalig um — Quelle ist die WAV
mit derselben Filterkette wie beim Aufnehmen, für die alten Aufnahmen ohne WAV die MP3 selbst (dann ohne
erneutes `loudnorm`, das steckt dort schon drin). Schreibt erst in eine `.tmp`, vergleicht die Dauer
(±2 s) und ersetzt erst dann; pausiert oberhalb von 88 °C, weil das Board keinen Lüfter hat.
Zwei Stolpersteine dabei: `-f mp3` ist Pflicht (aus der Endung `.mp3.tmp` leitet ffmpeg kein Format ab),
und eine 0-Byte-Altdatei (`rec_1784247236781.mp3`) bleibt als Fehler liegen statt den Lauf abzubrechen.
Dateien: `app/recorder.js` (Board-Backup `.bak-cbr`), `app/lab/recode-cbr.sh` (neu),
`app/lab/tocerr.py` (neu, Messwerkzeug), Kapitel [03 – Aufnahme-Pipeline](03-aufnahme-pipeline.md).
## 2026-08-02 (nachgereicht) — Grüne Nadel, Logo führt zurück zu den Aufnahmen
**Nadel grün statt rot:** der Anheften-Knopf in der Aufnahmenliste zeigte bei einer angehefteten Aufnahme