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": {
ext: ".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": {
ext: ".opus",
+7 -11
View File
@@ -43,17 +43,13 @@ export function registerConvertAudio(app: FastifyInstance) {
];
case "wav":
return ["-i", inPath, "-vn", "-c:a", "pcm_s16le", out];
case "ogg":
return [
"-i",
inPath,
"-vn",
"-c:a",
"libvorbis",
"-b:a",
`${settings.bitrateKbps}k`,
out,
];
case "ogg": {
// libvorbis ABR (-b:a) fails with "encoder setup failed" when the bitrate is
// too high for the source sample rate (e.g. 8 kHz). Use quality VBR (-q:a),
// which adapts to the rate. Map bitrate -> quality (~bitrate/32: 192k -> q6).
const quality = (settings.bitrateKbps / 32).toFixed(1);
return ["-i", inPath, "-vn", "-c:a", "libvorbis", "-q:a", quality, out];
}
case "flac":
return ["-i", inPath, "-vn", "-c:a", "flac", out];
case "m4a":
+4 -2
View File
@@ -373,8 +373,10 @@ ENV PORT=1349 \
LOG_DIR=/data/logs \
TRUST_PROXY=true \
OIDC_ENABLED=false \
EXTERNAL_URL= \
COOKIE_SECRET=
EXTERNAL_URL=
# 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)
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}"
# 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 = {
"bundleId": bundle_id,
"version": manifest["imageVersion"],
@@ -368,6 +377,7 @@ bundle_meta = {
"imageVersion": manifest["imageVersion"],
"pythonVersion": py_ver,
"models": model_ids,
"extractedSize": extracted_size,
}
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);
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");
expect(res.statusCode).toBe(200);
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;
expect(outName.endsWith(".ogg")).toBe(true);
}, 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 }) => {
const issues = instrument(page);
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");
if (!dl.ok) {
bug({
@@ -1063,7 +1063,7 @@ test.describe("IMAGE: image-pad", () => {
test("target=16:9 -> output is 16:9 ratio", async ({ page }) => {
const issues = instrument(page);
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");
if (!dl.ok) {
bug({
@@ -2027,16 +2027,22 @@ test.describe("DOCUMENT: extract-pages", () => {
test.describe("DOCUMENT: compress-pdf", () => {
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);
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");
if (!dl.ok) {
bug({
tool: "compress-pdf",
setting: "preset",
value: "screen",
setting: "quality",
value: "20",
expected: "valid PDF",
actual: dl.error ?? "error",
});
@@ -2045,32 +2051,36 @@ test.describe("DOCUMENT: compress-pdf", () => {
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);
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");
if (!dl.ok)
bug({
tool: "compress-pdf",
setting: "preset",
value: "ebook",
setting: "quality",
value: "80",
expected: "success",
actual: dl.error ?? "error",
});
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);
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");
if (!dl.ok)
bug({
tool: "compress-pdf",
setting: "preset",
value: "printer",
setting: "targetSize",
value: "100KB",
expected: "success",
actual: dl.error ?? "error",
});