# Project export: DirectorAI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: From one short sentence, our AI builds a "micro-film"; it creates scenes, characters, frames, and then generates video and audio. Each piece is independent, so one can tweak each part at any time.
- Devpost: https://devpost.com/software/directorai
- GitHub: https://github.com/Joy12343/hackathon
- Demo: https://directorai.weebly.com/
- Video: https://www.youtube.com/embed/cDs5pYc3Y3Q?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Joy12343 (5 commits)

## Devpost submission (written by the team)

### Inspiration

As AI-generated content floods social media, we were struck by how often these videos fail to tell a coherent story: Scenes jump erratically, Characters teleport, and plots unravel after just a quick seconds. At the same time, as avid fans of movies and TV shows, we often ask “what if?” - picturing scenes that have never been filmed. In this project, we aim to build an AI that could direct a “micro-film” with the narrative flow and emotional continuity of a human storyteller, and turn every “What if?” into a vivid reality. Not only were we intrigued by the creative potential, but we also left room for the users to unleash their creativity by freely adding, removing, and modifying every scene in the film. How We Built It: Core Ideas That’s why we built DirectorAI, an AI pipeline that transforms a single text prompt into a short film, complete with character, plot, environment, and sound. The system is structured as four major components Script Builder: Expands a prompt into a concrete, multi-scene screenplay with environment details, character profiles, and dialogue. We designed this module to mimic how human writers sketch out a story. Character & Scene Renderer: Generates reference portraits for each character for consistency and a key visual frame for every scene, resized to the appropriate size for the next step Video Composer: Generates a video of each scene based on the key visual frame Audio & Integration Engine: Extracts dialogue or sound cues from the script, generates matching voice or sound effects, and merges them with visuals for the final output. If the results are non-satisfactory, we provide options for users to Regenerate a scene by changing the scene description or key figure Insert an interpolated transition frame, crafted to follow the preceding scene naturally to smooth out awkward transitions. Users can iterate on either option until satisfied. Challenges We Faced The biggest challenge was temporal coherence—ensuring that character positions, camera angles, and scene flow felt natural across shots. For example, humans could intuitively understand that Michael walked from a roller-coaster to a carousel, but AI skips the journey entirely, and Michael will somehow disappear from the roller-coaster and teleport to a carousel. We tackled this by: Introducing cinematic shot types (wide, medium, close) into the script Designing detailed prompt engineering that guides the continuity of scene generation Allowing user-inserted interpolation frames for manual refinement Also, integrating all the parts - scripts, images, video, sounds, i.e., multiple APIs - into one frontend/backend system proved unexpectedly complex. Debugging handoffs between modules became a technical challenge. Conclusion & Takeaways What began as a simple idea—a better way to generate AI videos—evolved into a platform that captures the essence of cinematic storytelling through AI. We learned that directing with AI isn't just about generating pretty images. It's about rhythm, structure, and the invisible threads that tie one scene to the next. There’s still a long way to go before AI can match a human director’s intuition, but this project showed us how much potential lies ahead. With each iteration, the line between human storytelling and AI assistance gets thinner. We hope Director AI can be a small step toward democratizing the art of filmmaking. Thanks for all the suggestions and feedback!

## README (from the GitHub repository)

## Inspiration
As AI-generated content floods social media, we were struck by how often these videos fail to tell a coherent story: Scenes jump erratically, Characters teleport, and plots unravel after just a quick seconds. At the same time, as avid fans of movies and TV shows, we often ask “what if?” - picturing scenes that have never been filmed. In this project, we aim to build an AI that could direct a “micro-film” with the narrative flow and emotional continuity of a human storyteller, and turn every “What if?” into a vivid reality. Not only were we intrigued by the creative potential, but we also left room for the users to unleash their creativity by freely adding, removing, and modifying every scene in the film.

## How We Built It: Core Ideas
That’s why we built DirectorAI, an AI pipeline that transforms a single text prompt into a short film, complete with character, plot, environment, and sound. The system is structured as four major components:

