fix: convert input buffer to PNG before passing to face landmarks Python sidecar

AVIF (and other Sharp-native formats) were written as raw bytes to a
.png temp file, causing PIL to fail with "cannot identify image file".
Every other AI module wrapper already converts via sharp().png().toBuffer()
before writing; face-landmarks was the only one that skipped this step.
This commit is contained in:
SnapOtter
2026-05-13 14:18:49 +08:00
parent 0b3f46407a
commit 648c8c12f5
4 changed files with 15 additions and 10 deletions
+3 -1
View File
@@ -1,6 +1,7 @@
import { unlink, writeFile } from "node:fs/promises"; import { unlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import sharp from "sharp";
import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js"; import { type ProgressCallback, parseStdoutJson, runPythonWithProgress } from "./bridge.js";
export interface FaceLandmarkPoint { export interface FaceLandmarkPoint {
@@ -33,7 +34,8 @@ export async function detectFaceLandmarks(
const inputPath = join(tmpdir(), `face_landmarks_${Date.now()}.png`); const inputPath = join(tmpdir(), `face_landmarks_${Date.now()}.png`);
try { try {
await writeFile(inputPath, inputBuffer); const pngBuffer = await sharp(inputBuffer).png().toBuffer();
await writeFile(inputPath, pngBuffer);
const { stdout } = await runPythonWithProgress( const { stdout } = await runPythonWithProgress(
"face_landmarks.py", "face_landmarks.py",
[inputPath, "unused", "{}"], [inputPath, "unused", "{}"],
+4 -3
View File
@@ -68,13 +68,14 @@ describe("detectFaceLandmarks", () => {
); );
}); });
it("writes input buffer directly without sharp conversion", async () => { it("converts input buffer to PNG before writing", async () => {
const sharp = (await import("sharp")).default;
await detectFaceLandmarks(FAKE_INPUT); await detectFaceLandmarks(FAKE_INPUT);
// face-landmarks.ts does NOT use sharp -- it writes inputBuffer directly expect(sharp).toHaveBeenCalledWith(FAKE_INPUT);
expect(writeFile).toHaveBeenCalledWith( expect(writeFile).toHaveBeenCalledWith(
expect.stringContaining("face_landmarks_"), expect.stringContaining("face_landmarks_"),
FAKE_INPUT, Buffer.from("mock-png-data"),
); );
}); });
+5 -3
View File
@@ -580,11 +580,13 @@ describe("detectFaceLandmarks", () => {
expect(result.landmarks).toBeNull(); expect(result.landmarks).toBeNull();
}); });
it("does not use sharp to convert to PNG (writes buffer directly)", async () => { it("converts input buffer to PNG before writing", async () => {
await detectFaceLandmarks(FAKE_INPUT); await detectFaceLandmarks(FAKE_INPUT);
// face-landmarks writes inputBuffer directly, no sharp conversion expect(writeFile).toHaveBeenCalledWith(
expect(writeFile).toHaveBeenCalledWith(expect.stringContaining("face_landmarks_"), FAKE_INPUT); expect.stringContaining("face_landmarks_"),
Buffer.from("mock-png-data"),
);
}); });
it("cleans up temp file in finally block", async () => { it("cleans up temp file in finally block", async () => {
+3 -3
View File
@@ -630,13 +630,13 @@ describe("detectFaceLandmarks", () => {
await expect(detectFaceLandmarks(INPUT_BUFFER)).rejects.toThrow("MediaPipe not found"); await expect(detectFaceLandmarks(INPUT_BUFFER)).rejects.toThrow("MediaPipe not found");
}); });
it("writes raw input buffer (no sharp conversion)", async () => { it("converts input buffer to PNG before writing", async () => {
mockParseStdoutJson.mockReturnValue({ success: true, faceDetected: false }); mockParseStdoutJson.mockReturnValue({ success: true, faceDetected: false });
await detectFaceLandmarks(INPUT_BUFFER); await detectFaceLandmarks(INPUT_BUFFER);
// face-landmarks writes inputBuffer directly, no sharp pipeline expect(mockSharp).toHaveBeenCalledWith(INPUT_BUFFER);
expect(mockWriteFile).toHaveBeenCalledWith(expect.any(String), INPUT_BUFFER); expect(mockWriteFile).toHaveBeenCalledWith(expect.any(String), Buffer.from("mock-png"));
}); });
}); });