| |
| |
| |
|
|
| import type { PdfImage } from '@/lib/types/generation'; |
| import type { AgentInfo, SceneGenerationContext } from './pipeline-types'; |
|
|
| |
| export function buildCourseContext(ctx?: SceneGenerationContext): string { |
| if (!ctx) return ''; |
|
|
| const lines: string[] = []; |
|
|
| |
| lines.push('Course Outline:'); |
| ctx.allTitles.forEach((t, i) => { |
| const marker = i === ctx.pageIndex - 1 ? ' ← current' : ''; |
| lines.push(` ${i + 1}. ${t}${marker}`); |
| }); |
|
|
| |
| lines.push(''); |
| lines.push( |
| 'IMPORTANT: All pages belong to the SAME class session. Do NOT greet again after the first page. When referencing content from earlier pages, say "we just covered" or "as mentioned on page N" — NEVER say "last class" or "previous session" because there is no previous session.', |
| ); |
| lines.push(''); |
| if (ctx.pageIndex === 1) { |
| lines.push('Position: This is the FIRST page. Open with a greeting and course introduction.'); |
| } else if (ctx.pageIndex === ctx.totalPages) { |
| lines.push('Position: This is the LAST page. Conclude the course with a summary and closing.'); |
| lines.push( |
| 'Transition: Continue naturally from the previous page. Do NOT greet or re-introduce.', |
| ); |
| } else { |
| lines.push(`Position: Page ${ctx.pageIndex} of ${ctx.totalPages} (middle of the course).`); |
| lines.push( |
| 'Transition: Continue naturally from the previous page. Do NOT greet or re-introduce.', |
| ); |
| } |
|
|
| |
| if (ctx.previousSpeeches.length > 0) { |
| lines.push(''); |
| lines.push('Previous page speech (for transition reference):'); |
| const lastSpeech = ctx.previousSpeeches[ctx.previousSpeeches.length - 1]; |
| lines.push(` "...${lastSpeech.slice(-150)}"`); |
| } |
|
|
| return lines.join('\n'); |
| } |
|
|
| |
| export function formatAgentsForPrompt(agents?: AgentInfo[]): string { |
| if (!agents || agents.length === 0) return ''; |
|
|
| const lines = ['Classroom Agents:']; |
| for (const a of agents) { |
| const personaPart = a.persona ? ` — ${a.persona}` : ''; |
| lines.push(`- id: "${a.id}", name: "${a.name}", role: ${a.role}${personaPart}`); |
| } |
| return lines.join('\n'); |
| } |
|
|
| |
| export function formatTeacherPersonaForPrompt(agents?: AgentInfo[]): string { |
| if (!agents || agents.length === 0) return ''; |
|
|
| const teacher = agents.find((a) => a.role === 'teacher'); |
| if (!teacher?.persona) return ''; |
|
|
| return `Teacher Persona:\nName: ${teacher.name}\n${teacher.persona}\n\nAdapt the content style and tone to match this teacher's personality. IMPORTANT: The teacher's name and identity must NOT appear on the slides — no "Teacher ${teacher.name}'s tips", no "Teacher's message", etc. Slides should read as neutral, professional visual aids.`; |
| } |
|
|
| |
| |
| |
| |
| export function formatImageDescription(img: PdfImage): string { |
| let dimInfo = ''; |
| if (img.width && img.height) { |
| const ratio = (img.width / img.height).toFixed(2); |
| dimInfo = ` | size: ${img.width}×${img.height} (aspect ratio ${ratio})`; |
| } |
| const desc = img.description ? ` | ${img.description}` : ''; |
| return `- **${img.id}**: from PDF page ${img.pageNumber}${dimInfo}${desc}`; |
| } |
|
|
| |
| |
| |
| |
| export function formatImagePlaceholder(img: PdfImage): string { |
| let dimInfo = ''; |
| if (img.width && img.height) { |
| const ratio = (img.width / img.height).toFixed(2); |
| dimInfo = ` | size: ${img.width}×${img.height} (aspect ratio ${ratio})`; |
| } |
| return `- **${img.id}**: image from PDF page ${img.pageNumber}${dimInfo} [see attached]`; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function buildVisionUserContent( |
| userPrompt: string, |
| images: Array<{ id: string; src: string; width?: number; height?: number }>, |
| ): Array<{ type: 'text'; text: string } | { type: 'image'; image: string; mimeType?: string }> { |
| const parts: Array< |
| { type: 'text'; text: string } | { type: 'image'; image: string; mimeType?: string } |
| > = [{ type: 'text', text: userPrompt }]; |
| if (images.length > 0) { |
| parts.push({ type: 'text', text: '\n\n--- Attached Images ---' }); |
| for (const img of images) { |
| let dimInfo = ''; |
| if (img.width && img.height) { |
| const ratio = (img.width / img.height).toFixed(2); |
| dimInfo = ` (${img.width}×${img.height}, aspect ratio ${ratio})`; |
| } |
| parts.push({ type: 'text', text: `\n**${img.id}**${dimInfo}:` }); |
| |
| const dataUriMatch = img.src.match(/^data:([^;]+);base64,(.+)$/); |
| if (dataUriMatch) { |
| parts.push({ |
| type: 'image', |
| image: dataUriMatch[2], |
| mimeType: dataUriMatch[1], |
| }); |
| } else { |
| parts.push({ type: 'image', image: img.src }); |
| } |
| } |
| } |
| return parts; |
| } |
|
|
| |
| |
| |
| |
| export function buildLanguageText(directive?: string, sceneNote?: string): string { |
| if (!directive && !sceneNote) return ''; |
| let text = directive || ''; |
| if (sceneNote) { |
| text += (text ? '\n\n' : '') + `Additional language note for this scene: ${sceneNote}`; |
| } |
| return text; |
| } |
|
|