test(fixtures): regenerate Category A content as provably CC0 synthetics

Replace 13 content fixtures with deterministic, reproducible outputs
from gen-synthetic-content.mjs:

- QR codes (PNG/SVG/AVIF): qrcode lib encoding https://snapotter.com
- Barcodes (PNG/AVIF): Code 128B encoder for SNAPOTTER-TEST-123
- OCR text (English/Japanese PNG): Sharp SVG text rendering
- PDFs (2-page/6-page): minimal PDF byte generator
- Audio (MP3 with ID3 tags, 30s WAV): ffmpeg 440Hz sine wave
- Video (1s metadata, 30s hero): ffmpeg testsrc2 + sine tone

All outputs are CC0 with no third-party IP. The generator script is
idempotent and can be re-run to reproduce identical fixtures.
This commit is contained in:
SnapOtter
2026-06-20 01:04:18 +08:00
parent 1aa3f8e48f
commit 999240f20e
14 changed files with 349 additions and 3 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

+1 -3
View File
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="296" height="296">
<path d="M32,236v-28h56v56H32V236L32,236z M80,236v-20H40v40h40V236L80,236z M48,236v-12h24v24H48V236L48,236z M104,260v-4h-8v-16h8v-24h8v-8H96v-8H64v-8h8v-8H56v16h-8v-8H32v-8h16v-16h-8v8h-8v-16h16v8h8v8h8v-8h8v8h16v-8h-8v-8H56v-8h24v-8H56v-8h-8v8H32v-24h8v8h48v-8h-8v-8H64v8h-8v-8H40V96h16v16h8v-8h16v-8h8v8h-8v8h8v8h8v-16h8v-8h-8V72h16v8h8v8h-8v8h8v48h-16v-8h-8v8h-8v8h-8v8h8v8h8v8h16v-8h-8v-8h-8v-8h16v16h8v-16h8v16h8v-24h8v-8h8v8h-8v8h8v8h24v-8h-8v-8h-8v-16h-8v-8h8v-8h8v-8h-8v-8h-8v16h-8v-8h-8v8h-8v-8h8v-8h-8V72h-8v-8h8v8h8V40h8v16h16v-8h-8V32h16v8h-8v8h16v-8h8v-8h16v24h-16v-8h-8v16h8v24h8v-8h8v40h16v-8h-8V96h16v24h16v-8h-8V96h8v16h8v-8h16v16h-8v-8h-8v8h-8v24h8v8h-8v8h-16v8h-8v8h-16v-8h-8v-8h-8v16h8v8h24v8h16v-8h-8v-8h8v-8h8v8h8v8h8v-16h-8v-8h16v24h-8v16h8v16h-8v24h8v8h-24v16h-24v-8h16v-8h-16v-16h-8v16h-8v8h8v8h-16v-24h-8v16h-8v-8h-8v-32h8v24h8v-24h8v-16h-8v-8h-8v-8h8v-8h-8v-8h-8v32h8v8h-16v16h-8v16h8v8h-8v8h16v8h-16v-8h-8v-8h-8v16h-32V260L104,260z M128,248v-8h8v-24h-16v8h8v8h-16v8h-8v8h8v8h16V248L128,248z M240,240v-8h8v-16h8v-8h-8v-24h-8v24h8v8h-8v8h-8v24h8V240L240,240z M200,236v-4h-8v8h8V236L200,236z M152,220v-4h-8v8h8V220L152,220z M224,212v-12h-24v24h24V212L224,212z M208,212v-4h8v8h-8V212L208,212z M144,204v-4h16v-8h-16v-8h-8v8h8v8h-16v-8h-8v8h-8v-8h-8v-8h-8v-8h-8v8h-8v8h8v-8h8v8h8v8h8v8h32V204L144,204z M120,180v-4h-8v8h8V180L120,180z M160,176v-8h-16v8h8v8h8V176L160,176z M208,164v-4h-8v8h8V164L208,164z M224,156v-4h8v-24h-8v8h-8v8h-8v-8h-16v-8h-8v-8h8V96h-8v-8h-8v-8h-8v8h-8V64h8v8h8v-8h-8v-8h-8v8h-8v24h8v8h8v-8h8v24h-8v8h-8v8h8v16h8v-8h16v8h8v8h16v8h8V156L224,156z M216,148v-4h8v8h-8V148L216,148z M88,140v-4h8v-8h-8v8h-8v8h8V140L88,140z M112,124v-4h-8v8h8V124L112,124z M112,84v-4h-8v8h8V84L112,84z M144,80v-8h-8v16h8V80L144,80z M192,44v-4h-8v8h8V44L192,44z M256,260v-4h8v8h-8V260L256,260z M256,144v-8h-8v-8h8v8h8v16h-8V144L256,144z M32,60V32h56v56H32V60L32,60zM80,60V40H40v40h40V60L80,60z M48,60V48h24v24H48V60L48,60z M208,60V32h56v56h-56V60L208,60z M256,60V40h-40v40h40V60L256,60zM224,60V48h24v24h-24V60L224,60z M96,60v-4h8v8h-8V60L96,60z M112,52v-4h-8V32h8v8h8v-8h8v8h-8v16h-8V52L112,52z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="296" height="296" viewBox="0 0 29 29" shape-rendering="crispEdges"><path fill="#ffffff" d="M0 0h29v29H0z"/><path stroke="#000000" d="M2 2.5h7m1 0h1m3 0h1m1 0h1m1 0h1m1 0h7M2 3.5h1m5 0h1m1 0h2m1 0h2m5 0h1m5 0h1M2 4.5h1m1 0h3m1 0h1m1 0h2m2 0h2m2 0h1m1 0h1m1 0h3m1 0h1M2 5.5h1m1 0h3m1 0h1m4 0h1m1 0h1m4 0h1m1 0h3m1 0h1M2 6.5h1m1 0h3m1 0h1m1 0h1m1 0h2m2 0h1m1 0h1m1 0h1m1 0h3m1 0h1M2 7.5h1m5 0h1m2 0h2m1 0h2m4 0h1m5 0h1M2 8.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M12 9.5h4m2 0h1M2 10.5h1m2 0h6m1 0h1m1 0h1m1 0h4m2 0h1m1 0h3M2 11.5h1m2 0h2m2 0h3m2 0h4m3 0h5M2 12.5h1m1 0h1m1 0h1m1 0h3m2 0h1m1 0h1m1 0h1m3 0h1m1 0h1m2 0h1M2 13.5h1m6 0h1m1 0h1m1 0h1m2 0h1m2 0h1m3 0h4M2 14.5h2m1 0h4m1 0h1m1 0h1m1 0h1m2 0h1m2 0h1m5 0h1M2 15.5h1m1 0h2m1 0h1m2 0h1m5 0h3m3 0h1m2 0h1M2 16.5h9m2 0h6m1 0h1m1 0h5M2 17.5h1m1 0h3m2 0h1m2 0h2m5 0h3m1 0h2m1 0h1M2 18.5h1m5 0h1m7 0h7m1 0h2M10 19.5h6m1 0h2m3 0h1m1 0h2M2 20.5h7m1 0h1m1 0h2m4 0h1m1 0h1m1 0h1m3 0h1M2 21.5h1m5 0h1m1 0h1m1 0h1m1 0h1m1 0h3m3 0h1m2 0h2M2 22.5h1m1 0h3m1 0h1m1 0h1m3 0h9m2 0h2M2 23.5h1m1 0h3m1 0h1m1 0h2m4 0h2m1 0h2m4 0h2M2 24.5h1m1 0h3m1 0h1m3 0h1m1 0h1m1 0h1m1 0h1m3 0h5M2 25.5h1m5 0h1m2 0h1m4 0h1m2 0h4m1 0h3M2 26.5h7m1 0h1m1 0h1m2 0h2m1 0h3m2 0h1m2 0h1"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.
+348
View File
@@ -0,0 +1,348 @@
#!/usr/bin/env node
// gen-synthetic-content.mjs -- Regenerates Category A & B content fixtures as CC0.
//
// Category A: Deterministically regenerable (QR codes, barcodes, OCR text, PDFs,
// synthetic audio/video).
// Category B: Replaces copyrighted content (Simpsons GIF, ConvertICO SVG,
// Shutterstock stock photo) with project-owned CC0 alternatives.
//
// All output is provably CC0 (generated by project scripts, no third-party IP).
// Usage: node tests/fixtures/gen-synthetic-content.mjs
//
// Dependencies (resolved from apps/api/node_modules):
// sharp, qrcode
// System deps on PATH:
// ffmpeg
import { writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
const __dirname = dirname(fileURLToPath(import.meta.url));
const CONTENT = join(__dirname, "content");
const require = createRequire(join(__dirname, "../../apps/api/package.json"));
const sharp = require("sharp");
const QRCode = require("qrcode");
// ── Code 128B barcode encoder ──────────────────────────────
// Standard Code 128 bar patterns (11 modules each, stop = 13 modules).
// Each string is a binary pattern: 1 = dark bar, 0 = light space.
const CODE128 = [
"11011001100","11001101100","11001100110","10010011000","10010001100",
"10001001100","10011001000","10011000100","10001100100","11001001000",
"11001000100","11000100100","10110011100","10011011100","10011001110",
"10111001100","10011101100","10011100110","11001110010","11001011100",
"11001001110","11011100100","11001110100","11101101110","11101001100",
"11100101100","11100100110","11101100100","11100110100","11100110010",
"11011011000","11011000110","11000110110","10100011000","10001011000",
"10001000110","10110001000","10001101000","10001100010","11010001000",
"11000101000","11000100010","10110111000","10110001110","10001101110",
"10111011000","10111000110","10001110110","11101110110","11010001110",
"11000101110","11011101000","11011100010","11011101110","11101011000",
"11101000110","11100010110","11101101000","11101100010","11100011010",
"11101111010","11001000010","11110001010","10100110000","10100001100",
"10010110000","10010000110","10000101100","10000100110","10110010000",
"10110000100","10011010000","10011000010","10000110100","10000110010",
"11000010010","11001010000","11110111010","11000010100","10001111010",
"10100111100","10010111100","10010011110","10111100100","10011110100",
"10011110010","11110100100","11110010100","11110010010","11011011110",
"11011110110","11110110110","10101111000","10100011110","10001011110",
"10111101000","10111100010","11110101000","11110100010","10111011110",
"10111101110","11101011110","11110101110",
"11010000100", // 103 Start A
"11010010000", // 104 Start B
"11010011100", // 105 Start C
];
const CODE128_STOP = "1100011101011";
function encodeCode128B(text) {
const START_B = 104;
const values = [START_B];
let checksum = START_B;
for (let i = 0; i < text.length; i++) {
const v = text.charCodeAt(i) - 32;
values.push(v);
checksum += v * (i + 1);
}
values.push(checksum % 103);
let bits = "";
for (const v of values) bits += CODE128[v];
bits += CODE128_STOP;
return bits;
}
function barcodeSvg(text, width, height) {
const bits = encodeCode128B(text);
const barH = height - 40;
const quietZone = 20;
const barAreaW = width - 2 * quietZone;
const unitW = barAreaW / bits.length;
const bars = [];
for (let i = 0; i < bits.length; i++) {
if (bits[i] === "1") {
bars.push(`<rect x="${(quietZone + i * unitW).toFixed(2)}" y="10" width="${unitW.toFixed(2)}" height="${barH}" fill="black"/>`);
}
}
return `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<rect width="${width}" height="${height}" fill="white"/>
${bars.join("\n ")}
<text x="${width / 2}" y="${height - 8}" text-anchor="middle" font-family="monospace, Courier" font-size="16" fill="black">${text}</text>
</svg>`;
}
// ── Minimal PDF generator ──────────────────────────────────
function generatePdf(pageCount) {
const objects = [];
let nextObj = 1;
const addObj = (content) => {
const id = nextObj++;
objects.push({ id, content });
return id;
};
const catalogId = addObj(null);
const pagesId = addObj(null);
const fontId = addObj("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>");
const pageIds = [];
for (let i = 1; i <= pageCount; i++) {
const stream = `BT /F1 24 Tf 72 700 Td (Page ${i}) Tj ET`;
const sId = addObj(`<< /Length ${stream.length} >>\nstream\n${stream}\nendstream`);
const pId = addObj(
`<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 612 792] ` +
`/Contents ${sId} 0 R /Resources << /Font << /F1 ${fontId} 0 R >> >> >>`,
);
pageIds.push(pId);
}
objects[0].content = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`;
objects[1].content =
`<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] /Count ${pageCount} >>`;
let pdf = "%PDF-1.7\n%\x81\x81\x81\x81\n\n";
const offsets = [];
for (const obj of objects) {
offsets.push(pdf.length);
pdf += `${obj.id} 0 obj\n${obj.content}\nendobj\n\n`;
}
const xrefOffset = pdf.length;
pdf += `xref\n0 ${objects.length + 1}\n`;
pdf += "0000000000 65535 f \n";
for (const off of offsets) {
pdf += `${String(off).padStart(10, "0")} 00000 n \n`;
}
pdf += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\n`;
pdf += `startxref\n${xrefOffset}\n%%EOF\n`;
return Buffer.from(pdf, "binary");
}
// ── SVG logo (project-owned, replaces ConvertICO brand) ───
function projectSvg() {
return `<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>SnapOtter Test Fixture</title>
<desc>CC0 geometric test SVG generated by gen-synthetic-content.mjs</desc>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="400" y2="400">
<stop stop-color="#E07832"/>
<stop offset="1" stop-color="#C06520"/>
</linearGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
<rect width="400" height="400" fill="#1a1210"/>
<circle cx="200" cy="180" r="100" fill="url(#bg)" filter="url(#glow)"/>
<ellipse cx="200" cy="180" rx="60" ry="40" fill="#1a1210" opacity="0.3"/>
<circle cx="170" cy="160" r="12" fill="white"/>
<circle cx="230" cy="160" r="12" fill="white"/>
<circle cx="170" cy="160" r="5" fill="#1a1210"/>
<circle cx="230" cy="160" r="5" fill="#1a1210"/>
<ellipse cx="200" cy="195" rx="15" ry="8" fill="#1a1210" opacity="0.4"/>
<text x="200" y="320" font-family="Helvetica, Arial, sans-serif" font-size="28" font-weight="bold" text-anchor="middle" fill="#F0EBE4">TEST FIXTURE</text>
<text x="200" y="350" font-family="Helvetica, Arial, sans-serif" font-size="14" text-anchor="middle" fill="#6B6560">CC0 - Generated Asset</text>
</svg>`;
}
// ── Multi-face placeholder (replaces Shutterstock photo) ──
function multiFaceSvg(w, h) {
const cols = 5;
const rows = 2;
const cw = w / cols;
const ch = h / rows;
const colors = [
"#D4956A", "#C2885E", "#E8B88A", "#A67853", "#BF9570",
"#D9A87C", "#CB9068", "#E5C49E", "#B08060", "#C89878",
];
const shapes = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const cx = cw * c + cw / 2;
const cy = ch * r + ch / 2;
const rx = cw * 0.3;
const ry = ch * 0.38;
const idx = r * cols + c;
shapes.push(`<ellipse cx="${cx.toFixed(0)}" cy="${cy.toFixed(0)}" rx="${rx.toFixed(0)}" ry="${ry.toFixed(0)}" fill="${colors[idx]}" stroke="#888" stroke-width="0.5"/>`);
shapes.push(`<circle cx="${(cx - rx * 0.3).toFixed(0)}" cy="${(cy - ry * 0.15).toFixed(0)}" r="3" fill="#333"/>`);
shapes.push(`<circle cx="${(cx + rx * 0.3).toFixed(0)}" cy="${(cy - ry * 0.15).toFixed(0)}" r="3" fill="#333"/>`);
}
}
return `<svg width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">
<rect width="${w}" height="${h}" fill="#d0d0d0"/>
${shapes.join("\n ")}
</svg>`;
}
// ── ffmpeg helper (all args are hardcoded constants) ───────
function ff(args, label) {
try {
execSync(`ffmpeg ${args}`, { stdio: "pipe" });
console.log(` ${label}`);
} catch (e) {
console.error(` FAILED: ${label}`, e.stderr?.toString().slice(-300));
throw e;
}
}
// ── Main ───────────────────────────────────────────────────
async function main() {
console.log("Regenerating Category A & B content fixtures...\n");
// ── QR codes (A) ──
console.log("QR codes:");
const qrData = "https://snapotter.com";
const qrPng = await QRCode.toBuffer(qrData, {
width: 400, margin: 2, color: { dark: "#000000", light: "#ffffff" },
});
writeFileSync(join(CONTENT, "qr-code.png"), qrPng);
console.log(" qr-code.png");
const qrSvg = await QRCode.toString(qrData, { type: "svg", width: 296, margin: 2 });
writeFileSync(join(CONTENT, "qr-code.svg"), qrSvg);
console.log(" qr-code.svg");
const qrAvif = await sharp(qrPng).resize(400, 400).avif({ quality: 80 }).toBuffer();
writeFileSync(join(CONTENT, "qr-code.avif"), qrAvif);
console.log(" qr-code.avif");
// ── Barcodes (A) ──
console.log("Barcodes:");
const bcText = "SNAPOTTER-TEST-123";
const bcSvgStr = barcodeSvg(bcText, 699, 152);
const bcPng = await sharp(Buffer.from(bcSvgStr)).png().toBuffer();
writeFileSync(join(CONTENT, "barcode.png"), bcPng);
console.log(" barcode.png");
const bcAvif = await sharp(Buffer.from(bcSvgStr)).avif({ quality: 80 }).toBuffer();
writeFileSync(join(CONTENT, "barcode.avif"), bcAvif);
console.log(" barcode.avif");
// ── OCR text images (A) ──
console.log("OCR text images:");
const ocrEngSvg = `<svg width="800" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="800" height="200" fill="white"/>
<text x="400" y="115" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="42" font-weight="bold" fill="black">The quick brown fox 12345</text>
</svg>`;
const ocrEngPng = await sharp(Buffer.from(ocrEngSvg)).png().toBuffer();
writeFileSync(join(CONTENT, "ocr-clean.png"), ocrEngPng);
console.log(" ocr-clean.png");
const jpText =
"日本語の表記においては,漢字や仮名だけで" +
"なく,ローマ字やアラビア数字,さらに句読" +
"点や括弧類などの記述記号を用いる。これら" +
"を組み合わせて表す日本語の文書では,表記" +
"上における種々の問題がある。";
const chars = [...jpText];
const lineLen = Math.ceil(chars.length / 6);
const lines = [];
for (let i = 0; i < chars.length; i += lineLen) {
lines.push(chars.slice(i, i + lineLen).join(""));
}
const jpSvg = `<svg width="480" height="172" xmlns="http://www.w3.org/2000/svg">
<rect width="480" height="172" fill="white"/>
${lines.map((line, i) => `<text x="16" y="${30 + i * 28}" font-family="Hiragino Sans, Hiragino Kaku Gothic Pro, sans-serif" font-size="20" fill="black">${line}</text>`).join("\n ")}
</svg>`;
const jpPng = await sharp(Buffer.from(jpSvg)).png().toBuffer();
writeFileSync(join(CONTENT, "ocr-japanese.png"), jpPng);
console.log(" ocr-japanese.png");
// ── PDFs (A) ──
console.log("PDFs:");
writeFileSync(join(CONTENT, "alt-2page.pdf"), generatePdf(2));
console.log(" alt-2page.pdf");
writeFileSync(join(CONTENT, "multipage-6.pdf"), generatePdf(6));
console.log(" multipage-6.pdf");
// ── SVG logo (B: replaces ConvertICO brand) ──
console.log("SVG logo (replacing ConvertICO brand):");
writeFileSync(join(CONTENT, "svg-logo.svg"), projectSvg());
console.log(" svg-logo.svg");
// ── Multi-face placeholder (B: replaces Shutterstock photo) ──
console.log("Multi-face placeholder (replacing Shutterstock stock photo):");
const mfSvg = multiFaceSvg(433, 280);
const mfWebp = await sharp(Buffer.from(mfSvg)).webp({ quality: 80 }).toBuffer();
writeFileSync(join(CONTENT, "multi-face.webp"), mfWebp);
console.log(" multi-face.webp");
// ── Animated GIF (B: replaces copyrighted Simpsons clip) ──
console.log("Animated GIF (replacing copyrighted Simpsons clip):");
const gifOut = join(CONTENT, "animated-simpsons.gif");
ff(
`-f lavfi -i "testsrc=duration=3:size=320x320:rate=10" -pix_fmt rgb8 -loop 0 -y "${gifOut}"`,
"animated-simpsons.gif",
);
// ── Synthetic audio/video (A) ──
console.log("Synthetic audio/video:");
ff(
`-f lavfi -i "sine=frequency=440:duration=1.2:sample_rate=8000" ` +
`-c:a libmp3lame -b:a 64k -ar 8000 -ac 1 ` +
`-metadata title="Test Song" -metadata artist="Test Artist" ` +
`-metadata album="Test Album" -metadata date="2026" ` +
`-metadata genre="Electronic" -metadata track="1/10" ` +
`-y "${join(CONTENT, "audio-with-tags.mp3")}"`,
"audio-with-tags.mp3",
);
ff(
`-f lavfi -i "color=c=0xE07832:s=64x64:d=1:rate=16" ` +
`-c:v libx264 -pix_fmt yuv420p -movflags +faststart ` +
`-y "${join(CONTENT, "video-with-meta.mp4")}"`,
"video-with-meta.mp4",
);
ff(
`-f lavfi -i "testsrc2=duration=30:size=640x360:rate=24" ` +
`-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` +
`-c:v libx264 -pix_fmt yuv420p -preset ultrafast ` +
`-c:a aac -b:a 128k -ac 1 -movflags +faststart ` +
`-y "${join(CONTENT, "media-30s.mp4")}"`,
"media-30s.mp4",
);
ff(
`-f lavfi -i "sine=frequency=440:duration=30:sample_rate=44100" ` +
`-ac 1 -c:a pcm_s16le ` +
`-y "${join(CONTENT, "media-30s.wav")}"`,
"media-30s.wav",
);
console.log("\nDone. Run gen-manifest.mjs to re-stamp hashes.");
}
main().catch((e) => {
console.error("FATAL:", e);
process.exit(1);
});