odyssey-frontend / src /lib /components /StoryEngine.svelte
Fraser's picture
Initial backend setup with Gradio
7ac86fa
<script lang="ts">
import { onMount } from 'svelte';
import ContinuousVideoPlayer from './ContinuousVideoPlayer.svelte';
import NarrativeDisplay from './NarrativeDisplay.svelte';
import ChoiceInterface from './ChoiceInterface.svelte';
import SoraGenerator from './SoraGenerator.svelte';
import { generateNarrative, buildSoraPrompt } from '$lib/api/openai';
import {
apiKey,
currentScene,
previousFinalFrame,
addScene,
updateCurrentScene,
buildStoryContextText,
setGenerating,
setGenerationProgress,
setGenerationError,
isGenerating
} from '$lib/stores/story';
import type { StoryChoice, StoryScene, SoraGenerationParams } from '$lib/types';
export let onError: ((error: string) => void) | undefined = undefined;
let soraGenerator: any;
let waitingForVideo = false;
let showChoices = false;
let currentVideoUrl: string | undefined;
onMount(() => {
// Start the adventure automatically
startAdventure();
});
async function startAdventure() {
if (!$apiKey) {
const error = 'API key not set';
setGenerationError(error);
onError?.(error);
return;
}
setGenerating(true);
setGenerationError(null);
try {
// Generate the first scene
const narrative = await generateNarrative($apiKey, {
storyContext: '',
isFirstScene: true
});
// Create the first scene
const scene: StoryScene = {
id: `scene-${Date.now()}`,
narrative: narrative.narrative,
choices: narrative.choices,
timestamp: Date.now()
};
addScene(scene);
// Generate the first video
await generateVideoForCurrentScene(narrative.sceneDescription);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to start adventure';
setGenerationError(errorMsg);
onError?.(errorMsg);
setGenerating(false);
}
}
async function generateVideoForCurrentScene(sceneDescription: string) {
if (!$apiKey || !soraGenerator) return;
waitingForVideo = true;
showChoices = false;
try {
// Build the Sora prompt with context if available
const storyContext = buildStoryContextText();
const soraPrompt = buildSoraPrompt(sceneDescription, storyContext);
// Create generation parameters
const params: SoraGenerationParams = {
prompt: soraPrompt,
size: '1280x720',
seconds: 8,
model: 'sora-2',
inputReference: $previousFinalFrame || undefined
};
// Trigger video generation
await soraGenerator.generate();
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Video generation failed';
setGenerationError(errorMsg);
onError?.(errorMsg);
setGenerating(false);
waitingForVideo = false;
}
}
function handleVideoGenerated(event: CustomEvent<any>) {
const result = event.detail;
// Update current scene with video URL
updateCurrentScene({ videoUrl: result.videoUrl });
currentVideoUrl = result.videoUrl;
waitingForVideo = false;
setGenerating(false);
}
function handleVideoEnd(finalFrame: Blob) {
// Store the final frame for continuity
updateCurrentScene({ finalFrame });
// Show choices after video ends
showChoices = true;
}
async function handleChoiceSelected(choice: StoryChoice) {
if (!$apiKey || $isGenerating) return;
setGenerating(true);
setGenerationError(null);
showChoices = false;
try {
// Generate the next scene based on the choice
const storyContext = buildStoryContextText();
const narrative = await generateNarrative($apiKey, {
storyContext,
userChoice: choice.text,
isFirstScene: false
});
// Create the new scene
const scene: StoryScene = {
id: `scene-${Date.now()}`,
narrative: narrative.narrative,
choices: narrative.choices,
timestamp: Date.now()
};
addScene(scene);
// Generate video for the new scene
await generateVideoForCurrentScene(narrative.sceneDescription);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Failed to continue story';
setGenerationError(errorMsg);
onError?.(errorMsg);
setGenerating(false);
}
}
function handleVideoError(error: string) {
setGenerationError(error);
onError?.(error);
}
function handleProgress(progress: number) {
setGenerationProgress(progress);
}
function handleGenerationError(error: string) {
setGenerationError(error);
onError?.(error);
setGenerating(false);
}
// Create Sora params for the generator component
$: soraParams = $currentScene ? {
prompt: buildSoraPrompt($currentScene.narrative, buildStoryContextText()),
size: '1280x720',
seconds: 8,
model: 'sora-2',
inputReference: $previousFinalFrame || undefined
} as SoraGenerationParams : null;
</script>
<div class="story-engine">
{#if $currentScene}
<!-- Video Player -->
<div class="video-section">
<ContinuousVideoPlayer
videoUrl={currentVideoUrl}
onVideoEnd={handleVideoEnd}
onError={handleVideoError}
/>
</div>
<!-- Narrative Display -->
<NarrativeDisplay
narrative={$currentScene.narrative}
isVisible={!$isGenerating}
/>
<!-- Sora Generator (hidden UI, controlled programmatically) -->
{#if $apiKey && soraParams}
<SoraGenerator
bind:this={soraGenerator}
apiKey={$apiKey}
params={soraParams}
onVideoGenerated={handleVideoGenerated}
onProgress={handleProgress}
onError={handleGenerationError}
/>
{/if}
<!-- Choices (shown after video ends) -->
{#if showChoices && !$isGenerating}
<ChoiceInterface
choices={$currentScene.choices}
onChoiceSelected={handleChoiceSelected}
disabled={$isGenerating}
/>
{/if}
{:else if !$isGenerating}
<div class="loading">
<p>Initializing adventure...</p>
</div>
{/if}
</div>
<style>
.story-engine {
max-width: 1280px;
margin: 0 auto;
padding: 1rem;
}
.video-section {
margin-bottom: 1rem;
}
.loading {
text-align: center;
padding: 4rem 2rem;
color: #666;
}
.loading p {
font-size: 1.1rem;
margin: 0;
}
@media (max-width: 768px) {
.story-engine {
padding: 0.5rem;
}
}
</style>