1. Script Builder: Expands a prompt into a concrete, multi-scene screenplay with environment details, character profiles, and dialogue. We designed this module to mimic how human writers sketch out a story.
2. Character & Scene Renderer: Generates reference portraits for each character for consistency and a key visual frame for every scene, resized to the appropriate size for the next step
3. Video Composer: Generates a video of each scene based on the key visual frame
4. Audio & Integration Engine: Extracts dialogue or sound cues from the script, generates matching voice or sound effects, and merges them with visuals for the final output.
If the results are non-satisfactory, we provide options for users to

Regenerate a scene by changing the scene description or key figure
1. Insert an interpolated transition frame, crafted to follow the preceding scene naturally to smooth out awkward transitions.
2. Users can iterate on either option until satisfied.

## Challenges We Faced
The biggest challenge was temporal coherence—ensuring that character positions, camera angles, and scene flow felt natural across shots. For example, humans could intuitively understand that Michael walked from a roller-coaster to a carousel, but AI skips the journey entirely, and Michael will somehow disappear from the roller-coaster and teleport to a carousel. We tackled this by:

1. Introducing cinematic shot types (wide, medium, close) into the script
2. Designing detailed prompt engineering that guides the continuity of scene generation
3. Allowing user-inserted interpolation frames for manual refinement

Also, integrating all the parts - scripts, images, video, sounds, i.e., multiple APIs - into one frontend/backend system proved unexpectedly complex. Debugging handoffs between modules became a technical challenge.

## Conclusion & Takeaways
What began as a simple idea—a better way to generate AI videos—evolved into a platform that captures the essence of cinematic storytelling through AI. We learned that directing with AI isn't just about generating pretty images. It's about rhythm, structure, and the invisible threads that tie one scene to the next. There’s still a long way to go before AI can match a human director’s intuition, but this project showed us how much potential lies ahead. With each iteration, the line between human storytelling and AI assistance gets thinner. We hope Director AI can be a small step toward democratizing the art of filmmaking. Thanks for all the suggestions and feedback!



## Detected evidence (automated analysis)

Indexed codebase: 63 recognized source files, 164 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- HTML (language) — claimed on Devpost, not found in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (73 of 73)

```
.gitignore
api/character_generator.py
api/character_style_guide.py
api/character.py
api/non_writer.py
api/process_scene_images.py
api/scene_generator.py
api/scene_picture.py
api/single_voice_line.py
api/video_generator.py
api/writer.py
app/api/cloudinary/route.ts
app/api/compile-videos/route.ts
app/api/generate-character-images/route.ts
app/api/generate-images/route.ts
app/api/generate-story/route.ts
app/api/generate-video/route.ts
app/edit/page.tsx
app/frames/page.tsx
app/globals.css
app/input/page.tsx
app/layout.tsx
app/page.tsx
app/video/page.tsx
component/character_service.py
component/image_app.py
component/simple_video_service.py
component/temp_story.json
component/tempCodeRunnerFile.py
component/video_app.py
component/video_compiler.py
component/writer_app.py
components.json
components/ui/button.tsx
components/ui/card.tsx
components/ui/input.tsx
components/ui/textarea.tsx
eslint.config.mjs
lib/storage.ts
lib/utils.ts
next.config.js
next.config.mjs
package.json
postcss.config.js
postcss.config.mjs
README.md
requirements-video.txt
requirements.txt
scripts/check-services.sh
scripts/fix-dependencies.sh
scripts/fix-frontend-complete.sh
scripts/fix-frontend.sh
scripts/fix-moviepy.py
scripts/kill-services.sh
scripts/quick-fix.sh
scripts/reset-frontend.sh
scripts/setup-backend.py
scripts/setup-fresh.sh
scripts/start-all-services.sh
scripts/start-development.sh
scripts/start-image-service-only.sh
scripts/start-image-service.sh
scripts/start-services-step-by-step.sh
scripts/start-video-service-simple.sh
scripts/start-video-service.sh
scripts/test-image-generation.sh
scripts/test-services.sh
scripts/test-video-connection.sh
scripts/test-video-service.py
scripts/test-video-service.sh
scripts/troubleshoot-video.py
tailwind.config.js
tsconfig.json
```

