Skip to content

Commit f474bbc

Browse files
fix: youtube video summarisation (#9)
* fix: youtube video summarisation issue due to youtube api endpoints rules * fix: resolve dependabot alerts * fix: breaking changes with NextJS 16
1 parent 889b947 commit f474bbc

File tree

18 files changed

+846
-636
lines changed

18 files changed

+846
-636
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ yarn-error.log*
3535
*.tsbuildinfo
3636
next-env.d.ts
3737

38-
appwrite.config.json
38+
appwrite.config.json
39+
backupRoute.js

app/api/ai/youtube/route.js

Lines changed: 51 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { NextResponse } from "next/server";
22
import { GoogleGenerativeAI } from "@google/generative-ai";
33
import { cookies } from "next/headers";
4-
import { Innertube } from "youtubei.js";
54

65
// Initialize Gemini AI
76
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
@@ -21,21 +20,6 @@ function extractVideoId(url) {
2120
return null;
2221
}
2322

24-
// Format duration from seconds to readable format
25-
function formatDuration(seconds) {
26-
const hours = Math.floor(seconds / 3600);
27-
const minutes = Math.floor((seconds % 3600) / 60);
28-
const secs = seconds % 60;
29-
30-
if (hours > 0) {
31-
return `${hours}h ${minutes}m ${secs}s`;
32-
} else if (minutes > 0) {
33-
return `${minutes}m ${secs}s`;
34-
} else {
35-
return `${secs}s`;
36-
}
37-
}
38-
3923
export async function POST(request) {
4024
try {
4125
// Check if API key is configured
@@ -103,151 +87,15 @@ export async function POST(request) {
10387
);
10488
}
10589

106-
// Fetch transcript with multiple language fallback
107-
let transcript;
108-
let transcriptText = "";
109-
let transcriptLanguage = "en";
110-
let videoTitle = "YouTube Video"; // Default title
111-
112-
try {
113-
console.log(`[YouTube API] Fetching transcript for video ID: ${videoId}`);
114-
115-
// Initialize YouTube client
116-
const youtube = await Innertube.create();
117-
118-
// Get video info
119-
const info = await youtube.getInfo(videoId);
120-
121-
// Extract video title
122-
videoTitle = info.basic_info?.title || "YouTube Video";
123-
console.log(`[YouTube API] Video title: ${videoTitle}`);
124-
125-
// Get transcript/captions
126-
const transcriptData = await info.getTranscript();
127-
128-
if (!transcriptData || !transcriptData.transcript) {
129-
throw new Error("No transcript available");
130-
}
131-
132-
// Extract transcript segments with safe navigation
133-
const segments =
134-
transcriptData.transcript.content?.body?.initial_segments;
135-
136-
if (!segments || segments.length === 0) {
137-
throw new Error("No transcript segments available");
138-
}
139-
140-
transcript = segments
141-
.map(segment => {
142-
const runs = segment.snippet?.runs || [];
143-
return {
144-
text: runs.map(run => run.text || "").join(""),
145-
start: (segment.start_ms || 0) / 1000,
146-
duration: ((segment.end_ms || 0) - (segment.start_ms || 0)) / 1000,
147-
};
148-
})
149-
.filter(seg => seg.text.trim());
150-
151-
console.log(
152-
`[YouTube API] Transcript fetched. Segments: ${transcript?.length || 0}`,
153-
);
154-
155-
if (!transcript || transcript.length === 0) {
156-
// Try fetching video metadata to provide better error context
157-
const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
158-
return NextResponse.json(
159-
{
160-
error:
161-
"No transcript available for this video. This could be because:\n\n" +
162-
"• The video doesn't have captions/subtitles enabled\n" +
163-
"• The video is private or age-restricted\n" +
164-
"• Auto-generated captions are disabled\n\n" +
165-
"Please try another video that has captions enabled, or ask the video creator to add captions.",
166-
videoUrl: videoUrl,
167-
suggestion:
168-
"Look for videos with the 'CC' (closed captions) icon on YouTube",
169-
},
170-
{ status: 400 },
171-
);
172-
}
173-
174-
// Combine transcript segments
175-
transcriptText = transcript.map(segment => segment.text).join(" ");
176-
177-
// Limit content length to avoid token limits (approximately 50,000 characters)
178-
if (transcriptText.length > 50000) {
179-
transcriptText =
180-
transcriptText.substring(0, 50000) +
181-
"\n\n[Transcript truncated due to length...]";
182-
}
183-
} catch (error) {
184-
console.error("Transcript fetch error:", error);
185-
console.error("Error details:", {
186-
message: error.message,
187-
stack: error.stack,
188-
name: error.name,
189-
});
190-
191-
// Provide detailed error based on error type
192-
let errorMessage = "Failed to fetch video transcript. ";
193-
let suggestions = [];
194-
const errorMsg = error.message?.toLowerCase() || "";
195-
196-
if (errorMsg.includes("transcript") && errorMsg.includes("disabled")) {
197-
errorMessage +=
198-
"The video owner has disabled transcripts for this video.";
199-
suggestions.push(
200-
"Try finding a similar video from a different creator",
201-
);
202-
} else if (
203-
errorMsg.includes("unavailable") ||
204-
errorMsg.includes("not available")
205-
) {
206-
errorMessage += "The video is unavailable, private, or restricted.";
207-
suggestions.push("Check if the video is public and accessible");
208-
suggestions.push("Ensure the video exists and is not deleted");
209-
} else if (
210-
errorMsg.includes("could not") ||
211-
errorMsg.includes("no transcript") ||
212-
errorMsg.includes("no caption")
213-
) {
214-
errorMessage +=
215-
"No captions or subtitles are available for this video.";
216-
suggestions.push("Look for videos with the 'CC' icon on YouTube");
217-
suggestions.push(
218-
"Try videos from educational channels that typically include captions",
219-
);
220-
} else if (errorMsg.includes("cors") || errorMsg.includes("network")) {
221-
errorMessage += "Network error occurred while fetching the transcript.";
222-
suggestions.push("Check your internet connection");
223-
suggestions.push("Try again in a few moments");
224-
} else {
225-
errorMessage +=
226-
"An unexpected error occurred while fetching the transcript.";
227-
suggestions.push("Verify the YouTube URL is correct and complete");
228-
suggestions.push("Try a different video with confirmed captions");
229-
suggestions.push("Check browser console for detailed error logs");
230-
}
231-
232-
return NextResponse.json(
233-
{
234-
error: errorMessage,
235-
suggestions: suggestions,
236-
videoUrl: `https://www.youtube.com/watch?v=${videoId}`,
237-
details: error.message,
238-
technicalDetails:
239-
process.env.NODE_ENV === "development" ? error.stack : undefined,
240-
},
241-
{ status: 400 },
242-
);
243-
}
90+
const normalizedUrl = `https://www.youtube.com/watch?v=${videoId}`;
91+
console.log(`[YouTube API] Processing video: ${normalizedUrl}`);
24492

245-
// Generate summary using Gemini 2.5 Flash
93+
// Use Gemini 2.5 Flash with native YouTube support
24694
const model = genAI.getGenerativeModel({
24795
model: "gemini-flash-latest",
24896
});
24997

250-
const prompt = `You are an expert educational content summarizer. Analyze the following YouTube video transcript and create a comprehensive, well-structured summary in markdown format.
98+
const prompt = `You are an expert educational content summarizer. Analyze this YouTube video and create a comprehensive, well-structured summary in markdown format.
25199
252100
Your summary should include:
253101
@@ -276,26 +124,46 @@ Your summary should include:
276124
## 🔖 Tags
277125
(Suggest 5-7 relevant tags for categorizing this content)
278126
279-
---
127+
Please format your response using proper markdown with headings (##, ###), bullet points (-), numbered lists (1., 2., 3.), **bold**, and *italic* where appropriate to make it easy to read and understand.
280128
281-
**Video Transcript:**
282-
${transcriptText}
129+
Also, at the very beginning of your response, provide the video title in this exact format:
130+
VIDEO_TITLE: [actual video title here]
283131
284-
---
132+
Then continue with the summary.`;
285133

286-
Please format your response using proper markdown with headings (##, ###), bullet points (-), numbered lists (1., 2., 3.), **bold**, and *italic* where appropriate to make it easy to read and understand.`;
134+
// Send YouTube URL directly to Gemini - it handles the video natively!
135+
const result = await model.generateContent([
136+
{
137+
fileData: {
138+
mimeType: "video/mp4",
139+
fileUri: normalizedUrl,
140+
},
141+
},
142+
{ text: prompt },
143+
]);
287144

288-
const result = await model.generateContent(prompt);
289145
const response = await result.response;
290-
const summary = response.text();
146+
const fullText = response.text();
147+
148+
// Extract video title from response
149+
let videoTitle = "YouTube Video";
150+
let summary = fullText;
151+
152+
const titleMatch = fullText.match(/VIDEO_TITLE:\s*(.+?)(?:\n|$)/);
153+
if (titleMatch) {
154+
videoTitle = titleMatch[1].trim();
155+
// Remove the title line from the summary
156+
summary = fullText.replace(/VIDEO_TITLE:\s*.+?\n/, "").trim();
157+
}
158+
159+
console.log(`[YouTube API] Successfully summarized: ${videoTitle}`);
291160

292161
return NextResponse.json({
293162
success: true,
294163
summary: summary,
295164
videoTitle: videoTitle,
296165
videoId: videoId,
297-
youtubeUrl: `https://www.youtube.com/watch?v=${videoId}`,
298-
transcriptLength: transcriptText.length,
166+
youtubeUrl: normalizedUrl,
299167
});
300168
} catch (error) {
301169
console.error("YouTube Summarization Error:", error);
@@ -321,6 +189,24 @@ Please format your response using proper markdown with headings (##, ###), bulle
321189
);
322190
}
323191

192+
if (
193+
error.message?.includes("video") ||
194+
error.message?.includes("YouTube") ||
195+
error.message?.includes("could not")
196+
) {
197+
return NextResponse.json(
198+
{
199+
error:
200+
"Could not process this YouTube video. This could be because:\n\n" +
201+
"• The video is private, age-restricted, or unavailable\n" +
202+
"• The video is too long (try videos under 1 hour)\n" +
203+
"• The video has restrictions that prevent analysis\n\n" +
204+
"Please try a different video.",
205+
},
206+
{ status: 400 },
207+
);
208+
}
209+
324210
return NextResponse.json(
325211
{ error: error.message || "Failed to generate video summary" },
326212
{ status: 500 },

app/api/groups/[groupId]/files/[fileId]/route.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ export async function DELETE(request, { params }) {
1010
if (!session) {
1111
return NextResponse.json(
1212
{ detail: "Not authenticated" },
13-
{ status: 401 }
13+
{ status: 401 },
1414
);
1515
}
1616

17-
const { fileId, groupId } = params;
17+
const { fileId, groupId } = await params;
1818

1919
// Parse session data to get user ID
2020
let sessionData;
@@ -23,7 +23,7 @@ export async function DELETE(request, { params }) {
2323
} catch {
2424
return NextResponse.json(
2525
{ detail: "Invalid session format" },
26-
{ status: 401 }
26+
{ status: 401 },
2727
);
2828
}
2929

@@ -42,14 +42,14 @@ export async function DELETE(request, { params }) {
4242
const file = await databases.getDocument(
4343
process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID,
4444
process.env.NEXT_PUBLIC_APPWRITE_GROUP_FILES_COLLECTION_ID,
45-
fileId
45+
fileId,
4646
);
4747

4848
// Get group details
4949
const group = await databases.getDocument(
5050
process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID,
5151
process.env.NEXT_PUBLIC_APPWRITE_GROUPS_COLLECTION_ID,
52-
groupId
52+
groupId,
5353
);
5454

5555
// Check if user is the file uploader or group creator
@@ -59,7 +59,7 @@ export async function DELETE(request, { params }) {
5959
if (!isUploader && !isGroupCreator) {
6060
return NextResponse.json(
6161
{ detail: "You don't have permission to delete this file" },
62-
{ status: 403 }
62+
{ status: 403 },
6363
);
6464
}
6565

@@ -76,7 +76,7 @@ export async function DELETE(request, { params }) {
7676
await databases.deleteDocument(
7777
process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID,
7878
process.env.NEXT_PUBLIC_APPWRITE_GROUP_FILES_COLLECTION_ID,
79-
fileId
79+
fileId,
8080
);
8181

8282
return NextResponse.json({
@@ -87,7 +87,7 @@ export async function DELETE(request, { params }) {
8787
console.error("Error deleting file:", error);
8888
return NextResponse.json(
8989
{ detail: error.message || "Failed to delete file" },
90-
{ status: 500 }
90+
{ status: 500 },
9191
);
9292
}
9393
}

app/api/groups/[groupId]/files/route.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ export async function GET(request, { params }) {
1010
if (!session) {
1111
return NextResponse.json(
1212
{ detail: "Not authenticated" },
13-
{ status: 401 }
13+
{ status: 401 },
1414
);
1515
}
1616

17-
const { groupId } = params;
17+
const { groupId } = await params;
1818

1919
// Create admin client
2020
const adminClient = new Client()
@@ -32,7 +32,7 @@ export async function GET(request, { params }) {
3232
Query.equal("group_id", groupId),
3333
Query.orderDesc("uploaded_at"),
3434
Query.limit(100),
35-
]
35+
],
3636
);
3737

3838
// Enrich files with uploader names
@@ -44,7 +44,7 @@ export async function GET(request, { params }) {
4444
process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID,
4545
process.env.NEXT_PUBLIC_APPWRITE_USER_PROFILES_COLLECTION_ID ||
4646
"user_profiles",
47-
[Query.equal("user_id", file.uploaded_by), Query.limit(1)]
47+
[Query.equal("user_id", file.uploaded_by), Query.limit(1)],
4848
);
4949

5050
if (userProfile.documents.length > 0) {
@@ -60,15 +60,15 @@ export async function GET(request, { params }) {
6060
console.error("Error fetching user profile:", err);
6161
}
6262
return file;
63-
})
63+
}),
6464
);
6565

6666
return NextResponse.json(enrichedFiles);
6767
} catch (error) {
6868
console.error("Error fetching group files:", error);
6969
return NextResponse.json(
7070
{ detail: error.message || "Failed to fetch files" },
71-
{ status: 500 }
71+
{ status: 500 },
7272
);
7373
}
7474
}

0 commit comments

Comments
 (0)