mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 \
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user