diff --git a/src/content/__tests__/selectAttachedAudioClip.test.ts b/src/content/__tests__/selectAttachedAudioClip.test.ts index ed2c357..72aa656 100644 --- a/src/content/__tests__/selectAttachedAudioClip.test.ts +++ b/src/content/__tests__/selectAttachedAudioClip.test.ts @@ -85,6 +85,23 @@ describe("selectAttachedAudioClip", () => { expect(result.songTitle).toBe("my-track"); }); + it("decodes URL-encoded filenames from Slack URLs", async () => { + vi.mocked(transcribeSong).mockResolvedValue({ + title: "Singin in the Rain 2", + fullLyrics: "", + segments: [], + }); + vi.mocked(analyzeClips).mockResolvedValue([]); + + const result = await selectAttachedAudioClip({ + audioUrl: "https://blob.vercel-storage.com/content-attachments/audio/1774985247995-Singin%20in%20the%20Rain%202.mp3", + lipsync: false, + }); + + expect(result.songFilename).toBe("1774985247995-Singin-in-the-Rain-2.mp3"); + expect(result.songTitle).toBe("1774985247995-Singin-in-the-Rain-2"); + }); + it("transcribes the downloaded audio", async () => { vi.mocked(transcribeSong).mockResolvedValue({ title: "song", diff --git a/src/content/selectAttachedAudioClip.ts b/src/content/selectAttachedAudioClip.ts index c10d112..2fecb8a 100644 --- a/src/content/selectAttachedAudioClip.ts +++ b/src/content/selectAttachedAudioClip.ts @@ -30,9 +30,9 @@ export async function selectAttachedAudioClip({ const arrayBuffer = await response.arrayBuffer(); const songBuffer = Buffer.from(arrayBuffer); - // Derive filename from URL + // Derive filename from URL (decode %20, replace spaces for fal.ai compat) const urlPath = new URL(audioUrl).pathname; - const songFilename = urlPath.split("/").pop() ?? "attached-audio.mp3"; + const songFilename = decodeURIComponent(urlPath.split("/").pop() ?? "attached-audio.mp3").replace(/\s+/g, "-"); const songTitle = songFilename.replace(/\.(mp3|wav|m4a|ogg|aac)$/i, ""); logStep("Attached audio downloaded", { songTitle, sizeBytes: songBuffer.byteLength }); diff --git a/src/content/transcribeSong.ts b/src/content/transcribeSong.ts index bb19b51..102ad54 100644 --- a/src/content/transcribeSong.ts +++ b/src/content/transcribeSong.ts @@ -25,22 +25,52 @@ export async function transcribeSong( songBuffer: Buffer, songFilename: string, ): Promise { - logger.log("Transcribing song", { filename: songFilename }); + logger.log("Transcribing song", { + filename: songFilename, + bufferSize: songBuffer.byteLength, + }); // Upload audio to fal storage const file = new File([songBuffer], songFilename, { type: "audio/mpeg" }); - const audioUrl = await fal.storage.upload(file); + logger.log("Uploading audio to fal storage", { + fileName: file.name, + fileSize: file.size, + fileType: file.type, + }); + + let audioUrl: string; + try { + audioUrl = await fal.storage.upload(file); + logger.log("Audio uploaded to fal storage", { audioUrl }); + } catch (uploadError) { + logger.log("fal.storage.upload failed", { + error: String(uploadError), + message: (uploadError as Error).message, + }); + throw uploadError; + } // Transcribe with Whisper - const result = await fal.subscribe("fal-ai/whisper" as string, { - input: { - audio_url: audioUrl, - task: "transcribe", - chunk_level: "word", - language: "en", - }, - logs: true, - }); + let result; + try { + result = await fal.subscribe("fal-ai/whisper" as string, { + input: { + audio_url: audioUrl, + task: "transcribe", + chunk_level: "word", + language: "en", + }, + logs: true, + }); + } catch (whisperError) { + logger.log("fal-ai/whisper failed", { + error: String(whisperError), + message: (whisperError as Error).message, + audioUrl, + filename: songFilename, + }); + throw whisperError; + } const data = result.data as unknown as { text?: string;