### Dependencies

- package.json: @types/node@^20, @types/react@^18, @types/react-dom@^18, autoprefixer@^10.0.1, class-variance-authority@^0.7.0, cloudinary@^2.7.0, clsx@^2.0.0, eslint@^8, eslint-config-next@14.2.5, lucide-react@^0.294.0, next@^14.2.30, postcss@^8, react@^18, react-dom@^18, tailwind-merge@^2.0.0, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7
- requirements.txt: anthropic, cloudinary, google-genai, Pillow, python-dotenv

### Recent commits (newest first)

- Update README.md
- final version
- best version
- clean up
- Add a somewhat working frontend
- Initial commit from Create Next App

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
google-genai
anthropic
Pillow
cloudinary
python-dotenv

```

### package.json

```
{
  "name": "story-video-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "class-variance-authority": "^0.7.0",
    "cloudinary": "^2.7.0",
    "clsx": "^2.0.0",
    "lucide-react": "^0.294.0",
    "next": "^14.2.30",
    "react": "^18",
    "react-dom": "^18",
    "tailwind-merge": "^2.0.0"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.0.1",
    "eslint": "^8",
    "eslint-config-next": "14.2.5",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "tailwindcss-animate": "^1.0.7"
  }
}

```

### app/layout.tsx

```typescript
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"

const inter = Inter({ subsets: ["latin"] })

export const metadata: Metadata = {
  title: "Story to Video",
  description: "Transform stories into videos with AI",
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

```

### app/page.tsx

```typescript
import Link from "next/link"
import { Play, Film, Sparkles } from "lucide-react"

export default function Home() {
  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
      <div className="container mx-auto px-4 py-16">
        <div className="text-center mb-16">
          <div className="flex justify-center mb-6">
            <div className="p-4 bg-white/10 rounded-full backdrop-blur-sm">
              <Film className="w-12 h-12 text-white" />
            </div>
          </div>
          <h1 className="text-5xl md:text-7xl font-bold text-white mb-6">
            Story to{" "}
            <span className="text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-purple-400">Video</span>
          </h1>
          <p className="text-xl text-gray-300 max-w-2xl mx-auto mb-8">
            Transform your stories into cinematic videos with AI. From text to screenplay, characters to scenes, and
            finally to a complete video production.
          </p>
          <Link href="/input">
            <button className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 text-white px-8 py-4 text-lg font-semibold rounded-full shadow-lg hover:shadow-xl transition-all duration-300 flex items-center mx-auto">
              <Play className="w-5 h-5 mr-2" />
              Start Creating
            </button>
          </Link>
        </div>

        <div className="grid md:grid-cols-3 gap-8 mb-16">
          <div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg p-6 text-center hover:bg-white/15 transition-all duration-300">
            <div className="p-3 bg-pink-500/20 rounded-full w-fit mx-auto mb-4">
              <Sparkles className="w-8 h-8 text-pink-400" />
            </div>
            <h3 className="text-xl font-semibold text-white mb-2">AI Screenplay</h3>
            <p className="text-gray-300">
              Transform your story ideas into professional screenplays with detailed scenes.
            </p>
          </div>

          <div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg p-6 text-center hover:bg-white/15 transition-all duration-300">
            <div className="p-3 bg-blue-500/20 rounded-full w-fit mx-auto mb-4">
              <Sparkles className="w-8 h-8 text-blue-400" />
            </div>
            <h3 className="text-xl font-semibold text-white mb-2">Character Generation</h3>
            <p className="text-gray-300">Generate consistent character designs and cinematic scene images.</p>
          </div>

          <div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg p-6 text-center hover:bg-white/15 transition-all duration-300">
            <div className="p-3 bg-purple-500/20 rounded-full w-fit mx-auto mb-4">
              <Film className="w-8 h-8 text-purple-400" />
            </div>
            <h3 className="text-xl font-semibold text-white mb-2">Video Production</h3>
            <p className="text-gray-300">Automatically generate professional video sequences from your scenes.</p>
          </div>
        </div>
      </div>
    </div>
  )
}

