Das Loch am Anfang der Sprachnachrichten -- und ein halb fertiges Stueck

NACHGEMESSEN AN EINER ECHTEN NACHRICHT AUS DEM HAUS, nachdem die
Zweitfassungen heute Nacht live entstanden waren. `mumqbw6v….webm`
vom 29.09., 46 Pakete:

    Paket 1 bei   0,000 s
    Paket 2 bei  61,116 s
    ...
    Paket 46 bei 63,756 s

2,76 Sekunden Ton, verteilt ueber eine Zeitachse von 63,8 Sekunden.
Dazwischen ein Loch von einer Minute. `MediaRecorder` setzt diesen
Versatz selbst; die Datenbank kennt ihn nicht (dort steht die Angabe
des Absenders: 2910 ms).

WAS DAS FUER DEN HOERER HEISST: Das Abspielgeraet zeigt 1:03,
springt beim Antippen in eine Minute Stille und faengt erst danach
an. Auf einem Handy ueber Mobilfunk sieht genau das aus wie „laedt
die ganze Zeit" -- Miss' Satz aus Runde 5, woertlich.

Das war also nicht nur der Behaelter. Die Umwandlung von heute Nacht
hat das Loch brav mituebernommen: 63,8 s Zeitachse, 32 KB Inhalt.

GEAENDERT: `-af asetpts=N/SR/TB` vergibt die Zeitstempel neu,
fortlaufend ab null. An der echten Datei nachgemessen -- der
Toninhalt bleibt unveraendert (260 KiB dekodiert vorher wie
nachher), nur die Laenge geht von 63,8 s auf 2,76 s. Es wird nichts
abgeschnitten, sondern ein Loch geschlossen.

UND EIN ZWEITER FUND, von der Pruefung und nicht vom Nachdenken

Sie holte die zweite Fassung ab und bekam 44 BYTES. ffmpeg legt die
Zieldatei sofort an und fuellt sie danach -- bei `+faststart`
schreibt es sie am Ende noch einmal um. Die Stelle, die fragt „gibt
es die zweite Fassung?", sah in dieser Zeit eine Datei, die es gibt
und die nichts enthaelt. Wer dann zuhoert, bekommt ein Bruchstueck.

Jetzt entsteht sie unter `….m4a.teil` und wird erst am Ende
umbenannt. Umbenennen im selben Ordner ist EIN Schritt: entweder
vollstaendig da oder gar nicht. Der Bruchstuecknamen endet
ausdruecklich NICHT auf `.m4a`, sonst waere die Luecke nur
umbenannt -- und deshalb muss `-f mp4` dabeistehen, weil ffmpeg das
Format sonst an der Endung waehlt und `.teil` nicht kennt. Auch das
hat die Pruefung gefunden: Der erste Versuch mit Umbenennen erzeugte
gar keine Datei mehr.

WAS DIE PRUEFUNG JETZT MISST -- UND WAS NICHT

Sie vergleicht NICHT mehr die Zeitachsen beider Dateien. Genau das
war mein erster Anlauf, und er haette das Loch durchgewunken: Wenn
es mitwandert, sind beide Zeitachsen gleich lang und alles ist
gruen. Gefragt wird jetzt nach dem TON -- wie viele Bytes
herauskommen, wenn man beide dekodiert (231 KiB zu 231 KiB) -- und
danach, ob die Zeitachse zu dieser Tonmenge passt (2,46 s fuer
2,46 s Ton).

Ein dritter kleiner: `tonBytes` las die Rueckgabe von
`execFileSync`. ffmpeg schreibt seine Zusammenfassung aber auf
stderr -- die Messung meldete fuer beide Dateien 0 KiB. Rot, und zu
Recht: Sie hat nichts gemessen.

WAS BEWUSST OFFEN BLEIBT: Eine Aufnahme, die schon als MP4
hereinkommt (seit dem 02.10. der Normalfall), wird nicht
umgewandelt -- und traegt ein solches Loch, falls sie eines hat,
weiter. Dass sie eines haette, ist nicht gemessen: Seit der
Umstellung gibt es keine einzige neue Aufnahme. AAC noch einmal nach
AAC zu wandeln kostet Qualitaet fuer ein Problem, das ich nicht
beobachtet habe. Faellt es auf, ist die Stelle bekannt.

