Native Windows-Auslieferung statt Container
Docker faellt weg. Das Projekt hat keine npm-Abhaengigkeiten und Node fuehrt TypeScript direkt aus - der Container schrieb nur noch drei Programme fest, und dafuer braucht es keine Virtualisierung, sondern einen Ordner. Der Anlass war, dass ADR-0004 zwar `docker compose pull` versprach, es aber nie ein veroeffentlichtes Image gab. Wer installieren wollte, musste whisper.cpp aus dem Quelltext uebersetzen. Plattformschicht (packages/core/src/platform.ts): - Programme werden neben der Anwendung gesucht, nicht im PATH - Daten unter %LOCALAPPDATA%, damit Installation und Update ohne Administratorrechte auskommen - Schriften mitgeliefert: ohne fontsdir setzt libass wortlos eine Ersatzschrift ein - gemessen 720 statt 826 Pixel Zeilenbreite Windows-Eigenheiten, an echtem ffmpeg 8.1 durchprobiert: - Pfade im Filtergraphen brauchen alle drei Schritte (Vorwaertsschraegstriche, escapter Doppelpunkt, Anfuehrungszeichen); je zwei davon reichen nicht - `nice` gibt es nicht, ersetzt durch os.setPriority Auslieferung: - scripts/build-package.ts baut das Paket, laeuft auch unter Linux - install.ps1 ohne Administratorrechte, Autostart ueber den Aufgabenplaner - update legt neue Fassungen bereit; eingespielt wird beim naechsten Start, nie mitten im Stream - ZIP lesen und schreiben ohne Abhaengigkeiten (packages/core/src/zip.ts) Kein `irm ... | iex`-Einzeiler: git.zeasy.dev antwortete instanzweit mit einer Umleitung auf /user/login, und `iex` fuehrte die Anmeldeseite als PowerShell aus. Der Updater verfolgt Umleitungen jetzt nicht und hat keine erfundene Vorgabeadresse mehr. Ausserdem: backup.ps1 war an zwei Stellen dokumentiert, aber nie geschrieben. ADR-0007 ersetzt ADR-0004. 84 Tests, Typpruefung sauber. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhVSfMFdWnXKCv5JfLnArZ
This commit is contained in:
1 parent
71dd660d01
commit
5f6301268a
41 files changed
+2254
-312
No files matched your search
@@ -1,13 +1,15 @@
|
||||
// Aufrufe von ffmpeg und ffprobe als Kindprozesse.
|
||||
//
|
||||
// Zwei Dinge sind hier wichtiger als alles andere (ADR-0006):
|
||||
// 1. Der Prozess laeuft nachrangig (`nice`), damit das Encodieren dem
|
||||
// laufenden Stream keine Frames kostet.
|
||||
// 1. Der Prozess laeuft nachrangig (os.setPriority), damit das Encodieren
|
||||
// dem laufenden Stream keine Frames kostet.
|
||||
// 2. Er darf scheitern, ohne den Daemon mitzureissen - deshalb Zeitlimit,
|
||||
// sauberes Toeten und die letzten Zeilen von stderr in der Fehlermeldung.
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import os from 'node:os';
|
||||
import { logger } from '#core/logger.ts';
|
||||
import { binaries } from '#core/platform.ts';
|
||||
|
||||
const log = logger('ffmpeg');
|
||||
|
||||
@@ -16,8 +18,65 @@ export type RunOptions = {
|
||||
timeoutMs?: number;
|
||||
onProgress?: (seconds: number) => void;
|
||||
signal?: AbortSignal;
|
||||
/** Arbeitsverzeichnis. Erlaubt relative Dateinamen im Filtergraphen. */
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Blanke Programmnamen auf die mitgelieferten Dateien abbilden.
|
||||
*
|
||||
* Im Container lagen ffmpeg und whisper-cli im PATH. Nativ liegen sie neben
|
||||
* der Anwendung. Die Abbildung steht an dieser einen Stelle, damit die
|
||||
* Aufrufer weiter schlicht `run('ffmpeg', ...)` schreiben koennen.
|
||||
*/
|
||||
function resolveBinary(bin: string): string {
|
||||
if (bin === 'ffmpeg') return binaries.ffmpeg;
|
||||
if (bin === 'ffprobe') return binaries.ffprobe;
|
||||
if (bin === 'whisper-cli') return binaries.whisper;
|
||||
return bin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Einen Dateipfad so verpacken, dass ffmpeg ihn im Filtergraphen als *einen*
|
||||
* Wert liest.
|
||||
*
|
||||
* Das ist unter Windows die Stelle, an der es sonst reihenweise knallt:
|
||||
* C:\Users\... enthaelt einen Doppelpunkt, und der trennt in ffmpeg die
|
||||
* Filteroptionen voneinander. Rueckwaertsschraegstriche sind ausserdem
|
||||
* ffmpeg-Escapes und gehen unterwegs verloren.
|
||||
*
|
||||
* Es braucht **alle drei** Schritte. An echtem ffmpeg 8.1 durchprobiert:
|
||||
*
|
||||
* ass=filename=C:/x/s.ass -> "No option name near '/x/s.ass'"
|
||||
* ass=filename=C\:/x/s.ass -> ebenso
|
||||
* ass=filename='C:/x/s.ass' -> ebenso
|
||||
* ass=filename='C\:/x/s.ass' -> laeuft
|
||||
*
|
||||
* Anfuehrungszeichen allein schuetzen den Doppelpunkt also nicht, und das
|
||||
* Escape allein auch nicht.
|
||||
*/
|
||||
export function filterPath(file: string): string {
|
||||
const forward = file.replace(/\\/g, '/').replace(/:/g, '\\:');
|
||||
// Ein Anfuehrungszeichen laesst sich innerhalb der Gruppe nicht escapen:
|
||||
// Gruppe schliessen, Zeichen einzeln escapen, Gruppe wieder oeffnen. Bei
|
||||
// Benutzernamen wie O'Brien passiert das wirklich.
|
||||
return `'${forward.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filterausdruck zum Einbrennen einer ASS-Datei.
|
||||
*
|
||||
* `fontsdir` ist nicht optional im Sinne von "nice to have": im Container kam
|
||||
* DejaVu Sans aus dem Betriebssystem, Windows hat sie nicht. Fehlt der Ordner,
|
||||
* setzt libass wortlos eine Ersatzschrift ein und der Untertitel sieht anders
|
||||
* aus, als er soll - ohne jede Fehlermeldung.
|
||||
*/
|
||||
export function assFilter(assFile: string, fonts?: string): string {
|
||||
const parts = [`ass=filename=${filterPath(assFile)}`];
|
||||
if (fonts) parts.push(`fontsdir=${filterPath(fonts)}`);
|
||||
return parts.join(':');
|
||||
}
|
||||
|
||||
export class FfmpegError extends Error {
|
||||
code: number | null;
|
||||
tail: string;
|
||||
@@ -36,13 +95,22 @@ export function parseProgress(chunk: string): number | null {
|
||||
}
|
||||
|
||||
export function run(bin: string, args: string[], opts: RunOptions = {}): Promise<string> {
|
||||
const niceness = opts.niceness ?? 0;
|
||||
const cmd = niceness > 0 ? 'nice' : bin;
|
||||
const argv = niceness > 0 ? ['-n', String(niceness), bin, ...args] : args;
|
||||
const cmd = resolveBinary(bin);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
log.debug(`${cmd} ${argv.slice(0, 6).join(' ')} ...`);
|
||||
const child = spawn(cmd, argv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
log.debug(`${cmd} ${args.slice(0, 6).join(' ')} ...`);
|
||||
const child = spawn(cmd, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
||||
});
|
||||
|
||||
// Frueher lief das ueber `nice`, das es unter Windows nicht gibt.
|
||||
// os.setPriority kann beides und braucht keinen Hilfsprozess davor;
|
||||
// das Zurueckstufen darf jeder, nur das Hochstufen braucht Rechte.
|
||||
if (opts.niceness && child.pid !== undefined) {
|
||||
try { os.setPriority(child.pid, opts.niceness); }
|
||||
catch (err) { log.debug(`Prioritaet nicht setzbar: ${(err as Error).message}`); }
|
||||
}
|
||||
|
||||
let stdout = '';
|
||||
const errLines: string[] = [];
|
||||
|
||||
Reference in new issue
Block a user