```

### app/input/page.tsx

```typescript
"use client"

import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { ArrowLeft, Sparkles, Loader2 } from "lucide-react"

export default function InputPage() {
  const [story, setStory] = useState("")
  const [isGenerating, setIsGenerating] = useState(false)
  const router = useRouter()

  const handleGenerate = async () => {
    if (!story.trim()) return

    setIsGenerating(true)
    try {
      const response = await fetch("/api/generate-story", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ story }),
      })

      if (response.ok) {
        const data = await response.json()
        localStorage.setItem("storyData", JSON.stringify(data))
        router.push("/edit")
      } else {
        console.error("Failed to generate story")
        alert("Failed to generate story. Please try again.")
      }
    } catch (error) {
      console.error("Error generating story:", error)
      alert("Error generating story. Please check if backend services are running.")
    } finally {
      setIsGenerating(false)
    }
  }

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
      <div className="container mx-auto px-4 py-8">
        <div className="flex items-center mb-8">
          <Link href="/">
            <button className="text-white hover:bg-white/10 px-3 py-2 rounded-lg flex items-center">
              <ArrowLeft className="w-4 h-4 mr-2" />
              Back
            </button>
          </Link>
        </div>

        <div className="max-w-4xl mx-auto">
          <div className="text-center mb-8">
            <h1 className="text-4xl font-bold text-white mb-4">Tell Your Story</h1>
            <p className="text-gray-300 text-lg">
              Describe your story idea and let AI transform it into a cinematic experience
            </p>
          </div>

          <div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg p-6">
            <div className="mb-6">
              <div className="flex items-center text-white mb-4">
                <Sparkles className="w-5 h-5 mr-2 text-purple-400" />
                Story Input
              </div>
              <textarea
                placeholder="Enter your story here... For example: 'Two teenagers play basketball on a neighborhood court. Edward is confident and skilled, while Michael is determined to prove himself.'"
                value={story}
                onChange={(e) => setStory(e.target.value)}
                className="w-full min-h-[200px] bg-white/5 border border-white/20 text-white placeholder:text-gray-400 rounded-lg p-4 resize-none focus:outline-none focus:ring-2 focus:ring-purple-500"
              />
            </div>

            <div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4 mb-6">
              <h3 className="text-blue-300 font-medium mb-2">Tips for better results:</h3>
              <ul className="text-blue-200 text-sm space-y-1">
                <li>• Include character descriptions and personalities</li>
                <li>• Describe the setting and atmosphere</li>
                <li>• Mention key actions or conflicts</li>
                <li>• Keep it concise but detailed (2-3 paragraphs work well)</li>
              </ul>
            </div>

            <button
              onClick={handleGenerate}
              disabled={!story.trim() || isGenerating}
              className="w-full bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 text-white py-3 text-lg font-semibold rounded-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
            >
              {isGenerating ? (
                <>
                  <Loader2 className="w-5 h-5 mr-2 animate-spin" />
                  Generating Story...
                </>
              ) : (
                <>
                  <Sparkles className="w-5 h-5 mr-2" />
                  Generate Story
                </>
              )}
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

```

### app/video/page.tsx

```typescript
"use client"

import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { ArrowLeft, Download, Share2, RotateCcw, Play } from "lucide-react"

