Spaces:
Running
Running
| <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> | |