-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Gemini SDK upgrade + VideoPack schema alignment #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eb02877
7233ec3
2d92b29
41b22e3
967d9b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| "lint": "next lint" | ||
| }, | ||
| "dependencies": { | ||
| "@google/genai": "^1.43.0", | ||
| "@google/generative-ai": "^0.24.1", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| "@stripe/stripe-js": "^2.0.0", | ||
| "@supabase/supabase-js": "^2.39.0", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||||||||||
| import OpenAI from 'openai'; | ||||||||||||||||||||||||||||||
| import { GoogleGenerativeAI } from '@google/generative-ai'; | ||||||||||||||||||||||||||||||
| import { GoogleGenAI, Type } from '@google/genai'; | ||||||||||||||||||||||||||||||
| import { NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let _openai: OpenAI | null = null; | ||||||||||||||||||||||||||||||
|
|
@@ -8,13 +8,13 @@ function getOpenAI() { | |||||||||||||||||||||||||||||
| return _openai; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let _gemini: GoogleGenerativeAI | null = null; | ||||||||||||||||||||||||||||||
| let _gemini: GoogleGenAI | null = null; | ||||||||||||||||||||||||||||||
| function getGemini() { | ||||||||||||||||||||||||||||||
| if (!_gemini) _gemini = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || ''); | ||||||||||||||||||||||||||||||
| if (!_gemini) _gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' }); | ||||||||||||||||||||||||||||||
| return _gemini; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // JSON Schema for structured extraction via Responses API | ||||||||||||||||||||||||||||||
| // JSON Schema for structured extraction via OpenAI Responses API | ||||||||||||||||||||||||||||||
| const extractionSchema = { | ||||||||||||||||||||||||||||||
| type: 'object' as const, | ||||||||||||||||||||||||||||||
| properties: { | ||||||||||||||||||||||||||||||
|
|
@@ -54,6 +54,43 @@ const extractionSchema = { | |||||||||||||||||||||||||||||
| additionalProperties: false, | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Gemini responseSchema using @google/genai Type system | ||||||||||||||||||||||||||||||
| const geminiResponseSchema = { | ||||||||||||||||||||||||||||||
| type: Type.OBJECT, | ||||||||||||||||||||||||||||||
| properties: { | ||||||||||||||||||||||||||||||
| events: { | ||||||||||||||||||||||||||||||
| type: Type.ARRAY, | ||||||||||||||||||||||||||||||
| items: { | ||||||||||||||||||||||||||||||
| type: Type.OBJECT, | ||||||||||||||||||||||||||||||
| properties: { | ||||||||||||||||||||||||||||||
| type: { type: Type.STRING, enum: ['action', 'topic', 'insight', 'tool', 'resource'] }, | ||||||||||||||||||||||||||||||
| title: { type: Type.STRING }, | ||||||||||||||||||||||||||||||
| description: { type: Type.STRING }, | ||||||||||||||||||||||||||||||
| timestamp: { type: Type.STRING, nullable: true }, | ||||||||||||||||||||||||||||||
| priority: { type: Type.STRING, enum: ['high', 'medium', 'low'] }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| required: ['type', 'title', 'description', 'priority'], | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| actions: { | ||||||||||||||||||||||||||||||
| type: Type.ARRAY, | ||||||||||||||||||||||||||||||
| items: { | ||||||||||||||||||||||||||||||
| type: Type.OBJECT, | ||||||||||||||||||||||||||||||
| properties: { | ||||||||||||||||||||||||||||||
| title: { type: Type.STRING }, | ||||||||||||||||||||||||||||||
| description: { type: Type.STRING }, | ||||||||||||||||||||||||||||||
| category: { type: Type.STRING, enum: ['setup', 'build', 'deploy', 'learn', 'research', 'configure'] }, | ||||||||||||||||||||||||||||||
| estimatedMinutes: { type: Type.NUMBER, nullable: true }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| required: ['title', 'description', 'category'], | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| summary: { type: Type.STRING }, | ||||||||||||||||||||||||||||||
| topics: { type: Type.ARRAY, items: { type: Type.STRING } }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| required: ['events', 'actions', 'summary', 'topics'], | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const SYSTEM_PROMPT = `You are an expert content analyst. Extract structured data from video transcripts. | ||||||||||||||||||||||||||||||
| Be specific and practical — no vague or generic items. | ||||||||||||||||||||||||||||||
| For events: classify type (action/topic/insight/tool/resource) and priority (high/medium/low). | ||||||||||||||||||||||||||||||
|
|
@@ -94,15 +131,18 @@ async function extractWithOpenAI(trimmed: string, videoTitle?: string, videoUrl? | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| async function extractWithGemini(trimmed: string, videoTitle?: string, videoUrl?: string) { | ||||||||||||||||||||||||||||||
| const model = getGemini().getGenerativeModel({ | ||||||||||||||||||||||||||||||
| const ai = getGemini(); | ||||||||||||||||||||||||||||||
| const response = await ai.models.generateContent({ | ||||||||||||||||||||||||||||||
| model: 'gemini-2.0-flash', | ||||||||||||||||||||||||||||||
| generationConfig: { | ||||||||||||||||||||||||||||||
| responseMimeType: 'application/json', | ||||||||||||||||||||||||||||||
| contents: `${SYSTEM_PROMPT}\n\n${buildUserPrompt(trimmed, videoTitle, videoUrl)}`, | ||||||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||||||
| temperature: 0.3, | ||||||||||||||||||||||||||||||
| responseMimeType: 'application/json', | ||||||||||||||||||||||||||||||
| responseSchema: geminiResponseSchema, | ||||||||||||||||||||||||||||||
| tools: [{ googleSearch: {} }], | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| const result = await model.generateContent(`${SYSTEM_PROMPT}\n\n${buildUserPrompt(trimmed, videoTitle, videoUrl)}`); | ||||||||||||||||||||||||||||||
| const text = result.response.text(); | ||||||||||||||||||||||||||||||
| const text = response.text ?? ''; | ||||||||||||||||||||||||||||||
| return JSON.parse(text); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| return JSON.parse(text); | |
| if (!text || !text.trim()) { | |
| throw new Error('Gemini returned an empty response while JSON was expected from gemini-2.0-flash'); | |
| } | |
| try { | |
| return JSON.parse(text); | |
| } catch (err) { | |
| const message = err instanceof Error ? err.message : String(err); | |
| throw new Error(`Failed to parse Gemini JSON response from gemini-2.0-flash: ${message}`); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The call to JSON.parse(text) will throw an unhandled exception if text is an empty string, which can occur if the Gemini API returns an empty response (response.text is null or undefined). This would cause the API to return a 500 error. It's safer to handle this case to prevent the request from crashing.
| return JSON.parse(text); | |
| return text ? JSON.parse(text) : {}; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import OpenAI from 'openai'; | ||
| import { GoogleGenerativeAI } from '@google/generative-ai'; | ||
| import { GoogleGenAI } from '@google/genai'; | ||
| import { NextResponse } from 'next/server'; | ||
|
|
||
| let _openai: OpenAI | null = null; | ||
|
|
@@ -8,9 +8,9 @@ function getOpenAI() { | |
| return _openai; | ||
| } | ||
|
|
||
| let _gemini: GoogleGenerativeAI | null = null; | ||
| let _gemini: GoogleGenAI | null = null; | ||
| function getGemini() { | ||
| if (!_gemini) _gemini = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || ''); | ||
| if (!_gemini) _gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY || '' }); | ||
| return _gemini; | ||
| } | ||
|
|
||
|
|
@@ -121,32 +121,76 @@ Be thorough — capture all key points, quotes, and technical details.`, | |
| } | ||
| } | ||
|
|
||
| // Strategy 3: Gemini fallback (when OpenAI unavailable) | ||
| // Strategy 3: Gemini with direct YouTube URL processing + Google Search grounding | ||
| if (url && !audioUrl && process.env.GEMINI_API_KEY) { | ||
| try { | ||
| const model = getGemini().getGenerativeModel({ | ||
| const ai = getGemini(); | ||
| const result = await ai.models.generateContent({ | ||
| model: 'gemini-2.0-flash', | ||
| generationConfig: { temperature: 0.2 }, | ||
| contents: [ | ||
| { | ||
| role: 'user', | ||
| parts: [ | ||
| { | ||
| fileData: { | ||
| mimeType: 'video/*', | ||
| fileUri: url, | ||
| }, | ||
|
Comment on lines
+135
to
+138
|
||
| }, | ||
| { | ||
| text: 'Provide a complete, detailed transcript of this video. ' + | ||
| 'Include all spoken content verbatim. ' + | ||
| 'Include timestamps where possible in [MM:SS] format. ' + | ||
| 'Be thorough and comprehensive — capture every key point, quote, and technical detail.', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| config: { | ||
| temperature: 0.2, | ||
| tools: [{ googleSearch: {} }], | ||
| }, | ||
| }); | ||
|
|
||
| const result = await model.generateContent( | ||
| `You are a video content transcription assistant. ` + | ||
| `For the following YouTube video URL, provide a detailed transcript or content summary. ` + | ||
| `Include all key points, technical details, quotes, and actionable insights. ` + | ||
| `Be thorough and comprehensive.\n\nVideo URL: ${url}` | ||
| ); | ||
| const text = result.response.text(); | ||
| const text = result.text ?? ''; | ||
|
|
||
| if (text.length > 100) { | ||
| return NextResponse.json({ | ||
| success: true, | ||
| transcript: text, | ||
| source: 'gemini', | ||
| source: 'gemini-video', | ||
| wordCount: text.split(/\s+/).length, | ||
| }); | ||
| } | ||
| } catch (e) { | ||
| console.warn('Gemini transcript fallback failed:', e); | ||
| console.warn('Gemini video URL processing failed, trying text fallback:', e); | ||
|
|
||
| // Fallback: text-based Gemini with Google Search grounding | ||
| try { | ||
| const ai = getGemini(); | ||
| const result = await ai.models.generateContent({ | ||
| model: 'gemini-2.0-flash', | ||
| contents: `You are a video content transcription assistant. ` + | ||
| `For the following YouTube video URL, provide a detailed transcript or content summary. ` + | ||
| `Include all key points, technical details, quotes, and actionable insights. ` + | ||
| `Be thorough and comprehensive.\n\nVideo URL: ${url}`, | ||
| config: { | ||
| temperature: 0.2, | ||
| tools: [{ googleSearch: {} }], | ||
| }, | ||
| }); | ||
| const text = result.text ?? ''; | ||
|
|
||
| if (text.length > 100) { | ||
| return NextResponse.json({ | ||
| success: true, | ||
| transcript: text, | ||
| source: 'gemini', | ||
| wordCount: text.split(/\s+/).length, | ||
| }); | ||
| } | ||
| } catch (e2) { | ||
| console.warn('Gemini text fallback also failed:', e2); | ||
| } | ||
| } | ||
|
Comment on lines
126
to
194
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error handling logic for Gemini video processing has a deeply nested |
||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR migrates routes to
@google/genai, but@google/generative-airemains in dependencies. Keeping both SDKs increases bundle size and maintenance overhead. If nothing else inapps/webstill imports@google/generative-ai, remove it from dependencies (and update the lockfile) to avoid duplicate SDKs.