Fraser's picture
Initial backend setup with Gradio
7ac86fa
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<SoraJob> {
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<SoraJob> {
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<string> {
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<SoraGenerationResult> {
// 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<NarrativeGenerationResult> {
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.`;
}