Skip to content

Commit 7d34c8e

Browse files
committed
fix something
1 parent 22b33e9 commit 7d34c8e

File tree

30 files changed

+1537
-103
lines changed

30 files changed

+1537
-103
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ yarn-error.log*
4040
# typescript
4141
*.tsbuildinfo
4242
next-env.d.ts
43+
44+
# opencode
45+
opencode.jsonc

CLAUDE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,22 @@ The app uses Next.js 15 App Router with two main pages:
7777
- `/api/csrf-token`: Provides CSRF tokens for secure state-changing requests
7878
- `/api/save-analysis`: Saves complete video analysis to database
7979

80+
### Translation Feature
81+
82+
**Transcript Translation (EN ↔ ID)**
83+
- Toggle button in Transcript Viewer header (next to Auto/Manual toggle)
84+
- Uses OpenAI GPT-4o mini for high-quality translation
85+
- Batch processing: 30 segments per request for optimal performance
86+
- Parallel processing across batches
87+
- Smart caching:
88+
- Memory cache: Instant toggle between languages within session
89+
- Database cache: Persistent storage in `translated_transcripts` JSONB column
90+
- Auto-load cached translations on video revisit
91+
- AI Chat integration: Automatically uses transcript in active language
92+
- Background saving: Non-blocking database updates
93+
- Files: `app/api/translate-transcript/route.ts`, `components/transcript-viewer.tsx`
94+
- Migration: `supabase/migrations/20241027000000_add_translated_transcripts.sql`
95+
8096
### Key Technical Implementation
8197

