Initial OSS scaffold from Songs2VID (pre-strip)

This commit is contained in:
Songs2VID OSS
2026-08-03 06:15:05 +02:00
commit 935abfb22e
195 changed files with 25023 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
/**
* Watermark / branding helpers position math + FFmpeg-safe sanitization.
* Never interpolate unsanitized user text into filter graphs.
*/
import {
isWatermarkFontKey,
type WatermarkFontKey,
} from "./fonts";
export const WATERMARK_POSITIONS = [
"top-left",
"top-right",
"bottom-left",
"bottom-right",
"center",
] as const;
export type WatermarkPosition = (typeof WATERMARK_POSITIONS)[number];
export const WATERMARK_MODES = ["none", "default", "text", "logo"] as const;
export type WatermarkMode = (typeof WATERMARK_MODES)[number];
export type WatermarkSettings = {
mode: WatermarkMode;
text?: string | null;
logoPath?: string | null;
position: WatermarkPosition;
/** Pixel offset from the chosen anchor (0200). */
offsetX: number;
offsetY: number;
/**
* Typography (Pro / text mode).
* `system` = FFmpeg default; curated keys map to assets/fonts; `custom` uses fontPath.
*/
fontKey?: WatermarkFontKey;
/** Absolute/staging path to uploaded .ttf/.otf when fontKey === "custom". */
fontPath?: string | null;
};
export const DEFAULT_WATERMARK: WatermarkSettings = {
mode: "default",
text: null,
logoPath: null,
position: "bottom-right",
offsetX: 20,
offsetY: 20,
fontKey: "system",
fontPath: null,
};
export const WATERMARK_TEXT_MAX = 80;
export const WATERMARK_OFFSET_MIN = 0;
export const WATERMARK_OFFSET_MAX = 200;
export function isWatermarkPosition(v: unknown): v is WatermarkPosition {
return typeof v === "string" && (WATERMARK_POSITIONS as readonly string[]).includes(v);
}
export function isWatermarkMode(v: unknown): v is WatermarkMode {
return typeof v === "string" && (WATERMARK_MODES as readonly string[]).includes(v);
}
export function clampOffset(n: unknown, fallback = 20): number {
const v = typeof n === "number" ? n : Number(n);
if (!Number.isFinite(v)) return fallback;
return Math.max(WATERMARK_OFFSET_MIN, Math.min(WATERMARK_OFFSET_MAX, Math.round(v)));
}
/**
* Escape user text for FFmpeg drawtext.
* Strips control chars; escapes \, :, ', %, and [.
*/
export function sanitizeDrawtext(raw: string): string {
return raw
.slice(0, WATERMARK_TEXT_MAX)
.replace(/[\u0000-\u001f\u007f]/g, "")
.replace(/\\/g, "\\\\")
.replace(/:/g, "\\:")
.replace(/'/g, "\\'")
.replace(/%/g, "%%")
.replace(/\[/g, "\\[");
}
/** FFmpeg overlay=x:y expressions for a scaled watermark layer `[wm]`. */
export function overlayXy(
position: WatermarkPosition,
offsetX: number,
offsetY: number,
): { x: string; y: string } {
const ox = clampOffset(offsetX);
const oy = clampOffset(offsetY);
switch (position) {
case "top-left":
return { x: String(ox), y: String(oy) };
case "top-right":
return { x: `W-w-${ox}`, y: String(oy) };
case "bottom-left":
return { x: String(ox), y: `H-h-${oy}` };
case "center":
return { x: `(W-w)/2+${ox}`, y: `(H-h)/2+${oy}` };
case "bottom-right":
default:
return { x: `W-w-${ox}`, y: `H-h-${oy}` };
}
}
/** drawtext x/y for text watermarks. */
export function drawtextXy(
position: WatermarkPosition,
offsetX: number,
offsetY: number,
): { x: string; y: string } {
const ox = clampOffset(offsetX);
const oy = clampOffset(offsetY);
switch (position) {
case "top-left":
return { x: String(ox), y: String(oy) };
case "top-right":
return { x: `w-text_w-${ox}`, y: String(oy) };
case "bottom-left":
return { x: String(ox), y: `h-th-${oy}` };
case "center":
return { x: `(w-text_w)/2+${ox}`, y: `(h-th)/2+${oy}` };
case "bottom-right":
default:
return { x: `w-text_w-${ox}`, y: `h-th-${oy}` };
}
}
/**
* Build a drawtext filter segment (without leading comma).
* `fontfileEscaped` must already be sanitized via sanitizeFontfileForFilter.
*/
export function buildDrawtextFilter(opts: {
text: string;
fontSize: number;
fontColor?: string;
position: WatermarkPosition;
offsetX: number;
offsetY: number;
fontfileEscaped?: string | null;
}): string {
const text = sanitizeDrawtext(opts.text);
const { x, y } = drawtextXy(opts.position, opts.offsetX, opts.offsetY);
const color = opts.fontColor ?? "white@0.9";
const fontPart = opts.fontfileEscaped
? `:fontfile='${opts.fontfileEscaped}'`
: "";
return `drawtext=text='${text}'${fontPart}:fontsize=${opts.fontSize}:fontcolor=${color}:x=${x}:y=${y}`;
}
export function normalizeWatermarkSettings(
input: Partial<WatermarkSettings> | null | undefined,
includeWatermarkFallback: boolean,
): WatermarkSettings {
if (!input) {
return {
...DEFAULT_WATERMARK,
mode: includeWatermarkFallback ? "default" : "none",
};
}
const mode = isWatermarkMode(input.mode)
? input.mode
: includeWatermarkFallback
? "default"
: "none";
const fontKey = isWatermarkFontKey(input.fontKey) ? input.fontKey : "system";
return {
mode,
text: typeof input.text === "string" ? input.text.slice(0, WATERMARK_TEXT_MAX) : null,
logoPath: typeof input.logoPath === "string" ? input.logoPath : null,
position: isWatermarkPosition(input.position) ? input.position : "bottom-right",
offsetX: clampOffset(input.offsetX, 20),
offsetY: clampOffset(input.offsetY, 20),
fontKey,
fontPath: typeof input.fontPath === "string" ? input.fontPath : null,
};
}
/** True when settings go beyond Free-tier default branding toggle. */
export function requiresCustomWatermarkEntitlement(settings: WatermarkSettings): boolean {
if (settings.mode === "text" || settings.mode === "logo") return true;
if (settings.mode === "none") return false;
if (settings.position !== "bottom-right") return true;
if (settings.offsetX !== 20 || settings.offsetY !== 20) return true;
if (settings.fontKey && settings.fontKey !== "system") return true;
if (settings.fontPath) return true;
return false;
}
export const PREMIUM_REQUIRED_CODE = "PREMIUM_REQUIRED" as const;