import type { SoraGenerationParams, SoraGenerationResult, SoraJob, NarrativeGenerationParams, NarrativeGenerationResult, StoryChoice } from '$lib/types'; const API_BASE = 'https://api.openai.com/v1'; /** * Create a Sora video generation job */ export async function createSoraVideo( apiKey: string, params: SoraGenerationParams ): Promise { const formData = new FormData(); formData.append('model', params.model || 'sora-2'); formData.append('prompt', params.prompt); formData.append('seconds', String(params.seconds || 8)); if (params.size) { formData.append('size', params.size); } if (params.inputReference) { formData.append('input_reference', params.inputReference, 'reference.jpg'); } const response = await fetch(`${API_BASE}/videos`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` }, body: formData }); if (!response.ok) { const error = await response.json().catch(() => ({ error: { message: response.statusText } })); throw new Error(`Failed to create video: ${error.error?.message || response.statusText}`); } return await response.json(); } /** * Poll a Sora video job until completion */ export async function pollSoraJob( apiKey: string, jobId: string, onProgress?: (progress: number) => void ): Promise { const POLL_INTERVAL = 2000; // 2 seconds const MAX_ATTEMPTS = 300; // 10 minutes max for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { const response = await fetch(`${API_BASE}/videos/${jobId}`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (!response.ok) { throw new Error(`Failed to poll job: ${response.statusText}`); } const job: SoraJob = await response.json(); if (onProgress && job.progress !== undefined) { onProgress(job.progress); } if (job.status === 'completed') { return job; } if (job.status === 'failed') { throw new Error(job.error?.message || 'Video generation failed'); } await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL)); } throw new Error('Video generation timed out'); } /** * Download the generated video content */ export async function downloadSoraVideo( apiKey: string, jobId: string ): Promise { const response = await fetch(`${API_BASE}/videos/${jobId}/content?variant=video`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (!response.ok) { throw new Error(`Failed to download video: ${response.statusText}`); } // Create a blob URL for the video const blob = await response.blob(); return URL.createObjectURL(blob); } /** * Complete Sora video generation workflow */ export async function generateSoraVideo( apiKey: string, params: SoraGenerationParams, onProgress?: (progress: number) => void ): Promise { // Create the job const job = await createSoraVideo(apiKey, params); // Poll until complete const completedJob = await pollSoraJob(apiKey, job.id, onProgress); // Download the video const videoUrl = await downloadSoraVideo(apiKey, completedJob.id); return { videoUrl, jobId: job.id }; } /** * Generate narrative and choices using GPT-4 */ export async function generateNarrative( apiKey: string, params: NarrativeGenerationParams ): Promise { const systemPrompt = `You are a creative storyteller for an interactive video-based choose-your-own-adventure game. Your role: 1. Write engaging first-person narrative text that describes what the protagonist sees and experiences 2. Create a detailed scene description optimized for Sora video generation (cinematic, specific about visuals, camera movement, lighting) 3. Generate 2-4 meaningful choices that continue the story in interesting directions Guidelines: - Keep narratives concise but immersive (2-4 sentences) - Scene descriptions should be cinematic and specific about visual details - Choices should be distinct and lead to different narrative paths - Maintain story coherence and continuity - Keep content appropriate for general audiences Return a JSON object with this structure: { "narrative": "First-person narrative text shown to the player", "sceneDescription": "Detailed visual description for Sora video generation", "choices": [ {"id": "choice1", "text": "Action the player can take"}, {"id": "choice2", "text": "Another action the player can take"}, ... ] }`; let userPrompt = ''; if (params.isFirstScene) { userPrompt = `Create the opening scene for a new adventure. The player should start in an intriguing situation with clear choices ahead.`; } else { userPrompt = `Story context so far:\n${params.storyContext}\n\n`; if (params.userChoice) { userPrompt += `The player chose: ${params.userChoice}\n\n`; } userPrompt += `Continue the story from this point. What happens next?`; } const response = await fetch(`${API_BASE}/chat/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'gpt-4o', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], response_format: { type: 'json_object' }, temperature: 0.8 }) }); if (!response.ok) { const error = await response.json().catch(() => ({ error: { message: response.statusText } })); throw new Error(`Failed to generate narrative: ${error.error?.message || response.statusText}`); } const data = await response.json(); const content = data.choices[0]?.message?.content; if (!content) { throw new Error('No narrative generated'); } const result = JSON.parse(content); // Validate and ensure IDs for choices const choices: StoryChoice[] = (result.choices || []).map((choice: any, index: number) => ({ id: choice.id || `choice-${Date.now()}-${index}`, text: choice.text, description: choice.description })); return { narrative: result.narrative || '', sceneDescription: result.sceneDescription || result.narrative, choices }; } /** * Build a contextual prompt for Sora that includes continuity hints */ export function buildSoraPrompt(sceneDescription: string, storyContext?: string): string { if (!storyContext) { return sceneDescription; } // Add context similarly to sora-extend approach return `Context (for continuity): ${storyContext} Scene: ${sceneDescription} The scene should continue smoothly from the previous moment, maintaining consistent visual style, lighting, and subject identity.`; }