fix(audio): low-samplerate ogg encode + post-2.0 QA hygiene

Quality-VBR ogg (libvorbis -q:a) fixes 8 kHz 'encoder setup failed' in both ogg paths; drop empty COOKIE_SECRET ENV (app auto-generates); emit real bundle extractedSize; fix stale image-pad/compress-pdf QA specs.
This commit is contained in:
SnapOtter
2026-06-20 10:41:50 +08:00
committed by GitHub
parent b847dcc2ab
commit 19d9ed181a
6 changed files with 63 additions and 29 deletions
+4 -1
View File
@@ -94,7 +94,10 @@ const AUDIO_OUTPUTS: Record<string, AudioOutput> = {
".ogg": { ".ogg": {
ext: ".ogg", ext: ".ogg",
contentType: "audio/ogg", contentType: "audio/ogg",
encodeArgs: ["-c:a", "libvorbis", "-b:a", "192k"], // Quality-based VBR, not a fixed bitrate: libvorbis fails with "encoder setup
// failed" when a high fixed bitrate (192k) is requested for a low sample rate
// (e.g. 8 kHz mono). -q:a 6 is ~192 kbps for normal audio and adapts to the rate.
encodeArgs: ["-c:a", "libvorbis", "-q:a", "6"],
}, },
".opus": { ".opus": {
ext: ".opus", ext: ".opus",
+7 -11
View File
@@ -43,17 +43,13 @@ export function registerConvertAudio(app: FastifyInstance) {
]; ];
case "wav": case "wav":
return ["-i", inPath, "-vn", "-c:a", "pcm_s16le", out]; return ["-i", inPath, "-vn", "-c:a", "pcm_s16le", out];
case "ogg": case "ogg": {
return [ // libvorbis ABR (-b:a) fails with "encoder setup failed" when the bitrate is
"-i", // too high for the source sample rate (e.g. 8 kHz). Use quality VBR (-q:a),
inPath, // which adapts to the rate. Map bitrate -> quality (~bitrate/32: 192k -> q6).
"-vn", const quality = (settings.bitrateKbps / 32).toFixed(1);
"-c:a", return ["-i", inPath, "-vn", "-c:a", "libvorbis", "-q:a", quality, out];
"libvorbis", }
"-b:a",
`${settings.bitrateKbps}k`,
out,
];
case "flac": case "flac":
return ["-i", inPath, "-vn", "-c:a", "flac", out]; return ["-i", inPath, "-vn", "-c:a", "flac", out];
case "m4a": case "m4a":
+4 -2
View File
@@ -373,8 +373,10 @@ ENV PORT=1349 \
LOG_DIR=/data/logs \ LOG_DIR=/data/logs \
TRUST_PROXY=true \ TRUST_PROXY=true \
OIDC_ENABLED=false \ OIDC_ENABLED=false \
EXTERNAL_URL= \ EXTERNAL_URL=
COOKIE_SECRET=
# COOKIE_SECRET is intentionally not baked in: the app auto-generates and persists one
# on first boot if unset (see apps/api/src/index.ts). Override via runtime env to pin it.
# NVIDIA Container Toolkit env vars (harmless on non-GPU systems) # NVIDIA Container Toolkit env vars (harmless on non-GPU systems)
ENV NVIDIA_VISIBLE_DEVICES=all \ ENV NVIDIA_VISIBLE_DEVICES=all \
+10
View File
@@ -361,6 +361,15 @@ model_ids = [m["id"] for m in bundle.get("models", [])]
py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
# Uncompressed bundle size: BUILD_DIR holds site-packages + models at this point.
# Surfaces the real extractedSize for docker/feature-manifest.json (used by the
# disk-space pre-check in install_feature.py); several manifest entries were 0 before.
extracted_size = sum(
os.path.getsize(os.path.join(root, name))
for root, _dirs, files in os.walk(os.environ["BUILD_DIR"])
for name in files
)
bundle_meta = { bundle_meta = {
"bundleId": bundle_id, "bundleId": bundle_id,
"version": manifest["imageVersion"], "version": manifest["imageVersion"],
@@ -368,6 +377,7 @@ bundle_meta = {
"imageVersion": manifest["imageVersion"], "imageVersion": manifest["imageVersion"],
"pythonVersion": py_ver, "pythonVersion": py_ver,
"models": model_ids, "models": model_ids,
"extractedSize": extracted_size,
} }
out_path = os.path.join(os.environ["BUILD_DIR"], "bundle.json") out_path = os.path.join(os.environ["BUILD_DIR"], "bundle.json")
+14 -1
View File
@@ -49,7 +49,7 @@ describe.skipIf(!ffmpegAvailable())("convert-audio (requires ffmpeg)", () => {
}, 60_000); }, 60_000);
it("converts mp3 to ogg and returns 200", async () => { it("converts mp3 to ogg and returns 200", async () => {
// Use mp3 fixture (44100 Hz) because libvorbis rejects the 8 kHz wav // mp3 fixture (44100 Hz); the 8 kHz wav -> ogg case is the regression test below.
const res = await runTool({ format: "ogg" }, MP3, "tiny.mp3"); const res = await runTool({ format: "ogg" }, MP3, "tiny.mp3");
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
const envelope = JSON.parse(res.body); const envelope = JSON.parse(res.body);
@@ -63,4 +63,17 @@ describe.skipIf(!ffmpegAvailable())("convert-audio (requires ffmpeg)", () => {
const outName = envelope.downloadUrl.split("/").pop() as string; const outName = envelope.downloadUrl.split("/").pop() as string;
expect(outName.endsWith(".ogg")).toBe(true); expect(outName.endsWith(".ogg")).toBe(true);
}, 60_000); }, 60_000);
it("converts 8 kHz wav to ogg (regression: libvorbis low samplerate)", async () => {
// tiny.wav is 8 kHz; a fixed bitrate (-b:a) made libvorbis "encoder setup failed".
// The ogg path now uses -q:a (quality VBR), which adapts to the sample rate.
const res = await runTool({ format: "ogg" }, WAV, "tiny.wav");
expect(res.statusCode).toBe(200);
const envelope = JSON.parse(res.body);
expect(envelope.downloadUrl).toBeDefined();
const dl = await testApp.app.inject({ method: "GET", url: envelope.downloadUrl });
expect(dl.statusCode).toBe(200);
expect(dl.rawPayload.length).toBeGreaterThan(100);
expect((envelope.downloadUrl.split("/").pop() as string).endsWith(".ogg")).toBe(true);
}, 60_000);
}); });
+24 -14
View File
@@ -1035,7 +1035,7 @@ test.describe("IMAGE: image-pad", () => {
test("target=1:1 -> output is square (max dim)", async ({ page }) => { test("target=1:1 -> output is square (max dim)", async ({ page }) => {
const issues = instrument(page); const issues = instrument(page);
await setupTool(page, "image-pad", IMG_200x150); await setupTool(page, "image-pad", IMG_200x150);
await selectOption(page, "image-pad-target", "1:1"); await clickButton(page, "1:1");
const dl = await processAndDownload(page, "image-pad"); const dl = await processAndDownload(page, "image-pad");
if (!dl.ok) { if (!dl.ok) {
bug({ bug({
@@ -1063,7 +1063,7 @@ test.describe("IMAGE: image-pad", () => {
test("target=16:9 -> output is 16:9 ratio", async ({ page }) => { test("target=16:9 -> output is 16:9 ratio", async ({ page }) => {
const issues = instrument(page); const issues = instrument(page);
await setupTool(page, "image-pad", IMG_200x150); await setupTool(page, "image-pad", IMG_200x150);
await selectOption(page, "image-pad-target", "16:9"); await clickButton(page, "16:9");
const dl = await processAndDownload(page, "image-pad"); const dl = await processAndDownload(page, "image-pad");
if (!dl.ok) { if (!dl.ok) {
bug({ bug({
@@ -2027,16 +2027,22 @@ test.describe("DOCUMENT: extract-pages", () => {
test.describe("DOCUMENT: compress-pdf", () => { test.describe("DOCUMENT: compress-pdf", () => {
test.setTimeout(TOOL_TIMEOUT * 2); test.setTimeout(TOOL_TIMEOUT * 2);
test("preset=screen -> output is valid PDF", async ({ page }) => { // compress-pdf uses the shared CompressControls (quality / target-size modes),
// not screen/ebook/printer ghostscript presets. These drive the actual UI.
test("quality mode (q=20) -> output is valid PDF", async ({ page }) => {
const issues = instrument(page); const issues = instrument(page);
await setupTool(page, "compress-pdf", PDF_3PAGE); await setupTool(page, "compress-pdf", PDF_3PAGE);
await selectOption(page, "cpdf-preset", "screen"); await page
.getByRole("button", { name: /quality/i })
.first()
.click();
await setSlider(page, "compress-quality", 20);
const dl = await processAndDownload(page, "compress-pdf", "long"); const dl = await processAndDownload(page, "compress-pdf", "long");
if (!dl.ok) { if (!dl.ok) {
bug({ bug({
tool: "compress-pdf", tool: "compress-pdf",
setting: "preset", setting: "quality",
value: "screen", value: "20",
expected: "valid PDF", expected: "valid PDF",
actual: dl.error ?? "error", actual: dl.error ?? "error",
}); });
@@ -2045,32 +2051,36 @@ test.describe("DOCUMENT: compress-pdf", () => {
expect(magicMatches(dl.buf, "pdf")).toBe(true); expect(magicMatches(dl.buf, "pdf")).toBe(true);
}); });
test("preset=ebook -> output is valid PDF", async ({ page }) => { test("quality mode (q=80) -> output is valid PDF", async ({ page }) => {
const issues = instrument(page); const issues = instrument(page);
await setupTool(page, "compress-pdf", PDF_3PAGE); await setupTool(page, "compress-pdf", PDF_3PAGE);
await selectOption(page, "cpdf-preset", "ebook"); await page
.getByRole("button", { name: /quality/i })
.first()
.click();
await setSlider(page, "compress-quality", 80);
const dl = await processAndDownload(page, "compress-pdf", "long"); const dl = await processAndDownload(page, "compress-pdf", "long");
if (!dl.ok) if (!dl.ok)
bug({ bug({
tool: "compress-pdf", tool: "compress-pdf",
setting: "preset", setting: "quality",
value: "ebook", value: "80",
expected: "success", expected: "success",
actual: dl.error ?? "error", actual: dl.error ?? "error",
}); });
expect(dl.ok).toBe(true); expect(dl.ok).toBe(true);
}); });
test("preset=printer -> output is valid PDF", async ({ page }) => { test("target-size mode -> output is valid PDF", async ({ page }) => {
const issues = instrument(page); const issues = instrument(page);
await setupTool(page, "compress-pdf", PDF_3PAGE); await setupTool(page, "compress-pdf", PDF_3PAGE);
await selectOption(page, "cpdf-preset", "printer"); await fillInput(page, "compress-target-size", 100);
const dl = await processAndDownload(page, "compress-pdf", "long"); const dl = await processAndDownload(page, "compress-pdf", "long");
if (!dl.ok) if (!dl.ok)
bug({ bug({
tool: "compress-pdf", tool: "compress-pdf",
setting: "preset", setting: "targetSize",
value: "printer", value: "100KB",
expected: "success", expected: "success",
actual: dl.error ?? "error", actual: dl.error ?? "error",
}); });