Eine untaugliche Zweitfassung erkennt der Server selbst
NACHGEMESSEN AN DEN SECHS, DIE HEUTE NACHT LIVE ENTSTANDEN SIND --
vier tragen das Loch am Anfang, weil sie gebaut wurden, bevor es
bekannt war:
387,6 s Zeitachse fuer wenige Sekunden Ton
164,2 s
134,1 s
63,8 s
9,7 s (in Ordnung)
3,3 s (in Ordnung)
Sie gehoeren dem Dienstbenutzer; von aussen sind sie nicht
wegzuraeumen. Und mit „gibt es schon, dann weiter" waeren sie fuer
immer so geblieben.
EIN SCHALTER „alles neu ab Fassung 2" WAERE DIE BEQUEME LOESUNG und
die falsche: eine Zahl, die jemand hochzaehlen muss, wird beim
naechsten Mal vergessen. Gefragt wird deshalb die DATEI SELBST --
nach dem, was schiefgehen kann: Passt ihre Zeitachse zu der
Tonmenge, die sie traegt? AAC packt 1024 Abtastwerte in einen
Rahmen; Rahmenzahl mal 1024 durch die Abtastrate ist die echte
Laenge. Mehr als anderthalb Sekunden darueber heisst: Loch.
Was nicht passt, wird beim naechsten Nachruestlauf weggeraeumt und
neu gebaut -- einmal, denn danach passt es.
ZWEI FEHLER IN DIESER FUNKTION, BEIDE VON DER GEGENPROBE GEFUNDEN
Und beide waeren still geblieben:
1. ffprobe GIBT DIE WERTE IN SEINER REIHENFOLGE AUS, nicht in
meiner: erst `sample_rate`, dann `nb_frames`. Ich hatte
`[rahmen, rate, dauer]` destrukturiert und damit 48000 Rahmen
bei 95 Hz gerechnet -- eine „echte Laenge" von einer halben
Million Sekunden. Die Funktion haette JEDE Datei fuer tauglich
erklaert, immer. Gelesen wird jetzt mit Namen.
2. DER DATEINAME FEHLTE IM AUFRUF. ffprobe antwortete „You have to
specify one input file", der `catch` machte daraus ein
freundliches „taugt" -- und wieder sagte die Funktion zu allem
ja.
Das ist zweimal dieselbe Sorte: gruen und wertlos. Gefunden hat es
nicht das Lesen, sondern die Frage „kann sie ueberhaupt NEIN
sagen?". Genau dafuer gibt es die Gegenprobe.
WIE MAN EINE DATEI MIT LOCH NACHSTELLT -- UND WIE NICHT
`-itsoffset 40` war der naheliegende Weg und ergab 2,5 s: Der
MP4-Baukasten rechnet einen reinen Anfangsversatz wieder heraus.
Dasselbe mit `-copyts`, `-output_ts_offset` und `-muxdelay`. Was
bleibt, ist eine Zeitachse, die laenger ist als ihr Ton -- und das
stellt eine stumme Bildspur von 40 Sekunden her. Der Weg dorthin
ist ein anderer als bei `MediaRecorder`; die ZAHLEN, die die
Funktion liest, sind dieselben.
GEPRUEFT -- pruef-chat-anhaenge 164 -> 168 ok
eine Fassung mit zu langer Zeitachse nachgestellt (40,0 s)
und sie wird als untauglich erkannt
der Nachruestlauf ersetzt sie (40,0 s → 2,5 s)
und die neue gilt als tauglich — er wandelt sie nicht ewig weiter
Die letzte Zeile ist die zweite Gegenprobe: Ein Lauf, der bei jedem
Durchgang alles neu wandelt, waere eine Warnung, die immer kommt.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
===================================================================== */
|
||||
|
||||
import { existsSync, renameSync, unlinkSync } from "node:fs";
|
||||
import { execFile } from "node:child_process";
|
||||
import { execFile, execFileSync } from "node:child_process";
|
||||
import { delimiter, join, extname, basename, dirname } from "node:path";
|
||||
|
||||
/* Die Endung der Zweitfassung. AAC in MP4 — das Format, bei dem sich
|
||||
@@ -139,6 +139,73 @@ export function brauchtZweitfassung(pfad) {
|
||||
return extname(pfad).toLowerCase() !== ZWEITFASSUNG_ENDUNG;
|
||||
}
|
||||
|
||||
/** Taugt eine vorhandene Zweitfassung noch?
|
||||
*
|
||||
* ==== WARUM DIESE FRAGE UEBERHAUPT GESTELLT WIRD (03.10.2026) ====
|
||||
*
|
||||
* Die ersten sechs Zweitfassungen sind entstanden, BEVOR das Loch
|
||||
* am Anfang bekannt war (siehe `asetpts` weiter unten). Vier davon
|
||||
* tragen es: 387 s, 164 s, 134 s und 63,8 s Zeitachse fuer jeweils
|
||||
* wenige Sekunden Ton. Sie liegen auf dem Server und gehoeren dem
|
||||
* Dienstbenutzer — von aussen sind sie nicht wegzuraeumen.
|
||||
*
|
||||
* EIN SCHALTER „alles neu ab Fassung 2" WAERE DIE BEQUEME LOESUNG
|
||||
* und die falsche: Eine Zahl, die jemand hochzaehlen muss, wird
|
||||
* beim naechsten Mal vergessen. Gefragt wird deshalb die DATEI
|
||||
* SELBST — und zwar nach dem, was schiefgehen kann: Passt ihre
|
||||
* Zeitachse zu der Tonmenge, die sie traegt?
|
||||
*
|
||||
* GERECHNET WIRD AUS DEN TONRAHMEN. AAC packt 1024 Abtastwerte in
|
||||
* einen Rahmen; Rahmenzahl mal 1024 geteilt durch die Abtastrate
|
||||
* ist die echte Laenge. Weicht die angegebene Dauer um mehr als
|
||||
* anderthalb Sekunden nach OBEN ab, steckt ein Loch darin.
|
||||
*
|
||||
* DRITTER AUSGANG: Sagt ffprobe nichts (fehlt, oder die Datei gibt
|
||||
* die Rahmenzahl nicht her), gilt die Fassung als tauglich. Eine
|
||||
* Datei wegzuwerfen, weil man sie nicht beurteilen kann, waere die
|
||||
* schlechtere Antwort — und ein Lauf, der bei jedem Durchgang alles
|
||||
* neu wandelt, waere eine Warnung, die immer kommt. */
|
||||
export function zweitfassungTaugt(ziel) {
|
||||
const probe = ffprobePfad();
|
||||
if (!probe) return true;
|
||||
try {
|
||||
/* ==== MIT NAMEN LESEN, NICHT NACH REIHENFOLGE =================
|
||||
|
||||
Hier stand `-of default=…:nokey=1` und danach
|
||||
`const [rahmen, rate, dauer] = …`. ffprobe gibt die Werte aber
|
||||
in SEINER Reihenfolge aus, nicht in meiner: erst
|
||||
`sample_rate`, dann `nb_frames`. Die Rechnung lief damit mit
|
||||
48000 Rahmen und 95 Hz — und kam auf eine „echte Laenge" von
|
||||
einer halben Million Sekunden. Die Funktion haette JEDE Datei
|
||||
fuer tauglich erklaert, immer, und nie etwas gemeldet.
|
||||
|
||||
Das ist genau die Sorte, vor der die Hausregel warnt: Sie war
|
||||
gruen und wertlos. Aufgefallen ist es nicht beim Lesen,
|
||||
sondern beim Bauen der Gegenprobe — die Frage „kann sie
|
||||
ueberhaupt NEIN sagen?" hat sie gestellt. */
|
||||
const roh = execFileSync(probe, ["-v", "error", "-select_streams", "a:0",
|
||||
"-show_entries", "stream=nb_frames,sample_rate", "-show_entries", "format=duration",
|
||||
"-of", "default=noprint_wrappers=1", ziel], { encoding: "utf8", timeout: 20_000 });
|
||||
const wert = (name) => {
|
||||
const t = roh.match(new RegExp(`^${name}=(.+)$`, "m"));
|
||||
return t ? Number(t[1]) : NaN;
|
||||
};
|
||||
const rahmen = wert("nb_frames");
|
||||
const rate = wert("sample_rate");
|
||||
const dauer = wert("duration");
|
||||
/* DRITTER AUSGANG: Fehlt eine der drei Zahlen, ist die Frage
|
||||
nicht zu beantworten -- und „nicht beurteilbar" heisst hier
|
||||
„in Ordnung lassen". Eine Datei wegzuwerfen, weil man sie
|
||||
nicht messen kann, waere die schlechtere Antwort. */
|
||||
if (!Number.isFinite(rahmen) || !rahmen) return true;
|
||||
if (!Number.isFinite(rate) || !rate) return true;
|
||||
if (!Number.isFinite(dauer)) return true;
|
||||
/* AAC packt 1024 Abtastwerte in einen Rahmen. */
|
||||
const echt = (rahmen * 1024) / rate;
|
||||
return dauer <= echt + 1.5;
|
||||
} catch { return true; }
|
||||
}
|
||||
|
||||
/** Eine Tondatei in AAC/MP4 danebenlegen.
|
||||
*
|
||||
* Gibt ein Versprechen auf `true` (liegt jetzt da), `false` (ging
|
||||
|
||||
@@ -1214,17 +1214,23 @@ async function tonAufnehmen(seite, ms) {
|
||||
const ziel = zweitfassungPfad(quelle);
|
||||
|
||||
const probe = ffprobePfad();
|
||||
if (!probe) {
|
||||
melde(" -- KONNTE NICHT NACHSEHEN: ffprobe fehlt neben ffmpeg.");
|
||||
nichtNachsehbar++;
|
||||
} else {
|
||||
/* `frag` UND `spawnSyncFuerProbe` STEHEN HIER DRAUSSEN, weil
|
||||
der Abschnitt „untaugliche Fassung" weiter unten sie auch
|
||||
braucht. Innerhalb des else-Zweigs waeren sie dort nicht
|
||||
sichtbar -- und das faellt erst beim Laufen auf. */
|
||||
const frag = (datei, was) => {
|
||||
if (!probe) return "";
|
||||
try {
|
||||
return execFileSync(probe, ["-v", "error", "-show_entries", was,
|
||||
"-of", "default=noprint_wrappers=1:nokey=1", datei],
|
||||
{ encoding: "utf8" }).trim();
|
||||
} catch { return ""; }
|
||||
};
|
||||
const { spawnSync: spawnSyncFuerProbe } = await import("node:child_process");
|
||||
if (!probe) {
|
||||
melde(" -- KONNTE NICHT NACHSEHEN: ffprobe fehlt neben ffmpeg.");
|
||||
nichtNachsehbar++;
|
||||
} else {
|
||||
const codec = frag(ziel, "stream=codec_name");
|
||||
const codecAlt = frag(quelle, "stream=codec_name");
|
||||
const dauerNeu = Number(frag(ziel, "format=duration"));
|
||||
@@ -1245,7 +1251,6 @@ async function tonAufnehmen(seite, ms) {
|
||||
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
|
||||
@@ -1253,7 +1258,7 @@ async function tonAufnehmen(seite, ms) {
|
||||
Dateien 0 KiB -- die Pruefung wurde rot und hatte recht:
|
||||
Sie hat nichts gemessen. */
|
||||
const tonBytes = (datei) => {
|
||||
const r = spawnSync(ffmpegPfad(),
|
||||
const r = spawnSyncFuerProbe(ffmpegPfad(),
|
||||
["-nostdin", "-hide_banner", "-i", datei, "-f", "null", "-"],
|
||||
{ encoding: "utf8" });
|
||||
return String(r.stderr || "");
|
||||
@@ -1295,6 +1300,56 @@ async function tonAufnehmen(seite, ms) {
|
||||
ok(wiegt(ziel).size === kopf.bytes,
|
||||
` und was auf der Platte liegt, kommt auch heraus (${wiegt(ziel).size} Bytes)`);
|
||||
|
||||
/* ---- EINE UNTAUGLICHE FASSUNG WIRD ERSETZT ---------------
|
||||
|
||||
Die ersten sechs Zweitfassungen im Haus sind entstanden,
|
||||
BEVOR das Loch am Anfang bekannt war -- vier davon tragen
|
||||
es. Mit „gibt es schon, dann weiter" waeren sie fuer immer
|
||||
so geblieben. Geprueft wird deshalb, dass der Nachruestlauf
|
||||
eine Fassung, deren Zeitachse nicht zum Ton passt, ERSETZT.
|
||||
|
||||
NACHGESTELLT MIT EINER ECHTEN: Dieselbe Aufnahme wird von
|
||||
Hand mit dem alten Aufruf (ohne `asetpts`) und einem
|
||||
kuenstlichen Versatz gebaut -- so sieht sie aus wie die
|
||||
vier auf dem Server. */
|
||||
const { toeneNachruesten } = await import("./workspace-chat.js");
|
||||
const { zweitfassungTaugt } = await import("./helfer-ffmpeg.mjs");
|
||||
/* NICHT `weg2` NENNEN -- so hiess oben schon die Adresse der
|
||||
Zweitfassung, und `const` im selben Block macht daraus
|
||||
einen Absturz, BEVOR die obere Zeile ueberhaupt gelesen
|
||||
wird. Gefunden von der Pruefung selbst. */
|
||||
const { unlinkSync: dateiLoeschen } = await import("node:fs");
|
||||
dateiLoeschen(ziel);
|
||||
/* WIE MAN EINE DATEI MIT LOCH BAUT -- und wie nicht.
|
||||
`-itsoffset 40` war mein erster Versuch und ergab 2,5 s:
|
||||
Der MP4-Baukasten rechnet einen reinen Anfangsversatz
|
||||
wieder heraus. Was bleibt, ist eine Zeitachse, die laenger
|
||||
ist als der Ton, den sie traegt -- und genau das stellt
|
||||
eine stumme Bildspur von 40 Sekunden her. Der Weg dorthin
|
||||
ist ein anderer als bei `MediaRecorder`; die ZAHLEN, die
|
||||
`zweitfassungTaugt` liest, sind dieselben, und darum geht
|
||||
es hier. */
|
||||
spawnSyncFuerProbe(ffmpegPfad(), ["-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-f", "lavfi", "-i", "color=black:s=32x32:r=1:d=40", "-i", quelle,
|
||||
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac", "-b:a", "96k",
|
||||
"-f", "mp4", "-y", ziel]);
|
||||
const kaputtDauer = Number(frag(ziel, "format=duration"));
|
||||
ok(kaputtDauer > 30,
|
||||
`eine Fassung mit zu langer Zeitachse nachgestellt `
|
||||
+ `(${kaputtDauer.toFixed(1)} s)`);
|
||||
ok(zweitfassungTaugt(ziel) === false,
|
||||
" und sie wird als untauglich erkannt");
|
||||
await toeneNachruesten();
|
||||
const heilDauer = Number(frag(ziel, "format=duration"));
|
||||
ok(heilDauer > 0 && heilDauer < 10,
|
||||
`der Nachruestlauf ersetzt sie (${kaputtDauer.toFixed(1)} s → `
|
||||
+ `${heilDauer.toFixed(1)} s)`);
|
||||
/* GEGENPROBE: Die GUTE Fassung laesst er in Ruhe. Ohne sie
|
||||
koennte der Lauf bei jedem Durchgang alles neu wandeln --
|
||||
eine Warnung, die immer kommt. */
|
||||
ok(zweitfassungTaugt(ziel) === true,
|
||||
" und die neue gilt als tauglich — er wandelt sie nicht ewig weiter");
|
||||
|
||||
/* ---- UND BEIM LOESCHEN GEHEN BEIDE ------------------------
|
||||
Sonst bliebe bei jeder zurueckgenommenen Sprachnachricht
|
||||
eine m4a liegen -- ohne Zeile, die auf sie zeigt, und damit
|
||||
|
||||
@@ -47,7 +47,8 @@ import { liefereDatei } from "./helfer-ausliefern.mjs";
|
||||
helfer-ffmpeg.mjs. Sie ist der Grund, warum Miss sie ab heute
|
||||
hoeren kann (Support-Meldung #12). */
|
||||
import {
|
||||
zweitfassungBauen, zweitfassungPfad, brauchtZweitfassung, ZWEITFASSUNG_TYP,
|
||||
zweitfassungBauen, zweitfassungPfad, brauchtZweitfassung, zweitfassungTaugt,
|
||||
ZWEITFASSUNG_TYP,
|
||||
} from "./helfer-ffmpeg.mjs";
|
||||
|
||||
/** Eine Anhangsdatei entfernen -- MITSAMT ihrer zweiten Fassung.
|
||||
@@ -124,9 +125,32 @@ export async function toeneNachruesten(hoechstens = 20) {
|
||||
for (const z of zeilen) {
|
||||
if (gebaut >= hoechstens) break;
|
||||
if (!brauchtZweitfassung(z.anhang_datei)) continue;
|
||||
if (zweitfassungDa("ton", z.anhang_datei)) continue;
|
||||
const pfad = join(ANHANG_ORDNER, z.anhang_datei);
|
||||
try { statSync(pfad); } catch { continue; }
|
||||
/* ==== EINE UNTAUGLICHE FASSUNG WIRD ERSETZT (03.10.2026) ===
|
||||
|
||||
Hier stand nur „gibt es schon? dann weiter". Die ersten
|
||||
sechs Zweitfassungen sind aber entstanden, BEVOR das Loch am
|
||||
Anfang bekannt war -- vier davon tragen es (387 s, 164 s,
|
||||
134 s, 63,8 s Zeitachse fuer wenige Sekunden Ton). Mit „gibt
|
||||
es schon" waeren sie fuer immer so geblieben.
|
||||
|
||||
`zweitfassungTaugt` fragt die Datei selbst, nicht eine
|
||||
Versionsnummer: Passt ihre Zeitachse zu der Tonmenge, die
|
||||
sie traegt? Was nicht passt, wird weggeraeumt und neu
|
||||
gebaut -- einmal, denn danach passt es. */
|
||||
const ziel = zweitfassungPfad(pfad);
|
||||
let daUndGut = false;
|
||||
try {
|
||||
statSync(ziel);
|
||||
daUndGut = zweitfassungTaugt(ziel);
|
||||
if (!daUndGut) {
|
||||
unlinkSync(ziel);
|
||||
console.log(`[ton] Zweitfassung von ${z.anhang_datei} war unbrauchbar `
|
||||
+ "(Zeitachse passt nicht zum Ton) -- wird neu gebaut.");
|
||||
}
|
||||
} catch { /* es gibt sie noch nicht */ }
|
||||
if (daUndGut) continue;
|
||||
/* NACHEINANDER, nicht alle auf einmal: Zwanzig ffmpeg-Laeufe
|
||||
gleichzeitig nehmen dem Server genau die Rechenzeit weg, die
|
||||
er fuer die Seite braucht. */
|
||||
|
||||
Reference in New Issue
Block a user