import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("../../../packages/ai/src/bridge.js", () => ({ runPythonWithProgress: vi.fn(), parseStdoutJson: vi.fn(), })); import { parseStdoutJson, runPythonWithProgress } from "../../../packages/ai/src/bridge.js"; import { transcribeAudio } from "../../../packages/ai/src/transcription.js"; const FAKE_AUDIO = "/tmp/test-audio/input.wav"; beforeEach(() => { vi.clearAllMocks(); vi.mocked(runPythonWithProgress).mockResolvedValue({ stdout: '{"success":true,"language":"en","segments":[{"start":0.0,"end":1.5,"text":"Hello"}],"text":"Hello"}', stderr: "", }); vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", segments: [{ start: 0.0, end: 1.5, text: "Hello" }], text: "Hello", }); }); afterEach(() => { vi.restoreAllMocks(); }); describe("transcribeAudio", () => { describe("request serialization", () => { it("calls transcribe.py with input path and options JSON", async () => { await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(runPythonWithProgress).toHaveBeenCalledWith( "transcribe.py", [FAKE_AUDIO, JSON.stringify({ language: "auto", task: "transcribe" })], expect.objectContaining({ timeout: 30 * 60_000 }), ); }); it("serializes a specific language", async () => { await transcribeAudio(FAKE_AUDIO, { language: "de" }); const args = vi.mocked(runPythonWithProgress).mock.calls[0][1]; expect(JSON.parse(args[1])).toEqual({ language: "de", task: "transcribe" }); }); it("always includes task: transcribe", async () => { await transcribeAudio(FAKE_AUDIO, { language: "ja" }); const args = vi.mocked(runPythonWithProgress).mock.calls[0][1]; const parsed = JSON.parse(args[1]); expect(parsed.task).toBe("transcribe"); }); it("uses 30 minute timeout", async () => { await transcribeAudio(FAKE_AUDIO, { language: "auto" }); const options = vi.mocked(runPythonWithProgress).mock.calls[0][2]; expect(options.timeout).toBe(30 * 60_000); }); }); describe("segment key mapping", () => { it("maps python start/end to startS/endS", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", segments: [ { start: 0.0, end: 1.5, text: "Hello" }, { start: 2.0, end: 4.123, text: "World" }, ], text: "Hello World", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.segments).toEqual([ { startS: 0.0, endS: 1.5, text: "Hello" }, { startS: 2.0, endS: 4.123, text: "World" }, ]); }); it("trims segment text", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", segments: [{ start: 0, end: 1, text: " padded text " }], text: "padded text", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.segments[0].text).toBe("padded text"); }); it("defaults missing start/end to 0", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", segments: [{ text: "no timestamps" }], text: "no timestamps", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.segments[0]).toEqual({ startS: 0, endS: 0, text: "no timestamps" }); }); it("defaults missing text to empty string", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", segments: [{ start: 0, end: 1 }], text: "", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.segments[0].text).toBe(""); }); }); describe("defensive parsing", () => { it("returns empty segments when segments field is missing", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", text: "Hello", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.segments).toEqual([]); }); it("returns empty segments when segments is null", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", segments: null, text: "Hello", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.segments).toEqual([]); }); it("returns empty segments when segments is not an array", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", segments: "not-an-array", text: "Hello", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.segments).toEqual([]); }); it("defaults language to en when missing", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, segments: [], text: "", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.language).toBe("en"); }); it("defaults text to empty string when missing", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "en", segments: [], }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.text).toBe(""); }); }); describe("response parsing", () => { it("returns language, text, and segments", async () => { const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result).toEqual({ language: "en", text: "Hello", segments: [{ startS: 0.0, endS: 1.5, text: "Hello" }], }); }); it("returns detected language", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ success: true, language: "fr", segments: [], text: "Bonjour", }); const result = await transcribeAudio(FAKE_AUDIO, { language: "auto" }); expect(result.language).toBe("fr"); }); }); describe("error handling", () => { it("throws when Python returns an error field", async () => { vi.mocked(parseStdoutJson).mockReturnValue({ error: "Model not found", }); await expect(transcribeAudio(FAKE_AUDIO, { language: "auto" })).rejects.toThrow( "Model not found", ); }); it("propagates bridge timeout", async () => { vi.mocked(runPythonWithProgress).mockRejectedValue(new Error("Python script timed out")); await expect(transcribeAudio(FAKE_AUDIO, { language: "auto" })).rejects.toThrow("timed out"); }); it("propagates OOM errors from bridge", async () => { vi.mocked(runPythonWithProgress).mockRejectedValue( new Error("Process killed (out of memory)"), ); await expect(transcribeAudio(FAKE_AUDIO, { language: "auto" })).rejects.toThrow( "out of memory", ); }); it("propagates parseStdoutJson errors", async () => { vi.mocked(parseStdoutJson).mockImplementation(() => { throw new Error("No JSON response from Python script"); }); await expect(transcribeAudio(FAKE_AUDIO, { language: "auto" })).rejects.toThrow( "No JSON response from Python script", ); }); }); describe("onProgress forwarding", () => { it("passes onProgress to bridge", async () => { const onProgress = vi.fn(); await transcribeAudio(FAKE_AUDIO, { language: "auto" }, onProgress); expect(runPythonWithProgress).toHaveBeenCalledWith( "transcribe.py", expect.any(Array), expect.objectContaining({ onProgress }), ); }); it("omits onProgress when not provided", async () => { await transcribeAudio(FAKE_AUDIO, { language: "auto" }); const options = vi.mocked(runPythonWithProgress).mock.calls[0][2]; expect(options.onProgress).toBeUndefined(); }); }); });