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
+71
View File
@@ -0,0 +1,71 @@
/**
* Download curated watermark fonts into assets/fonts for FFmpeg drawtext.
* Run: node scripts/fetch-watermark-fonts.mjs
*/
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const outDir = path.join(__dirname, "..", "assets", "fonts");
/** Direct TTF URLs from Google Fonts GitHub (OFL). */
const FONTS = [
{
file: "Inter-Regular.ttf",
url: "https://github.com/google/fonts/raw/main/ofl/inter/Inter%5Bopsz%2Cwght%5D.ttf",
},
{
file: "Montserrat-Regular.ttf",
url: "https://github.com/google/fonts/raw/main/ofl/montserrat/Montserrat%5Bwght%5D.ttf",
},
{
file: "Roboto-Regular.ttf",
url: "https://github.com/google/fonts/raw/main/ofl/roboto/Roboto%5Bwdth%2Cwght%5D.ttf",
},
{
file: "Oswald-Regular.ttf",
url: "https://github.com/google/fonts/raw/main/ofl/oswald/Oswald%5Bwght%5D.ttf",
},
{
file: "PlayfairDisplay-Regular.ttf",
url: "https://github.com/google/fonts/raw/main/ofl/playfairdisplay/PlayfairDisplay%5Bwght%5D.ttf",
},
];
await fs.mkdir(outDir, { recursive: true });
for (const font of FONTS) {
const dest = path.join(outDir, font.file);
try {
await fs.access(dest);
console.log("skip (exists)", font.file);
continue;
} catch {
/* download */
}
console.log("fetch", font.file);
const res = await fetch(font.url, {
headers: { "User-Agent": "songs2vid-font-fetch/1.0" },
redirect: "follow",
});
if (!res.ok) {
console.error("FAILED", font.file, res.status);
continue;
}
const buf = Buffer.from(await res.arrayBuffer());
await fs.writeFile(dest, buf);
console.log("wrote", font.file, buf.length, "bytes");
}
await fs.writeFile(
path.join(outDir, "README.md"),
`# Watermark fonts
Curated TTF assets for FFmpeg \`drawtext\` (OFL via Google Fonts).
Refresh with \`node scripts/fetch-watermark-fonts.mjs\`.
`,
"utf8",
);
console.log("done →", outDir);
+103
View File
@@ -0,0 +1,103 @@
import assert from "node:assert/strict";
import {
BLUR_AMOUNT_MAX,
INVALID_LAYOUT_TEMPLATE_MESSAGE,
blurToBoxblur,
boxblurFilterSegment,
buildArtTrackFilterComplex,
clampBlurAmount,
clampTextPadding,
clampTitleArtistGap,
computeLayoutGeometry,
normalizeLayoutSettings,
requiresArtTrackLayoutEntitlement,
} from "../lib/layout";
import { sanitizeDrawtext } from "../lib/watermark";
function testEnumsAndClamp() {
assert.equal(clampBlurAmount(150), BLUR_AMOUNT_MAX);
assert.equal(clampBlurAmount(-1), 0);
assert.equal(clampTextPadding(999), 120);
assert.equal(clampTextPadding(1), 16);
assert.equal(clampTitleArtistGap(100), 64);
assert.equal(clampTitleArtistGap(-5), 0);
assert.equal(blurToBoxblur(0), null);
const mid = blurToBoxblur(50);
assert.ok(mid && mid.radius >= 1 && mid.power >= 1);
assert.ok(boxblurFilterSegment(60).includes("boxblur="));
assert.equal(boxblurFilterSegment(0), "");
}
function testNormalize() {
const classic = normalizeLayoutSettings(null);
assert.equal(classic.template, null);
assert.equal(classic.titleArtistGap, 10);
assert.equal(classic.textOffsetX, 0);
assert.equal(
normalizeLayoutSettings({
layout_template: "CENTERED_COMPACT",
blur_amount: 80,
title_artist_gap: 24,
text_offset_y: -20,
}).titleArtistGap,
24,
);
assert.throws(
() => normalizeLayoutSettings({ template: "NOT_A_TEMPLATE" }),
(err: Error) => err.message === INVALID_LAYOUT_TEMPLATE_MESSAGE,
);
assert.throws(
() => normalizeLayoutSettings({ template: "COVER_LEFT_TEXT_RIGHT", x: 10 }),
(err: Error) => err.message === INVALID_LAYOUT_TEMPLATE_MESSAGE,
);
assert.equal(
requiresArtTrackLayoutEntitlement(normalizeLayoutSettings({ template: null })),
false,
);
assert.equal(
requiresArtTrackLayoutEntitlement(
normalizeLayoutSettings({ template: "COVER_TOP_TEXT_BOTTOM" }),
),
true,
);
}
function testGeometryAndFilter() {
const geo = computeLayoutGeometry("COVER_TOP_TEXT_BOTTOM", 1920, 1080, 48, 16, 10, -5);
assert.ok(geo.coverMaxW > 1600, "top layout cover should be near full width");
assert.ok(geo.artistY !== geo.titleY);
const withGap = computeLayoutGeometry("COVER_LEFT_TEXT_RIGHT", 1280, 720, 40, 40, 0, 0);
const tight = computeLayoutGeometry("COVER_LEFT_TEXT_RIGHT", 1280, 720, 40, 0, 0, 0);
assert.ok(Number(withGap.artistY) - Number(withGap.titleY) > Number(tight.artistY) - Number(tight.titleY));
const fc = buildArtTrackFilterComplex({
width: 1280,
height: 720,
layout: {
template: "CENTERED_COMPACT",
blurAmount: 40,
blurOpacity: 70,
textPadding: 40,
titleArtistGap: 18,
textOffsetX: 5,
textOffsetY: -8,
},
titleEscaped: sanitizeDrawtext("Hello:World"),
artistEscaped: sanitizeDrawtext("Artist"),
});
assert.ok(fc.includes("split=2"));
assert.ok(fc.includes("blend=") || fc.includes("[blurred]"));
assert.ok(fc.includes("overlay="));
assert.ok(fc.includes("drawtext="));
assert.ok(fc.endsWith("[laid]"));
}
testEnumsAndClamp();
testNormalize();
testGeometryAndFilter();
console.log("layout.test.ts: ok");
+25
View File
@@ -0,0 +1,25 @@
import assert from "node:assert/strict";
import {
artistFontSizeForWidth,
scaleFontToPreview,
titleFontSizeForWidth,
watermarkFontSizeForWidth,
} from "../lib/preview-typography";
function testFontSizesMatchLayout() {
const width = 1280;
assert.equal(titleFontSizeForWidth(width), Math.max(22, Math.round(width * 0.032)));
assert.equal(artistFontSizeForWidth(width), Math.max(16, Math.round(width * 0.02)));
assert.equal(watermarkFontSizeForWidth(width), Math.max(22, Math.round(width * 0.028)));
}
function testPreviewScaling() {
const encodeWidth = 1920;
const titlePx = titleFontSizeForWidth(encodeWidth);
const previewPx = scaleFontToPreview(titlePx, encodeWidth);
assert.ok(previewPx > 0 && previewPx < titlePx);
}
testFontSizesMatchLayout();
testPreviewScaling();
console.log("preview-typography.test.ts: ok");
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Build multi-arch production image and optionally push to Docker Hub.
# Usage:
# ./scripts/release-cloud.sh # build locally (current arch)
# ./scripts/release-cloud.sh --push # build+push linux/amd64,linux/arm64
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
IMAGE="${DOCKER_IMAGE:-atakanozban/songs2vid}"
TAG="${DOCKER_TAG:-latest}"
FULL="${IMAGE}:${TAG}"
echo "==> Prisma generate"
npx prisma generate
echo "==> Typecheck / lint (optional soft)"
npm run lint || true
echo "==> Watermark unit checks"
npx tsx scripts/watermark.test.ts
if [[ "${1:-}" == "--push" ]]; then
echo "==> Multi-arch build+push ${FULL} (amd64,arm64)"
docker buildx create --name s2yt-builder --use 2>/dev/null || docker buildx use s2yt-builder
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t "$FULL" \
--push \
.
echo "Pushed ${FULL}. Deploy on songs2vid.com host with deploy/songs2vid/docker-compose.yml"
else
echo "==> Local docker build ${FULL}"
docker build -t "$FULL" .
echo "Built ${FULL}. Re-run with --push for Hub multi-arch."
fi
+53
View File
@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import { Plan } from "@prisma/client";
import {
resolveBurnedSongTitle,
resolveYouTubeTitle,
validateYouTubeTitle,
} from "../lib/titles";
function testYouTubeTitleResolution() {
assert.equal(
resolveYouTubeTitle({ title: "My Video", songTitle: "Song", artist: "Artist" }, Plan.FREE),
"My Video",
);
assert.equal(
resolveYouTubeTitle({ title: "", songTitle: "Song", artist: "Artist" }, Plan.FREE),
"",
);
assert.equal(
resolveYouTubeTitle({ title: "", songTitle: "Song", artist: "Artist" }, Plan.PREMIUM),
"Artist - Song",
);
assert.equal(
resolveYouTubeTitle({ title: "Custom", songTitle: "Song", artist: "Artist" }, Plan.PREMIUM),
"Custom",
);
assert.equal(
resolveYouTubeTitle({ title: "", songTitle: "Song", artist: "" }, Plan.PREMIUM),
"Song",
);
}
function testBurnedSongTitle() {
assert.equal(resolveBurnedSongTitle({ songTitle: "Track" }), "Track");
assert.equal(resolveBurnedSongTitle({ title: "YouTube only" }, "Fallback"), "Fallback");
}
function testValidation() {
assert.equal(validateYouTubeTitle({ title: "Ok" }, Plan.FREE), null);
assert.ok(validateYouTubeTitle({ title: "" }, Plan.FREE));
assert.equal(
validateYouTubeTitle({ title: "", songTitle: "S", artist: "A" }, Plan.PREMIUM),
null,
);
}
testYouTubeTitleResolution();
testBurnedSongTitle();
testValidation();
console.log("titles.test.ts: ok");
+89
View File
@@ -0,0 +1,89 @@
import assert from "node:assert/strict";
import { sanitizeFontfileForFilter } from "../lib/fonts";
import {
buildDrawtextFilter,
clampOffset,
drawtextXy,
overlayXy,
requiresCustomWatermarkEntitlement,
sanitizeDrawtext,
normalizeWatermarkSettings,
} from "../lib/watermark";
function testSanitize() {
const s = sanitizeDrawtext("Hi:there'100%[x]");
assert.ok(s.includes("\\:"), "colon escaped as \\\\:");
assert.ok(s.includes("\\'"), "quote escaped");
assert.equal(sanitizeDrawtext("a".repeat(200)).length, 80);
assert.equal(sanitizeDrawtext("ok\nline").includes("\n"), false);
}
function testPositions() {
assert.deepEqual(overlayXy("bottom-right", 20, 20), { x: "W-w-20", y: "H-h-20" });
assert.deepEqual(overlayXy("top-left", 10, 5), { x: "10", y: "5" });
assert.deepEqual(drawtextXy("center", 0, 0), {
x: "(w-text_w)/2+0",
y: "(h-th)/2+0",
});
}
function testEntitlements() {
assert.equal(
requiresCustomWatermarkEntitlement(
normalizeWatermarkSettings({ mode: "default" }, true),
),
false,
);
assert.equal(
requiresCustomWatermarkEntitlement(
normalizeWatermarkSettings({ mode: "text", text: "Brand" }, true),
),
true,
);
assert.equal(
requiresCustomWatermarkEntitlement(
normalizeWatermarkSettings({ mode: "default", position: "top-left" }, true),
),
true,
);
assert.equal(
requiresCustomWatermarkEntitlement(
normalizeWatermarkSettings({ mode: "default", fontKey: "inter" }, true),
),
true,
);
assert.equal(clampOffset(999), 200);
assert.equal(clampOffset(-5), 0);
}
function testDrawtextFontfile() {
const withFont = buildDrawtextFilter({
text: "Brand",
fontSize: 24,
position: "bottom-right",
offsetX: 20,
offsetY: 20,
fontfileEscaped: sanitizeFontfileForFilter("C:\\fonts\\My Font.ttf"),
});
assert.ok(withFont.includes("fontfile="), "fontfile present");
assert.ok(withFont.includes("C\\:/fonts/My Font.ttf") || withFont.includes("C\\:/fonts/My\\ Font.ttf") || withFont.includes("fontfile='"), "escaped path");
assert.ok(!withFont.includes("C:\\fonts"), "backslashes normalized");
const injection = sanitizeFontfileForFilter("/tmp/evil':drawtext=text='x");
assert.ok(injection.includes("\\'"), "quote escaped in font path");
const noFont = buildDrawtextFilter({
text: "Hi",
fontSize: 18,
position: "top-left",
offsetX: 0,
offsetY: 0,
});
assert.equal(noFont.includes("fontfile"), false);
}
testSanitize();
testPositions();
testEntitlements();
testDrawtextFontfile();
console.log("watermark.test.ts: ok");