8298
#### Quote Matching System (`lib/quote-matcher.ts`)
@@ -317,6 +333,7 @@ The application uses aggressive parallel processing to minimize latency:
317333
### Environment Variables
318334
Required in `.env.local`:
319335
- `GEMINI_API_KEY`: Google Gemini API key for AI generation
336+
- `OPENAI_API_KEY`: OpenAI API key for transcript translation (GPT-4o mini)
320337
- `SUPADATA_API_KEY`: Supadata API key for transcript fetching
321338
- `NEXT_PUBLIC_SUPABASE_URL`: Supabase project URL
322339
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`: Supabase anonymous key

app/analyze/[videoId]/page.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export default function AnalyzePage() {
104104
const [videoInfo, setVideoInfo] = useState<VideoInfo | null>(null);
105105
const [videoPreview, setVideoPreview] = useState<string>("");
106106
const [transcript, setTranscript] = useState<TranscriptSegment[]>([]);
107+
const [translatedTranscripts, setTranslatedTranscripts] = useState<Record<string, TranscriptSegment[]>>({});
107108
const [topics, setTopics] = useState<Topic[]>([]);
108109
const [baseTopics, setBaseTopics] = useState<Topic[]>([]);
109110
const [themes, setThemes] = useState<string[]>([]);
@@ -491,6 +492,11 @@ export default function AnalyzePage() {
491492

492493
// Load all cached data
493494
setTranscript(sanitizedTranscript);
495+
496+
console.log('[AnalyzePage] Cached translations from API:', cacheData.translatedTranscripts);
497+
if (cacheData.translatedTranscripts) {
498+
setTranslatedTranscripts(cacheData.translatedTranscripts);
499+
}
494500

495501
const cachedVideoInfo = cacheData.videoInfo ?? null;
496502
if (cachedVideoInfo) {
@@ -776,8 +782,9 @@ export default function AnalyzePage() {
776782
setGenerationStartTime(Date.now());
777783

778784
// Create abort controllers for both requests
779-
const topicsController = abortManager.current.createController('topics');
780-
const takeawaysController = abortManager.current.createController('takeaways', 60000);
785+
// Extended timeout for AI generation (Gemini can take 60+ seconds)
786+
const topicsController = abortManager.current.createController('topics', 120000); // 2 minutes
787+
const takeawaysController = abortManager.current.createController('takeaways', 120000); // 2 minutes
781788

782789
// Start topics generation using cached video-analysis endpoint
783790
const topicsPromise = fetch("/api/video-analysis", {
@@ -1684,6 +1691,9 @@ export default function AnalyzePage() {
16841691
onCancelEditing={handleCancelEditing}
16851692
isAuthenticated={!!user}
16861693
onRequestSignIn={promptSignInForNotes}
1694+
youtubeId={videoId}
1695+
cachedTranslations={translatedTranscripts}
1696+
onTranslationUpdate={setTranslatedTranscripts}
16871697
/>
16881698
</div>
16891699
</div>

app/api/chat/route.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ ${chatHistoryContext || 'No prior conversation'}
126126
</context>
127127
<goal>Deliver concise, factual answers. Use the transcript when it is relevant to the question; otherwise respond with your best general knowledge.</goal>
128128
<instructions>
129+
<step name="Language Detection">
130+
<item>CRITICAL: Detect the language of the user's question and the transcript.</item>
131+
<item>If the user asks in Indonesian, respond in Indonesian.</item>
132+
<item>If the user asks in English, respond in English.</item>
133+
<item>If the transcript is in Indonesian, use Indonesian context naturally.</item>
134+
<item>Always match the user's language in your response.</item>
135+
</step>
129136
<step name="Assess Intent">
130137
<item>Decide whether the user's question requires information from the transcript.</item>
131138
<item>If the question is general knowledge or unrelated to the video, answer directly without forcing transcript references.</item>
@@ -137,16 +144,18 @@ ${chatHistoryContext || 'No prior conversation'}
137144
<item>List the same timestamps in the timestamps array, zero-padded and in the order they appear. Provide no more than five unique timestamps.</item>
138145
</step>
139146
<step name="AnswerFormatting">
147+
<item>Respond in the SAME LANGUAGE as the user's question.</item>
140148
<item>Respond in concise, complete sentences that mirror the transcript's language when applicable.</item>
141149
<item>If the transcript lacks the requested information or was unnecessary, state that clearly and return an empty timestamps array.</item>
142150
</step>
143151
</instructions>
144152
<validationChecklist>
153+
<item>Did you respond in the SAME LANGUAGE as the user's question?</item>
145154
<item>If you cited the transcript, does every factual statement have a supporting timestamp in brackets?</item>
146155
<item>Are all timestamps valid moments within the transcript?</item>
147156
<item>If the transcript was unnecessary or lacked the answer, did you state that and keep the timestamps array empty?</item>
148157
</validationChecklist>
149-
<outputFormat>Return strict JSON object: {"answer":"string","timestamps":["MM:SS"]}. No extra commentary.</outputFormat>
158+
<outputFormat>Return strict JSON object: {"answer":"string","timestamps":["MM:SS"]}. No extra commentary. The "answer" field must be in the SAME LANGUAGE as the user's question.</outputFormat>
150159
<transcript><![CDATA[
151160
${transcriptContext}
152161
]]></transcript>

app/api/check-video-cache/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ async function handler(req: NextRequest) {
5555
videoId: videoId,
5656
topics: cachedVideo.topics,
5757
transcript: cachedVideo.transcript,
58+
translatedTranscripts: cachedVideo.translated_transcripts || {},
5859
videoInfo: {
5960
title: cachedVideo.title,
6061
author: cachedVideo.author,

app/api/generate-summary/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,6 @@ export const POST = withSecurity(handler, {
207207
maxBodySize: 10 * 1024 * 1024, // 10MB for large transcripts
208208
allowedMethods: ['POST']
209209
});
210+
211+
// Allow up to 2 minutes for AI summary generation
212+
export const maxDuration = 120;

app/api/generate-topics/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,6 @@ export const POST = withSecurity(handler, {
5858
maxBodySize: 10 * 1024 * 1024, // 10MB for large transcripts
5959
allowedMethods: ['POST']
6060
// Note: Rate limiting is handled internally by the route for dynamic limits based on auth
61-
});
61+
});
62+
// Allow up to 2 minutes for AI topic generation
63+
export const maxDuration = 120;

app/api/notes/route.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,15 @@ async function handler(req: NextRequest) {
9999
if (req.method === 'POST') {
100100
try {
101101
const body = await req.json();
102+
console.log('[notes API] POST request body:', body);
103+
102104
const validatedData = noteInsertSchema.parse(body);
105+
console.log('[notes API] Validated data:', validatedData);
103106

104107
const youtubeId = validatedData.youtubeId;
105108

106109
if (!youtubeId) {
110+
console.error('[notes API] Missing youtubeId');
107111
return NextResponse.json(
108112
{ error: 'youtubeId is required' },
109113
{ status: 400 }
@@ -122,43 +126,51 @@ async function handler(req: NextRequest) {
122126
}
123127

124128
const video = videos?.[0];
129+
console.log('[notes API] Found video:', video);
125130

126131
if (!video?.id) {
132+
console.error('[notes API] Video not found for youtubeId:', youtubeId);
127133
return NextResponse.json(
128134
{ error: 'Video not found' },
129135
{ status: 404 }
130136
);
131137
}
132138

139+
const insertPayload = {
140+
user_id: user.id,
141+
video_id: video.id,
142+
source: validatedData.source,
143+
source_id: validatedData.sourceId ?? null,
144+
note_text: validatedData.text,
145+
metadata: validatedData.metadata ?? null
146+
};
147+
console.log('[notes API] Inserting note with payload:', insertPayload);
148+
133149
const { data: noteRow, error } = await supabase
134150
.from('user_notes')
135-
.insert({
136-
user_id: user.id,
137-
video_id: video.id,
138-
source: validatedData.source,
139-
source_id: validatedData.sourceId || null,
140-
note_text: validatedData.text,
141-
metadata: validatedData.metadata || {}
142-
})
151+
.insert(insertPayload)
143152
.select()
144153
.single();
145154

146155
if (error) {
156+
console.error('[notes API] Supabase insert error:', error);
147157
throw error;
148158
}
149159

160+
console.log('[notes API] Note created successfully:', noteRow);
150161
return NextResponse.json({ note: mapNote(noteRow as NoteRow) }, { status: 201 });
151162
} catch (error) {
152163
if (error instanceof z.ZodError) {
164+
console.error('[notes API] Validation error:', error.issues);
153165
return NextResponse.json(
154166
{ error: 'Validation failed', details: formatValidationError(error) },
155167
{ status: 400 }
156168
);
157169
}
158170

159-
console.error('Error creating note:', error);
171+
console.error('[notes API] Error creating note:', error);
160172
return NextResponse.json(
161-
{ error: 'Failed to save note' },
173+
{ error: 'Failed to save note', details: error instanceof Error ? error.message : 'Unknown error' },
162174
{ status: 500 }
163175
);
164176
}

app/api/save-analysis/route.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ const saveAnalysisSchema = z.object({
2222
topics: z.array(z.any()),
2323
summary: z.string().nullable().optional(),
2424
suggestedQuestions: z.array(z.string()).nullable().optional(),
25-
model: z.string().default('gemini-2.5-flash')
25+
model: z.string().default('gemini-2.5-flash'),
26+
translatedTranscripts: z.record(z.string(), z.array(z.object({
27+
text: z.string(),
28+
start: z.number(),
29+
duration: z.number()
30+
}))).optional()
2631
});
2732

2833
async function handler(req: NextRequest) {
@@ -52,7 +57,8 @@ async function handler(req: NextRequest) {
5257
topics,
5358
summary,
5459
suggestedQuestions,
55-
model
60+
model,
61+
translatedTranscripts
5662
} = validatedData;
5763

5864
const supabase = await createClient();
@@ -75,6 +81,17 @@ async function handler(req: NextRequest) {
7581
})
7682
.single();
7783

84+
if (result && translatedTranscripts && Object.keys(translatedTranscripts).length > 0) {
85+
const { error: updateError } = await supabase
86+
.from('video_analyses')
87+
.update({ translated_transcripts: translatedTranscripts })
88+
.eq('youtube_id', videoId);
89+
90+
if (updateError) {
91+
console.error('Error saving translated transcripts:', updateError);
92+
}
93+
}
94+
7895
if (saveError) {
7996
console.error('Error saving video analysis:', saveError);
8097
return NextResponse.json(

app/api/transcript/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,15 @@ async function handler(request: NextRequest) {
3131
}
3232

3333
let transcriptSegments: any[] | null = null;
34+
3435
try {
36+
console.log('[transcript API] Fetching transcript from Supadata for videoId:', videoId);
3537
const response = await fetch(`https://api.supadata.ai/v1/youtube/transcript?url=https://www.youtube.com/watch?v=${videoId}&lang=en`, {
3638
method: 'GET',
3739
headers: {
3840
'x-api-key': apiKey,
39-
'Content-Type': 'application/json'
41+
'Content-Type': 'application/json',
42+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
4043
}
4144
});
4245

0 commit comments

Comments
 (0)