fix(stop): Antwort in 0,04 s statt bis 51 s — MP3-Encode in den Hintergrund
POST /api/record/stop wartete auf den kompletten loudnorm-Encode: gemessen ~1,4 s pro aufgenommener Minute (#114: 35,5 min -> 50,9 s). Sichtbar war davon nichts als ein ausgegrauter Knopf; der Reiterwechsel baute den Knopf per render() wieder klickbar auf und der zweite Klick lief in "not recording", weil stopAndSave() current sofort auf null setzt. - recorder.js: encodeMp3() ohne await, schreibt <name>.mp3.part und benennt erst danach um. MP3_ARGS + "-f mp3" (ffmpeg leitet den Muxer aus der Endung ab und scheitert an .part). - web/src/main.js: state.stopping ueberlebt render() -> Knopf bleibt gesperrt, Uhr friert ein, Toast mit geschaetzter Wartezeit, zweiter Klick verworfen, "not recording" kein roter Fehler. Player laedt nach, solange /media 404 liefert (#mp3wait, 5-s-Retry, Abbruch nach 5 min). - server.js: sweepOrphans erkennt rec_*.mp3.part. Gegenprobe: 0,033 s / 0,044 s Stop-Antwort, "mp3 fertig" ~2 s spaeter, 64 kbit CBR / 48 kHz / mono ohne Xing-Tabelle, /media 200. Doku: Kapitel 03 (neuer Abschnitt + Vorher/Nachher-Sequenzdiagramm), Changelog 2026-08-20. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,9 +19,11 @@ sequenceDiagram
|
||||
SRV->>REC: addMarker() → Namensmarker mit Offset im Puffer
|
||||
GUI->>SRV: Stop
|
||||
SRV->>REC: stopAndSave()
|
||||
REC->>DB: WAV + MP3 + Zeile (status=transcribing)
|
||||
REC->>REC: parecord beenden, WAV nach recordings/ verschieben
|
||||
REC-)REC: encodeMp3() — Hintergrund, kein await
|
||||
REC->>DB: WAV-Zeile (status=transcribing)
|
||||
SRV->>DB: addMarkers() → Tabelle markers
|
||||
Note over REC: kehrt nach ~1,5 s zurück — GUI frei
|
||||
Note over REC: kehrt in ~0,04 s zurück — GUI frei
|
||||
GUI->>GUI: Kontextbox klappt auf (Freitext, seit 2026-08-17)
|
||||
GUI->>SRV: PATCH /api/recordings/:id/context {context_note}
|
||||
SRV->>DB: setContextNote() → recordings.context_note
|
||||
@@ -80,6 +82,78 @@ 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“).
|
||||
|
||||
## Der Stop-Knopf: sofortige Antwort, MP3 im Hintergrund
|
||||
|
||||
Bis 2026-08-20 hing `POST /api/record/stop` so lange, wie der MP3-Encode dauerte — **gemessen ~1,4 s pro
|
||||
aufgenommener Minute** (aus `wav_file`-Zeitstempel + `duration_ms` gegen `created_at`):
|
||||
|
||||
| Aufnahme | Dauer | Stop-Antwort |
|
||||
|---|---|---|
|
||||
| #114 | 35,5 min | 50,9 s |
|
||||
| #118 | 30,8 min | 43,8 s |
|
||||
| #111 | 20,8 min | 30,4 s |
|
||||
| #117 | 1,1 min | 2,0 s |
|
||||
|
||||
Sichtbar war in dieser Zeit **nichts**: die GUI grante nur den Knopf aus (`opacity .55`) und stoppte die
|
||||
Uhr. Der Nutzer hielt die Anwendung für hängengeblieben, wechselte den Reiter — was `render()` auslöst und
|
||||
den Knopf frisch (also klickbar) aufbaut, obwohl `state.recording` noch `true` war — und drückte ein
|
||||
zweites Mal Stop. Der zweite Stop lief zwangsläufig in `not recording`, weil `stopAndSave()` das
|
||||
`current`-Objekt schon in seiner ersten Zeile auf `null` setzt.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant U as Nutzer
|
||||
participant GUI as Web-GUI
|
||||
participant SRV as server.js
|
||||
participant FF as ffmpeg (Hintergrund)
|
||||
|
||||
rect rgba(229,89,94,.12)
|
||||
Note over U,SRV: VORHER — Encode blockierte die Antwort
|
||||
U->>GUI: Klick Stop
|
||||
GUI->>SRV: POST /api/record/stop
|
||||
Note over GUI: Knopf grau, Uhr steht,<br/>kein Toast (bis 51 s)
|
||||
U->>GUI: Reiterwechsel → render() → Knopf wieder aktiv
|
||||
U->>GUI: Klick Stop (zum zweiten Mal)
|
||||
GUI->>SRV: POST /api/record/stop
|
||||
SRV--)GUI: 500 „not recording"
|
||||
SRV--)GUI: (erste Antwort trifft jetzt ein)
|
||||
end
|
||||
|
||||
rect rgba(126,182,124,.12)
|
||||
Note over U,FF: NACHHER — Antwort in 0,04 s
|
||||
U->>GUI: Klick Stop
|
||||
GUI->>GUI: state.stopping = true → ⏳, gesperrt,<br/>Toast mit geschätzter Wartezeit
|
||||
GUI->>SRV: POST /api/record/stop
|
||||
SRV->>SRV: parecord beenden, WAV verschieben, DB-Zeile
|
||||
SRV-)FF: encodeMp3() ohne await
|
||||
SRV--)GUI: {id, …} nach ~0,04 s
|
||||
FF-->>FF: rec_*.mp3.part → rename → rec_*.mp3
|
||||
GUI->>GUI: Player lädt alle 5 s nach, solange /media 404 liefert
|
||||
end
|
||||
```
|
||||
|
||||
Drei Teile hat der Fix:
|
||||
|
||||
1. **`recorder.js stopAndSave()`** verschiebt erst das WAV nach `recordings/` (das ist die Arbeitskopie für
|
||||
Whisper) und ruft `encodeMp3()` dann **ohne `await`** auf. Geschrieben wird nach `<name>.mp3.part`,
|
||||
danach `fs.renameSync` — so bekommt der Browser nie eine halbe Datei zu hören, sondern bis zum
|
||||
Umbenennen ein 404. **`-f mp3` ist dabei Pflicht:** ffmpeg leitet den Muxer aus der Dateiendung ab und
|
||||
scheitert an `.part` mit `Error initializing the muxer … Invalid argument` (erst so aufgefallen —
|
||||
beide Encode-Zweige fielen still aus, das MP3 fehlte ganz).
|
||||
2. **`state.stopping` in der GUI** überlebt jeden `render()`. Der Knopf bleibt darum auch nach einem
|
||||
Reiterwechsel gesperrt (zeigt ⏳), die Uhr friert ein, die Live-Karte in der Liste sagt „Aufnahme wird
|
||||
gespeichert…", und ein Toast nennt die geschätzte Wartezeit. Ein zweiter Klick wird verworfen; kommt
|
||||
trotzdem ein `not recording` zurück (zweites Browserfenster, F5 mitten im Speichern), meldet die GUI
|
||||
sachlich „Aufnahme war bereits gestoppt" statt eines roten Fehlers.
|
||||
3. **Der Player lädt nach:** `#mp3wait` zeigt „Wiedergabe-Datei wird noch erzeugt", das `error`-Ereignis
|
||||
des `<audio>`-Tags ruft alle 5 s `audio.load()` (max. 5 min). `sweepOrphans` kennt jetzt auch
|
||||
`rec_*.mp3.part`, damit eine beim Absturz liegengebliebene Halbdatei nach der Altersgrenze wegfällt.
|
||||
|
||||
Nachgemessen nach dem Umbau: Stop antwortet in **0,033–0,044 s** (12- und 15-s-Testclips), das MP3 stand
|
||||
~2 s später mit 64 kbit CBR, 48 kHz, mono und ohne Xing-Tabelle da — die Anforderungen des nächsten
|
||||
Abschnitts bleiben also erfüllt.
|
||||
|
||||
## Wiedergabe-MP3: konstante Bitrate, keine Xing-Tabelle
|
||||
|
||||
Die WAV ist die Arbeitskopie (Whisper, Diarization), die MP3 ist nur fürs Abspielen im Browser —
|
||||
@@ -87,9 +161,12 @@ Die WAV ist die Arbeitskopie (Whisper, Diarization), die MP3 ist nur fürs Abspi
|
||||
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
|
||||
-codec:a libmp3lame -b:a 64k -ar 48000 -ac 1 -write_xing 0 -f mp3
|
||||
```
|
||||
|
||||
(`-f mp3` kam mit dem Hintergrund-Encode dazu, weil die Zieldatei `<name>.mp3.part` heißt — siehe
|
||||
vorherigen Abschnitt.)
|
||||
|
||||
**Kein VBR** (früher `-qscale:a 3`) und **keine Xing-Tabelle**. Der Grund liegt im Sprung, nicht in der
|
||||
Wiedergabe:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user