GEPRUEFT -- pruef-chat-anhaenge 163 -> 164 ok

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-03 01:55:46 +02:00
co-authored by Claude Opus 5
parent d70d00cedc
commit ff48a0a6ef
2 changed files with 113 additions and 12 deletions
+67 -2
View File
@@ -43,7 +43,7 @@
kommt nur niemand dazu.
===================================================================== */
import { existsSync } from "node:fs";
import { existsSync, renameSync, unlinkSync } from "node:fs";
import { execFile } from "node:child_process";
import { delimiter, join, extname, basename, dirname } from "node:path";
@@ -161,6 +161,30 @@ export function zweitfassungBauen(pfad) {
denselben Ordner; ohne diese Zeile würde er jedes Mal alles
neu wandeln. */
if (existsSync(ziel)) return fertig(true);
/* ==== ERST FERTIG, DANN SICHTBAR (03.10.2026) ================
GEFUNDEN VON DER PRÜFUNG, nicht beim Nachdenken: Sie holte die
zweite Fassung ab und bekam 44 Bytes.
ffmpeg LEGT DIE ZIELDATEI SOFORT AN und füllt sie danach — bei
`+faststart` schreibt es sie am Ende sogar noch einmal um. Die
Stelle, die fragt „gibt es die zweite Fassung?", sieht in
dieser Zeit eine Datei, die es zwar gibt, die aber noch nichts
enthält. Wer dann zuhört, bekommt ein Bruchstück.
Deshalb entsteht sie unter einem ANDEREN Namen und wird erst
am Ende umbenannt. Umbenennen im selben Ordner ist EIN Schritt:
Entweder die Datei ist vollständig da oder gar nicht.
DER BRUCHSTÜCKNAME ENDET NICHT AUF `.m4a` — sonst hielte
`zweitfassungDa()` ihn für das Ergebnis, und die Lücke wäre
nur umbenannt. Dafür muss das Format ausdrücklich dastehen
(`-f mp4`): ffmpeg wählt es sonst an der Endung, und `.teil`
kennt es nicht. (Auch das hat die Prüfung gefunden — der erste
Versuch mit Umbenennen erzeugte gar keine Datei mehr.) */
const bruchstueck = ziel + ".teil";
try { if (existsSync(bruchstueck)) unlinkSync(bruchstueck); } catch { /* egal */ }
execFile(ff, [
"-nostdin", "-hide_banner", "-loglevel", "error",
"-i", pfad,
@@ -168,17 +192,58 @@ export function zweitfassungBauen(pfad) {
Audiodatei hätte es nichts verloren und bringt ffmpeg bei
manchen Dateien zum Stolpern. */
"-vn",
/* ==== DAS LOCH AM ANFANG (03.10.2026, gemessen) =============
An einer echten Sprachnachricht aus dem Haus nachgesehen
(`mumqbw6v…webm`, 29.09.): Sie enthält 46 Pakete mit
zusammen 2,76 Sekunden Ton — verteilt über eine Zeitachse
von 63,8 Sekunden. Das erste Paket steht bei 0,0 s, das
zweite bei 61,1 s. Dazwischen: nichts.
WAS DAS FÜR DEN HÖRER HEISST: Das Abspielgerät zeigt 1:03,
springt beim Antippen in eine Minute Stille und fängt erst
danach an. Auf einem Handy über Mobilfunk sieht genau das
aus wie „lädt die ganze Zeit" — Miss' Satz aus Runde 5.
DAS IST NICHT UNSER FEHLER UND TROTZDEM UNSER PROBLEM:
`MediaRecorder` setzt diesen Versatz selbst. Reparieren
lässt er sich nur hier, beim Umwandeln.
`asetpts=N/SR/TB` vergibt die Zeitstempel neu, fortlaufend
ab null. Nachgemessen bleibt der Toninhalt dabei
unverändert: 260 KiB dekodiert vorher wie nachher, nur die
Länge geht von 63,8 s auf 2,76 s. Es wird also nichts
abgeschnitten, sondern ein Loch geschlossen.
WAS ES KOSTET: Eine Aufnahme mit einer echten Pause in der
Mitte rückt zusammen. Bei einer Sprachnachricht ist das
richtig so — `MediaRecorder` nimmt während einer Pause
ohnehin nichts auf, es entstünde also auch dort nur ein
Loch aus Stille. */
"-af", "asetpts=N/SR/TB",
"-c:a", "aac", "-b:a", RATE,
"-movflags", "+faststart",
/* NICHT ÜBERSCHREIBEN. Eine halbe Datei von einem
abgebrochenen Lauf soll auffallen, nicht stillschweigend
ersetzt werden -- und `existsSync` oben hat ohnehin schon
gefragt. */
"-n", ziel,
"-f", "mp4", "-n", bruchstueck,
], { timeout: FRIST_MS }, (fehler) => {
const weg = () => {
try { if (existsSync(bruchstueck)) unlinkSync(bruchstueck); } catch { /* egal */ }
};
if (fehler) {
console.error(`[ton] Zweitfassung fuer ${basename(pfad)} ging nicht:`,
String(fehler.message || fehler).split("\n")[0]);
weg();
return fertig(false);
}
/* DER EINE SCHRITT, DER SIE SICHTBAR MACHT. Siehe oben: Erst
fertig, dann unter dem Namen, nach dem gefragt wird. */
try { renameSync(bruchstueck, ziel); } catch (f) {
console.error(`[ton] Zweitfassung fuer ${basename(pfad)} nicht umbenannt:`,
f?.message);
weg();
return fertig(false);
}
fertig(true);
+46 -10
View File
@@ -1232,16 +1232,52 @@ async function tonAufnehmen(seite, ms) {
ok(codecAlt === "opus",
`das Original ist Opus -- also genau der Fall von Miss (${codecAlt})`);
ok(codec === "aac", `drinnen steckt AAC, nicht nur eine neue Endung (${codec})`);
ok(dauerAlt > 0 && Math.abs(dauerNeu - dauerAlt) < 0.5,
`und sie ist so lang wie das Original (${dauerAlt.toFixed(2)} s → `
+ `${dauerNeu.toFixed(2)} s)`);
/* GEGENPROBE ZUR LAENGE: Eine halbierte Datei MUSS
auffallen. Ohne diese Zeile waere die Pruefung darueber
auch mit einer abgeschnittenen Aufnahme gruen, solange
ffprobe irgendeine Zahl liefert. */
ok(Math.abs((dauerAlt / 2) - dauerAlt) >= 0.5,
` Gegenprobe: eine halbierte Laenge waere aufgefallen `
+ `(${(dauerAlt / 2).toFixed(2)} s gegen ${dauerAlt.toFixed(2)} s)`);
/* ==== DIE LAENGE IST DER TON, NICHT DIE ZEITACHSE ======
Verglichen wird NICHT `format=duration` beider Dateien.
An einer echten Sprachnachricht aus dem Haus gemessen:
Das Original hat 2,76 s Ton auf einer Zeitachse von
63,8 s -- `MediaRecorder` setzt das erste Paket auf 0
und das zweite auf 61,1. Wer die Zeitachsen
vergleicht, prueft dann, ob das Loch mitgewandert ist.
Gefragt wird deshalb nach dem TON: wie viele Bytes
herauskommen, wenn man beide dekodiert. Die muessen
gleich sein -- dann ist nichts abgeschnitten und nichts
dazuerfunden. */
const { spawnSync } = await import("node:child_process");
/* UEBER stderr UND NICHT ueber die Rueckgabe: ffmpeg
schreibt seine Zusammenfassung („audio:260KiB") dorthin,
nicht auf die Standardausgabe. Mein erster Anlauf las
die Rueckgabe von `execFileSync` und bekam bei beiden
Dateien 0 KiB -- die Pruefung wurde rot und hatte recht:
Sie hat nichts gemessen. */
const tonBytes = (datei) => {
const r = spawnSync(ffmpegPfad(),
["-nostdin", "-hide_banner", "-i", datei, "-f", "null", "-"],
{ encoding: "utf8" });
return String(r.stderr || "");
};
const kib = (t) => Number((t.match(/audio:(\d+)KiB/) || [])[1] || 0);
const tonAlt = kib(tonBytes(quelle));
const tonNeu = kib(tonBytes(ziel));
ok(tonAlt > 0 && Math.abs(tonNeu - tonAlt) <= 2,
`der Toninhalt bleibt derselbe (${tonAlt} KiB → ${tonNeu} KiB dekodiert)`);
/* UND DAS LOCH IST WEG. Die Zeitachse der Zweitfassung darf
nicht laenger sein als der Ton, den sie traegt. 48 kHz,
zwei Byte, ein Kanal: 1 KiB sind rund 10,7 ms. */
const tonSekunden = (tonNeu * 1024) / (48000 * 2);
ok(dauerNeu > 0 && dauerNeu < tonSekunden + 1.5,
`und die Zeitachse passt zum Ton (${dauerNeu.toFixed(2)} s fuer `
+ `${tonSekunden.toFixed(2)} s Ton) — das Loch am Anfang ist weg`);
/* GEGENPROBE: Mit dem Loch waere es aufgefallen. Die
Originaldatei aus diesem Lauf hat keines (Chromium
erzeugt es nicht immer) -- deshalb wird hier gerechnet,
was gewesen waere, statt zu behaupten, es koenne nicht
vorkommen. */
ok(dauerAlt === 0 || dauerAlt < tonSekunden + 1.5 || dauerNeu < dauerAlt,
` Gegenprobe: eine Zeitachse von ${dauerAlt.toFixed(2)} s fuer `
+ `${tonSekunden.toFixed(2)} s Ton waere aufgefallen`);
}
/* ---- DIE INHALTSUEBERSICHT STEHT VORNE --------------------