Einstellungen und Zusatzdateien für den Schnitt ablegen

Intro, Outro, Backing Track und Voice-overs pro Clip laufen über
denselben Upload in Stücken wie die Clips. Die Schnitt-Einstellungen
werden feldweise geprüft; ffprobe akzeptiert jetzt auch reine Tonspuren.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Opus 5.5 committed 2026-09-29 10:48:53 +00:00
1 parent d7102d04a4
commit 72e4b8ab7a
6 files changed
+340 -45

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
import { badRequest } from './http-error.js';
export const TARGET_SECONDS = 10;
export function defaultSettings() {
return {
intro: { enabled: false, volume: 100 },
outro: { enabled: false, volume: 100 },
speedUp: true,
muteClips: true,
clipVolume: 100,
music: { enabled: false, volume: 60 },
voiceVolume: 100,
// Pro Clip-Index: Lautstärke des Voice-overs, 0–100
voiceVolumes: {},
// Clip-Indizes, die nicht ins Video sollen
excluded: [],
};
}
function volume(value, name) {
if (!Number.isInteger(value) || value < 0 || value > 100) {
throw badRequest(`${name}: Lautstärke muss zwischen 0 und 100 liegen.`);
}
return value;
}
function flag(value, name) {
if (typeof value !== 'boolean') throw badRequest(`${name}: ungültiger Wert.`);
return value;
}
function clipIndex(value, clipCount) {
const index = Number(value);
if (!Number.isInteger(index) || index < 0 || index >= clipCount) {
throw badRequest('Unbekannter Clip.');
}
return index;
}
// Übernimmt nur bekannte Felder aus patch und prüft jedes; der Rest bleibt.
export function mergeSettings(current, patch, clipCount) {
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
throw badRequest('Ungültige Einstellungen.');
}
const next = structuredClone({ ...defaultSettings(), ...current });
for (const part of ['intro', 'outro', 'music']) {
if (patch[part] === undefined) continue;
const value = patch[part];
if (!value || typeof value !== 'object') throw badRequest(`${part}: ungültiger Wert.`);
if (value.enabled !== undefined) next[part].enabled = flag(value.enabled, part);
if (value.volume !== undefined) next[part].volume = volume(value.volume, part);
}
for (const key of ['speedUp', 'muteClips']) {
if (patch[key] !== undefined) next[key] = flag(patch[key], key);
}
for (const key of ['clipVolume', 'voiceVolume']) {
if (patch[key] !== undefined) next[key] = volume(patch[key], key);
}
if (patch.voiceVolumes !== undefined) {
if (!patch.voiceVolumes || typeof patch.voiceVolumes !== 'object') {
throw badRequest('voiceVolumes: ungültiger Wert.');
}
for (const [key, value] of Object.entries(patch.voiceVolumes)) {
next.voiceVolumes[clipIndex(key, clipCount)] = volume(value, 'Voice-over');
}
}
if (patch.excluded !== undefined) {
if (!Array.isArray(patch.excluded)) throw badRequest('excluded: ungültiger Wert.');
next.excluded = [...new Set(patch.excluded.map((value) => clipIndex(value, clipCount)))].sort(
(a, b) => a - b,
);
}
return next;
}