export default function VideoPage() {
  const [videoUrl, setVideoUrl] = useState<string | null>(null)
  const [isPlaying, setIsPlaying] = useState(false)
  const router = useRouter()

  useEffect(() => {
    const url = localStorage.getItem("videoUrl")
    if (url) {
      setVideoUrl(url)
    } else {
      router.push("/frames")
    }
  }, [router])

  const handlePlayPause = () => {
    const video = document.getElementById("main-video") as HTMLVideoElement
    if (video) {
      if (isPlaying) {
        video.pause()
      } else {
        video.play()
      }
      setIsPlaying(!isPlaying)
    }
  }

  const downloadVideo = () => {
    if (videoUrl) {
      const a = document.createElement("a")
      a.href = videoUrl
      a.download = "story-video.mp4"
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
    }
  }

  const shareVideo = async () => {
    if (navigator.share && videoUrl) {
      try {
        await navigator.share({
          title: "My Story Video",
          text: "Check out this video I created with AI!",
          url: videoUrl,
        })
      } catch (error) {
        console.log("Error sharing:", error)
      }
    } else {
      navigator.clipboard.writeText(window.location.href)
      alert("Video link copied to clipboard!")
    }
  }

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
      <div className="container mx-auto px-4 py-8">
        <div className="flex items-center justify-between mb-8">
          <Link href="/frames">
            <button className="text-white hover:bg-white/10 px-3 py-2 rounded-lg flex items-center">
              <ArrowLeft className="w-4 h-4 mr-2" />
              Back to Frames
            </button>
          </Link>
          <Link href="/">
            <button className="border border-white/20 text-white hover:bg-white/10 px-4 py-2 rounded-lg flex items-center">
              <RotateCcw className="w-4 h-4 mr-2" />
              Create New Story
            </button>
          </Link>
        </div>

        <div className="max-w-4xl mx-auto">
          <div className="text-center mb-8">
            <h1 className="text-4xl font-bold text-white mb-4">Your Story Video</h1>
            <p className="text-gray-300 text-lg">
              Your cinematic masterpiece is ready! Watch, download, or share your creation.
            </p>
          </div>

          <div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg p-0 mb-8">
            <div className="relative">
              {videoUrl ? (
                <video id="main-video" className="w-full aspect-video rounded-t-lg" controls>
                  <source src={videoUrl} type="video/mp4" />
                  Your browser does not support the video tag.
                </video>
              ) : (
                <div className="w-full aspect-video bg-gray-800 rounded-t-lg flex items-center justify-center">
                  <div className="text-center text-gray-400">
                    <Play className="w-16 h-16 mx-auto mb-4" />
                    <p>Video not available</p>
                  </div>
                </div>
              )}
            </div>
          </div>

          <div className="grid md:grid-cols-2 gap-4 mb-8">
            <button
              onClick={downloadVideo}
              className="bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white py-3 px-6 rounded-lg flex items-center justify-center"
            >
              <Download className="w-5 h-5 mr-2" />
              Download Video
            </button>
            <button
              onClick={shareVideo}
              className="bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700 text-white py-3 px-6 rounded-lg flex items-center justify-center"
            >
              <Share2 className="w-5 h-5 mr-2" />
              Share Video
            </button>
          </div>

          <div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg p-6">
            <h2 className="text-white text-xl font-semibold mb-4">Video Details</h2>
            <div className="grid md:grid-cols-2 gap-6">
              <div className="space-y-3">
                <div className="flex justify-between">
                  <span className="text-gray-300">Resolution:</span>
                  <span className="text-white">1920x1080 (Full HD)</span>
                </div>
                <div className="flex justify-between">
                  <span className="text-gray-300">Aspect Ratio:</span>
                  <span className="text-white">16:9</span>
                </div>
                <div className="flex justify-between">
                  <span className="text-gray-300">Frame Rate:</span>
                  <span className="text-white">24 FPS</span>
                </div>
              </div>
              <div className="space-y-3">
                <div className="flex justify-between">
                  <span className="text-gray-300">Format:</span>
                  <span className="text-white">MP4</span>
                </div>
                <div className="flex justify-between">
                  <span className="text-gray-300">Quality:</span>
                  <span className="text-white">Cinematic</span>
                </div>
                <div className="flex justify-between">
                  <span className="text-gray-300">Status:</span>
                  <span className="text-green-400">Complete</span>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

```

### app/frames/page.tsx

```typescript
"use client"

import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { ArrowLeft, Play, Loader2, Film } from "lucide-react"
import Image from "next/image"

interface Scene {
  Scene: number
  Description: string
  Dialogue: string
}

export default function FramesPage() {
  const [scenes, setScenes] = useState<Scene[]>([])
  const [sceneImages, setSceneImages] = useState<Record<number, string>>({})
  const [isGeneratingVideo, setIsGeneratingVideo] = useState(false)
  const [currentFrame, setCurrentFrame] = useState(0)
  const router = useRouter()

  useEffect(() => {
    const storyData = localStorage.getItem("storyData")
    const storedSceneImages = localStorage.getItem("sceneImages")

    if (storyData) {
      const data = JSON.parse(storyData)
      setScenes(data.Scenes || [])
    } else {
      router.push("/input")
    }

    if (storedSceneImages) {
      setSceneImages(JSON.parse(storedSceneImages))
    }
  }, [router])

  const generateVideo = async () => {
    setIsGeneratingVideo(true)
    try {
      const videoUrls: string[] = []
      for (let i = 0; i < scenes.length; i++) {
        setCurrentFrame(i)
        const scene = scenes[i]
        const imageUrl = sceneImages[scene.Scene]
        const prompt = scene.Description

        if (!imageUrl) {
          console.error(`No image for scene ${scene.Scene}`)
          continue
        }

        // Call backend to generate video for this scene
        const response = await fetch("/api/generate-video", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            prompt,
            imageUrl,
            aspectRatio: "16:9",
          }),
        })

        if (!response.ok) {
          const errorText = await response.text()
          console.error(`Failed to generate video for scene ${scene.Scene}:`, errorText)
          continue
        }

        const data = await response.json()
        if (data.videoUrl) {
          videoUrls.push(data.videoUrl)
        } else {
          console.error(`No video URL returned for scene ${scene.Scene}`)
        }
      }

      // If only one video, use it; if multiple, call compile endpoint
      let finalVideoUrl = videoUrls[0] || ""
      if (videoUrls.length > 1) {
        // Call backend to compile videos
        const compileRes = await fetch("/api/compile-videos", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ video_urls: videoUrls }),
        })
        if (compileRes.ok) {
          const compileData = await compileRes.json()
          finalVideoUrl = compileData.compiled_video_url || finalVideoUrl
        } else {
          const errorText = await compileRes.text()
          console.error("Failed to compile videos:", errorText)
        }
      }

      if (finalVideoUrl) {
        localStorage.setItem("videoUrl", finalVideoUrl)
        router.push("/video")
      } else {
        throw new Error("No video was generated.")
      }
    } catch (error) {
      console.error("Error generating video:", error)
      // Optionally show error to user
    } finally {
      setIsGeneratingVideo(false)
    }
  }

  return (
    <div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
      <div className="container mx-auto px-4 py-8">
        <div className="flex items-center justify-between mb-8">
          <Link href="/edit">
            <button className="text-white hover:bg-white/10 px-3 py-2 rounded-lg flex items-center">
              <ArrowLeft className="w-4 h-4 mr-2" />
              Back to Edit
            </button>
          </Link>
          <button
            onClick={generateVideo}
            disabled={isGeneratingVideo}
            className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 text-white px-6 py-2 rounded-lg flex items-center disabled:opacity-50"
          >
            {isGeneratingVideo ? (
              <>
                <Loader2 className="w-4 h-4 mr-2 animate-spin" />
                Generating Video...
              </>
            ) : (
              <>
                <Play className="w-4 h-4 mr-2" />
                Generate Video
              </>
            )}
          </button>
        </div>

        <div className="max-w-6xl mx-auto">
          <div className="text-center mb-8">
            <h1 className="text-4xl font-bold text-white mb-4">Video Frames</h1>
            <p className="text-gray-300 text-lg">Preview your story frames before generating the final video</p>
          </div>

          {isGeneratingVideo && (
            <div className="bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg p-6 mb-8">
              <div className="text-center">
                <div className="flex items-center justify-center mb-4">
                  <Film className="w-8 h-8 text-purple-400 mr-3" />
                  <span className="text-white text-lg font-semibold">
                    Processing Frame {currentFrame + 1} of {scenes.length}
                  </span>
                </div>
                <div className="w-full bg-gray-700 rounded-full h-2 mb-4">
                  <div
                    className="bg-gradient-to-r from-pink-500 to-purple-600 h-2 rounded-full transition-all duration-500"
                    style={{ width: `${((currentFrame + 1) / scenes.length) * 100}%` }}
                  ></div>
                </div>
                <p className="text-gray-300">Generating video for Scene {currentFrame + 1}...</p>
              </div>
            </div>
          )}

          <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
            {scenes.map((scene, index) => (
              <div
                key={scene.Scene}
                className={`bg-white/10 backdrop-blur-sm border border-white/20 rounded-lg p-6 hover:bg-whi
[truncated — 3063 more characters]
```

### app/edit/page.tsx

```typescript
"use client"

import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { ArrowLeft, Users, Film, Play, Loader2, RefreshCw } from "lucide-react"
import Image from "next/image"

interface Character {
  Name: string
  Description: string
  Personality: string
  Role: string
}

interface Scene {
  Scene: number
  Description: string
  Dialogue: string
}

interface StoryData {
  Background: string
  Characters: Character[]
  Scenes: Scene[]
}

export default function EditPage() {
  const [storyData, setStoryData] = useState<StoryData | null>(null)
  const [isGeneratingImages, setIsGeneratingImages] = useState(false)
  const [characterImages, setCharacterImages] = useState<Record<string, string>>({})
  const [sceneImages, setSceneImages] = useState<Record<number, string>>({})
  const [generationProgress, setGenerationProgress] = useState("")
  const [generationLog, setGenerationLog] = useState<string[]>([])
  const router = useRouter()

  async function uploadToCloudinary(imageBase64: string): Promise<string> {
    const res = await fetch('/api/cloudinary', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ imageBase64 })
    })
    if (!res.ok) throw new Error('Upload failed: ' + res.statusText)
    const { url } = await res.json()
    return url
  }

  useEffect(() => {
    const data = localStorage.getItem("storyData")
    if (data) {
      setStoryData(JSON.parse(data))
    } else {
      router.push("/input")
    }
  }, [router])

  const addToLog = (message: string) => {
    setGenerationLog((prev) => [...prev, message])
    console.log(message)
  }

  const generateCharacterImage = async (character: Character) => {
    try {
      addToLog(`🎨 Generating image for ${character.Name}...`)

      const response = await fetch("/api/generate-character-images", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ character }),
      })

      if (response.ok) {
        const data = await response.json()
        if (data.base64_image) {
          const imageUrl = `data:image/png;base64,${data.base64_image}`
          setCharacterImages((prev) => ({ ...prev, [character.Name]: imageUrl }))
          addToLog(`✅ Generated ${data.type || "AI"} image for ${character.Name}`)
          return imageUrl
        }
      } else {
        const errorText = await response.text()
        addToLog(`❌ Failed to generate image for ${character.Name}: ${errorText}`)
      }
    } catch (error) {
      addToLog(`❌ Error generating image for ${character.Name}: ${error}`)
    }

    // Fallback placeholder
    const placeholder = `/placeholder.svg?height=300&width=200&text=${encodeURIComponent(character.Name)}`
    setCharacterImages((prev) => ({ ...prev, [character.Name]: placeholder }))
    return placeholder
  }

  const generateSceneImage = async (scene: Scene) => {
    try {
      addToLog(`🎬 Generating image for Scene ${scene.Scene}...`)

      const response = await fetch("/api/generate-images", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ description: scene.Description }),
      })

      if (response.ok) {
        const data = await response.json()
        if (data.base64_image) {
          const imageUrl = `data:image/png;base64,${data.base64_image}`
          setSceneImages((prev) => ({ ...prev, [scene.Scene]: imageUrl }))
          addToLog(`✅ Generated ${data.type || "AI"} image for Scene ${scene.Scene}`)
          return imageUrl
        }
      } else {
        const errorText = await response.text()
        addToLog(`❌ Failed to generate image for Scene ${scene.Scene}: ${errorText}`)
      }
    } catch (error) {
      addToLog(`❌ Error generating image for Scene ${scene.Scene}: ${error}`)
    }

    // Fallback placeholder
    const placeholder = `/placeholder.svg?height=200&width=350&text=Scene ${scene.Scene}`
    setSceneImages((prev) => ({ ...prev, [scene.Scene]: placeholder }))
    return placeholder
  }

  const generateAllImages = async () => {
    if (!storyData) return

    setIsGeneratingImages(true)
    setGenerationProgress("Starting image generation...")
    setGenerationLog([])

    try {
      addToLog("🚀 Starting image generation process...")

      // Generate character images
      if (storyData.Characters.length > 0) {
        setGenerationProgress("Generating character images...")
        addToLog(`👥 Generating images for ${storyData.Characters.length} characters...`)

        for (const character of storyData.Characters) {
          if (!characterImages[character.Name]) {
            await generateCharacterImage(character)
            // Small delay between requests
            await new Promise((resolve) => setTimeout(resolve, 1000))
          } else {
            addToLog(`⏭️ Skipping ${character.Name} (already generated)`)
          }
        }
      }

      // Generate scene images
      if (storyData.Scenes.length > 0) {
        setGenerationProgress("Generating scene images...")
        addToLog(`🎬 Generating images for ${storyData.Scenes.length} scenes...`)

        for (const scene of storyData.Scenes) {
          if (!sceneImages[scene.Scene]) {
            await generateSceneImage(scene)
            // Small delay between requests
            await new Promise((resolve) => setTimeout(resolve, 1000))
          } else {
            addToLog(`⏭️ Skipping Scene ${scene.Scene} (already generated)`)
          }
        }
      }

      setGenerationProgress("Image generation complete!")
      addToLog("🎉 All images generated successfully!")
    } catch (error) {
      addToLog(`❌ Error during image generation: ${error}`)
      setGenerationProgress("Error generating images")
    } finally {
      setIsGeneratingImages(false)
      setTimeout(() => setGenerationProgress(""), 3000)
  
[truncated — 11590 more characters]
```

### app/api/cloudinary/route.ts

```typescript
// app/api/cloudinary/route.ts
import { NextResponse } from 'next/server'
import { v2 as cloudinary } from 'cloudinary'

// configure from your env
cloudinary.config({
  cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
  api_key:    process.env.CLOUDINARY_API_KEY,
  api_secret: process.env.CLOUDINARY_API_SECRET,
})

export async function POST(request: Request) {
  const { imageBase64 } = await request.json()
  const uploadResult = await cloudinary.uploader.upload(imageBase64, {
    folder: 'your-folder',
    // any other options…
  })
  return NextResponse.json({ url: uploadResult.secure_url })
}

```

### app/api/generate-video/route.ts

```typescript
import { NextRequest, NextResponse } from "next/server";

const VIDEO_SERVICE_URL =
  process.env.VIDEO_SERVICE_URL || "http://localhost:5002";

/**
 * POST /api/video
 * Proxies the request to the Flask service at 5002.
 * Body: { prompt: string, imageUrl: string, aspectRatio?: string }
 */
export async function POST(request: NextRequest) {
  console.log("🛎️  [Next] /api/video hit");

  // 1) read and sanity-check body
  const { prompt, imageUrl, aspectRatio = "16:9" } = await request.json();
  if (!prompt || !imageUrl) {
    return NextResponse.json(
      { error: "Prompt and image URL are required" },
      { status: 400 },
    );
  }

  // 2) forward to Flask
  const upstream = await fetch(`${VIDEO_SERVICE_URL}/api/video`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt, imageUrl, aspectRatio }),
  });

  // 3) propagate upstream failures verbatim
  if (!upstream.ok) {
    const text = await upstream.text();
    console.error("⚠️  Upstream error:", upstream.status, text);
    return NextResponse.json(
      { error: text || "Video service error" },
      { status: upstream.status },
    );
  }

  // 4) return the JSON { success, videoUrl } from Flask
  const data = await upstream.json();
  return NextResponse.json({
    success: true,
    videoUrl: data.videoUrl ?? data.video_url,
  });
}

```

[54 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]