Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- YouTube: detect obviously truncated caption-track transcripts on long videos and fall through to yt-dlp transcription instead of caching a broken partial result (#184, thanks @sportiz91).
- Chrome extension chat: handle plain-string assistant replies in the side-panel agent loop instead of crashing on `.filter()` tool-call extraction (#186, thanks @Youpen-y).
- YouTube: treat yt-dlp “no audio stream” videos as a non-fatal unavailable transcript case so summarize can continue cleanly with an explanatory note (#161, thanks @mdsakalu).
- Homebrew: make the tap formula fail clearly on Linux instead of installing a macOS binary, and add generator/test coverage for the macOS-only guard (#147, thanks @steipete).
- Firecrawl: reject `--firecrawl always` for YouTube URLs with an explicit guidance error instead of silently skipping Firecrawl on the transcript-first path (#145, thanks @steipete).
- YouTube: keep Gemini-only no-caption runs on the transcription path by forwarding the Google API key from the top-level URL flow into link-preview transcription config (#148, thanks @bytrangle).
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/content/transcript/providers/youtube/yt-dlp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,17 @@ export const fetchTranscriptWithYtDlp = async ({
if (result.notes.length > 0) notes.push(...result.notes);
return { text: result.text, provider: result.provider, error: result.error, notes };
} catch (error) {
if (
error instanceof Error &&
error.message.includes("unable to obtain file audio codec with ffprobe")
) {
return {
text: "",
provider: null,
error: null,
notes: [...notes, "yt-dlp: Media has no audio stream"],
};
}
return {
text: null,
provider: null,
Expand Down
27 changes: 27 additions & 0 deletions tests/transcript.youtube-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,33 @@ describe("YouTube transcript provider module", () => {
expect(result.notes).toContain("captionTracks transcript appears truncated");
});

it("returns unavailable with a note when yt-dlp finds no audio stream", async () => {
ytdlp.fetchTranscriptWithYtDlp.mockResolvedValue({
text: "",
provider: null,
error: null,
notes: ["yt-dlp: Media has no audio stream"],
});

const result = await fetchTranscript(
{
url: "https://www.youtube.com/watch?v=abcdefghijk",
html: "<html></html>",
resourceKey: null,
},
{
...baseOptions,
youtubeTranscriptMode: "auto",
ytDlpPath: "/usr/bin/yt-dlp",
openaiApiKey: "OPENAI",
},
);

expect(result.text).toBeNull();
expect(result.source).toBe("unavailable");
expect(result.attemptedProviders).toEqual(["captionTracks", "yt-dlp", "unavailable"]);
expect(result.notes).toContain("yt-dlp: Media has no audio stream");
});
it("returns segments when timestamps are requested", async () => {
captions.fetchTranscriptFromCaptionTracks.mockResolvedValue({
text: "Creator caption",
Expand Down
26 changes: 26 additions & 0 deletions tests/transcript.yt-dlp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,32 @@ describe("yt-dlp transcript helper", () => {
expect(result.error?.message).toMatch(/yt-dlp exited with code 1/);
});

it("returns empty text and a note when yt-dlp fails with 'unable to obtain file audio codec'", async () => {
spawnMock.mockImplementation(() => {
const proc = new EventEmitter() as any;
proc.stdout = new PassThrough();
proc.stderr = new PassThrough();
process.nextTick(() => {
proc.stderr.write(
"ERROR: Postprocessing: WARNING: unable to obtain file audio codec with ffprobe\n",
);
proc.stderr.end();
process.nextTick(() => proc.emit("close", 1, null));
});
return proc;
});

const result = await fetchTranscriptWithYtDlp({
ytDlpPath: "/usr/bin/yt-dlp",
openaiApiKey: "OPENAI",
url: "https://youtu.be/dQw4w9WgXcQ",
});

expect(result.text).toBe("");
expect(result.error).toBeNull();
expect(result.notes).toContain("yt-dlp: Media has no audio stream");
});

it("passes --no-playlist to yt-dlp", async () => {
mockSpawnSuccess();
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(
Expand Down