# Project export: Vibe Video

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: TreeHacks 2026
- Tagline: Let AI autonomously edit entire videos for you
- Devpost: https://devpost.com/software/vibe-video-5ndrj4
- GitHub: https://github.com/Arihan10/Vibe-Video-Test
- Team: 1 GitHub contributor(s) — Arihan Sharma (83 commits)

## Devpost submission (written by the team)

### Inspiration

Video editing is stuck in the past. Tools like Premiere and Final Cut are powerful but painfully manual — you spend hours dragging clips around a timeline for what should be a creative process. Meanwhile, coding got "vibe coding" with Cursor. We asked: why can't video editing work the same way? Throw in your raw footage, get a real edit back, then iterate on it conversationally.

### What it does

Vibe Video is an AI-powered video editor that turns raw clips into a fully edited video — then lets you refine it like you'd refine code in Cursor. Drop in your footage, and the AI assembles a coherent edit with cuts, ordering, and flow. From there, you can edit the transcript directly to change the video, give natural language instructions ("make the intro punchier," "cut everything after the second interview clip"), and watch your changes apply in real time. It's vibe editing.

### How we built it

We built the frontend as a web app with a transcript-based editing interface that mirrors the Cursor-style inline editing experience. On the backend, we use AI to analyze and transcribe the uploaded clips, then intelligently sequence and cut them into a cohesive edit. The transcript serves as the single source of truth — edits to the text propagate back to the video timeline, so changing words literally changes the video.

### Challenges we ran into

Video processing is slow. Getting the feedback loop tight enough that editing felt conversational rather than batch-job-and-wait was a constant battle. Syncing transcript edits back to precise video cuts without weird jumps or artifacts was also trickier than expected — off-by-a-frame errors add up fast.

### Accomplishments we're proud of

The core loop actually feels good. You can go from a pile of raw clips to a watchable edit in minutes, and the transcript-driven editing is genuinely intuitive. The fact that it works end-to-end in a weekend is something we're proud of.

### What we learned

Working with video programmatically is a whole different beast compared to text or images. We gained a much deeper appreciation for how hard real-time media manipulation is — and how much opportunity there is to make it better with AI.

### What's next

Multi-track editing, AI-suggested b-roll and transitions, and support for longer-form content like podcasts and vlogs. We also want to add a collaborative mode where multiple people can vibe-edit the same project. The dream is to make video editing as fast as thought.

## README (from the GitHub repository)

# Vibe-Video

## Backend (FastAPI)

### Setup
- Python: **3.13 recommended** (your install log shows Python 3.14 fails building `pydantic-core`)
- **FFmpeg** must be installed and available in PATH (for automatic audio extraction from video files)
- Create and activate a virtualenv (recommended)
- Install deps:
  - `pip install -r backend/requirements.txt`
- Set environment variables:
  - `ASSEMBLYAI_API_KEY`
  - `GEMINI_API_KEY`


You can use the template at `backend/env.example` and copy it to a local `.env` in `backend/` (or export vars in your shell).

If you must stay on Python 3.14, one workaround is:
- `export PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`
but the more reliable fix is using Python 3.13.

#### Using pyenv (recommended)
If you use `pyenv`, this repo includes `backend/.python-version` so `cd backend` will automatically select Python **3.13.1** (once pyenv is initialized in your shell).

If `python3 -V` still shows 3.14 inside `backend/`, your shell likely isn't loading pyenv. Add the following to your dotfiles and restart the terminal:

- `~/.zprofile`:
  - `export PYENV_ROOT="$HOME/.pyenv"`
  - `export PATH="$PYENV_ROOT/bin:$PATH"`
  - `eval "$(pyenv init --path)"`
- `~/.zshrc`:
  - `eval "$(pyenv init - zsh)"`

Then recreate the venv (from `backend/`):
- `rm -rf .venv`
- `python3 -m venv .venv`
- `source .venv/bin/activate`
- `python -m pip install -r requirements.txt`

### Run
From the `backend/` directory:
- `uvicorn app.main:app --reload`

### API
- Health: `GET /health`
- Subtitle pipeline: `POST /api/v1/subtitle`

Example:
- `curl -F "file=@/path/to/video.mp4" "http://localhost:8000/api/v1/subtitle"`

Response includes:
- Full transcript: `text`
- Word timestamps: `words[]`
- Re-segmented phrases: `phrases[]`


## Detected evidence (automated analysis)

Indexed codebase: 94 recognized source files, 794 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — 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

## Codebase structure (from repository index)

### Files (119 of 119)

```
.gitignore
backend/.python-version
backend/app/analysis/__init__.py
backend/app/analysis/clips.py
backend/app/analysis/gemini.py
backend/app/analysis/models.py
backend/app/analysis/service.py
backend/app/config.py
backend/app/editing/__init__.py
backend/app/editing/claude.py
backend/app/editing/converter.py
backend/app/editing/models.py
backend/app/editing/service.py
backend/app/main.py
backend/app/sequencing/__init__.py
backend/app/sequencing/models.py
backend/app/sequencing/service.py
backend/app/transcription/assemblyai.py
backend/app/transcription/audio.py
backend/app/transcription/models.py
backend/app/transcription/segmenter.py
backend/app/transcription/service.py
backend/main.py
backend/pyproject.toml
backend/README.md
backend/requirements.txt
backend/uv.lock
FIXED_TIMELINE_JSON.md
frontend/.gitignore
frontend/app/api/adobe/create-job/route.ts
frontend/app/api/adobe/download-url/route.ts
frontend/app/api/adobe/job-status/[id]/route.ts
frontend/app/api/adobe/s3-download/route.ts
frontend/app/api/adobe/s3-upload/route.ts
frontend/app/api/adobe/upload-request/route.ts
frontend/app/api/ai/route.ts
frontend/app/api/save-audio/route.ts
frontend/app/api/save-edit/route.ts
frontend/app/api/transcribe/route.ts
frontend/app/globals.css
frontend/app/layout.tsx
frontend/app/page.tsx
frontend/ARCHITECTURE.md
frontend/AUDIO_SYNC_DEBUG.md
frontend/BACKEND_REQUIREMENTS.md
frontend/BROLL_INTEGRATION.md
frontend/components/ai/AIAssistant.tsx
frontend/components/ai/ChatMessage.tsx
frontend/components/CaptionOverlay.tsx
frontend/components/ExportButton.tsx
frontend/components/ExportDialog.tsx
frontend/components/SegmentDisplay.tsx
frontend/components/SimpleUpload.tsx
frontend/components/Studio.tsx
frontend/components/timeline/AudioWaveform.tsx
frontend/components/timeline/EffectPropertiesModal.tsx
frontend/components/timeline/EffectsDropdown.tsx
frontend/components/timeline/Playhead.tsx
frontend/components/timeline/Timeline.tsx
frontend/components/timeline/TimelineClip.tsx
frontend/components/timeline/TimelineEffect.tsx
frontend/components/timeline/TimelineHeader.tsx
frontend/components/timeline/TimelineLayer.tsx
frontend/components/timeline/TimeRuler.tsx
frontend/components/VideoPlayer.tsx
frontend/components/VideoPreview.tsx
frontend/components/VideoUpload.tsx
frontend/contexts/EditorContext.tsx
frontend/contexts/PlayerContext.tsx
frontend/eslint.config.mjs
frontend/HOW_TO_DEBUG_BACKEND_RESPONSE.md
frontend/INTEGRATION_STATUS.md
frontend/lib/api/upload.ts
frontend/lib/audio-playback.ts
frontend/lib/backend_response.json
frontend/lib/caption-generator.ts
frontend/lib/color-grading-presets.ts
frontend/lib/data-loader.ts
frontend/lib/data.ts
frontend/lib/effects-registry.ts
frontend/lib/export/audio-processor.ts
frontend/lib/export/export-video.ts
frontend/lib/export/ffmpeg-helper.ts
frontend/lib/export/frame-capture.ts
frontend/lib/export/index.ts
frontend/lib/export/README.md
frontend/lib/export/render-surface.ts
frontend/lib/file-mapping.ts
frontend/lib/kenny_recordings.json
frontend/lib/kenny_timeline.json
frontend/lib/mockTranscript.json
frontend/lib/mockTranscript2.json
frontend/lib/mockTranscript3.json
frontend/lib/player-api.ts
frontend/lib/reese_recordings_ori.json
frontend/lib/reese_recordings.json
frontend/lib/sampleEdit.json
frontend/lib/sampleEdit2.json
frontend/lib/sampleTranscript.json
frontend/lib/storage/transcripts.ts
frontend/lib/temp.txt
frontend/lib/timeline copy.json
frontend/lib/timeline_ori.json
frontend/lib/timeline-state.ts
frontend/lib/timeline.json
frontend/lib/types.ts
frontend/lib/voice-enhancement.ts
frontend/lib/webgl-renderer.ts
frontend/MIGRATION_COMPLETE.md
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/stores/editorStore.ts
frontend/tsconfig.json
frontend/tsconfig.tsbuildinfo
IMPLEMENTATION_SUMMARY.md
README.md
TESTING_INSTRUCTIONS.md
```

### Dependencies

- backend/requirements.txt: annotated-types@==0.7.0, anyio@==4.12.1, certifi@==2026.1.4, charset-normalizer@==3.4.4, click@==8.3.1, fastapi@==0.115.8, google-auth@==2.47.0, google-genai@==0.8.0, h11@==0.16.0, httpcore@==1.0.9, httptools@==0.7.1, httpx@==0.28.1, idna@==3.11, pyasn1@==0.6.2, pyasn1_modules@==0.4.2, pydantic@==2.10.6, pydantic_core@==2.27.2, pydantic-settings@==2.7.1, python-dotenv@==1.2.1, python-multipart@==0.0.9, PyYAML@==6.0.3, requests@==2.32.3, rsa@==4.9.1, starlette@==0.45.3, typing_extensions@==4.15.0, urllib3@==2.6.3, uvicorn@==0.34.0, uvloop@==0.22.1, watchfiles@==1.1.1, websockets@==14.2
- frontend/package.json: @ffmpeg/ffmpeg@^0.12.15, @ffmpeg/util@^0.12.2, @lexical/react@^0.39.0, @tailwindcss/postcss@^4, @twick/browser-render@^0.15.7, @twick/studio@^0.15.7, @types/crypto-js@^4.2.2, @types/node@^20, @types/react@^19, @types/react-dom@^19, assemblyai@^4.22.1, crypto-js@^4.2.0, eslint@^9, eslint-config-next@16.1.4, form-data@^4.0.5, lexical@^0.39.0, next@16.1.4, react@19.2.3, react-dom@19.2.3, tailwindcss@^4, typescript@^5, zustand@^5.0.10

### Recent commits (newest first)

- fixed export
- fixed playback and optimized export
- post hackathon fixes
- Merge branch 'main' of https://github.com/Arihan10/Vibe-Video
- merge
- working changes arihan sharma
- ia
- ai
- feat: enhance AIAssistant and ChatMessage components with improved styling and loading indicators; add detailed logging for audio processing and export functions
- Merge branch 'main-2'
- feat: implement layer reordering functionality in timeline component
- feat: add color grading presets and enhance effects management in timeline components
- ia
- feat: synchronize player duration and current time with editor updates
- feat: enhance AIAssistant with loading state, auto-scroll, and error handling for AI responses
- chore: update backend requirements.txt
- Merge branch 'main' of github.com:Arihan10/Vibe-Video
- fix: effects added to the t=selected clip not t=0
- better dropdown
- merge

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

### TESTING_INSTRUCTIONS.md

```markdown
# Testing Instructions - Unified State Management

## Quick Verification

The data loader has been tested and verified:
- ✅ 15 clips created from timeline.json
- ✅ 15 segments with words extracted from reese_recordings.json
- ✅ Total duration: 66.74 seconds
- ✅ All timeline entries processed successfully

## Run the Application

```bash
cd frontend
npm run dev
```

Then open http://localhost:3000

## What to Look For

### 1. Console Logs
Open browser console (F12). You should see:
```
📊 Data Loader Results: {
  totalClips: 15,
  totalSegments: 15,
  duration: 66.74,
  firstClip: {...},
  firstSegment: {...}
}
```

### 2. Transcript Panel (Left Side)
- Should display continuous text from all segments
- Words should be clickable
- Current word should highlight in blue during playback
- Hover should show underline
- Top should show "15 segments"

### 3. Timeline (Bottom)
- Should show 15 clip blocks
- Each block should have thumbnails (if generated)
- Total timeline length: ~67 seconds
- Clips should be positioned continuously (no gaps)

### 4. Video Preview (Center)
- Should load video from `/videos/IMG_5243.MOV`
- Video should play correctly
- Scrubbing should update highlighted word

## Interactive Tests

### Test 1: Word Highlighting
1. Click play
2. Watch transcript - current word should highlight in blue
3. Timeline playhead should move in sync

### Test 2: Seek by Clicking Words
1. Click any word in transcript
2. Video should jump to that word's timestamp
3. Timeline playhead should update

### Test 3: Scrub Timeline
1. Drag timeline playhead
2. Transcript should highlight corresponding word
3. Video should show correct frame

## Data Structure Verification

### Check Zustand DevTools (if installed)
1. Install Redux DevTools Extension
2. Look for "EditorStore" in DevTools
3. Inspect state:
   - `segments` array should have 15 items
   - `clips` array should have 15 items
   - `duration` should be ~66.74

### Manual Console Check
```javascript
// In browser console:
window.useEditorStore = require('@/stores/editorStore').useEditorStore;

// Get current state
const state = window.useEditorStore.getState();
console.log('Segments:', state.segments.length);
console.log('Clips:', state.clips.length);
console.log('Duration:', state.duration);
console.log('First segment:', state.segments[0]);
```

## Troubleshooting

### Video doesn't load
- Verify `/videos/IMG_5243.MOV` exists in `frontend/public/videos/`
- Check browser console for 404 errors
- Video path in data-loader.ts line 103: `src: '/videos/IMG_5243.MOV'`

### No words showing in transcript
- Check console for "Data Loader Results" log
- Verify `segments` array is not empty
- Check for errors in data-loader.ts

### Timeline is empty
- Check console for Timeline render logs
- Verify `clips` array has data
- Check Timeline.tsx is using Zustand store

### Words don't highlight during playback
- Verify PlayerContext is providing currentTime
- Check SegmentDisplay.tsx activeTimelineWord calculat
[truncated — 874 more characters]
```

### FIXED_TIMELINE_JSON.md

```markdown
# ✅ FIXED: All Components Now Use timeline.json

## What Was Changed

### 1. EditorContext.tsx - NOW USES TIMELINE.JSON ✅
**Before:**
```typescript
import sampleEditData from '@/lib/sampleEdit.json';
import mockTranscript2 from '@/lib/mockTranscript2.json';

const [editData, setEditData] = useState<EditData>(() =>
  migrateLegacyData(sampleEditData as unknown as EditData)
);
```

**After:**
```typescript
import { useEditorStore } from '@/stores/editorStore';

// Get data from Zustand store (which loads from timeline.json)
const zustandClips = useEditorStore(state => state.clips);
const zustandSegments = useEditorStore(state => state.segments);
const zustandDuration = useEditorStore(state => state.duration);

// Convert to EditorContext format
const [editData, setEditData] = useState<EditData>(() =>
  convertZustandToEditData(zustandClips, zustandSegments, zustandDuration)
);
```

**Result:** EditorContext is now a wrapper around Zustand store, which loads from `timeline.json` + `reese_recordings.json`

### 2. VideoPlayer.tsx - NOW USES TIMELINE.JSON ✅
**Before:**
```typescript
import editData from "@/lib/sampleEdit.json";
const [duration] = useState(editData.duration);
```

**After:**
```typescript
import { useEditorStore } from "@/stores/editorStore";

const clips = useEditorStore(state => state.clips);
const duration = useEditorStore(state => state.duration);
```

### 3. save-edit API - NOW SAVES TO project.json ✅
**Before:**
```typescript
const filePath = path.join(process.cwd(), "lib", "sampleEdit.json");
```

**After:**
```typescript
const filePath = path.join(process.cwd(), "lib", "project.json");
// Saves format: { clips, segments, duration }
```

## Data Flow (COMPLETE)

```
┌─────────────────────────────────────────────┐
│  Source Files (User's Timeline Data)       │
├─────────────────────────────────────────────┤
│  • reese_recordings.json                    │
│  • timeline.json                            │
└─────────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────────┐
│  Data Loader (data-loader.ts)              │
├─────────────────────────────────────────────┤
│  • Processes timeline array                 │
│  • Extracts words from reese_recordings     │
│  • Converts ms → seconds                    │
│  • Creates clips[] and segments[]           │
└─────────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────────┐
│  Zustand Store (editorStore.ts)            │
├─────────────────────────────────────────────┤
│  State:                                     │
│  • clips: TimelineClip[] (15 items)         │
│  • segments: Segment[] (15 items)           │
│  • duration: 66.74s                         │
└─────────────────────────────────────────────┘
              ↓
      ┌───────┴────────┐
      ↓                ↓
┌──────────┐    ┌──────────────┐
│ Direct   │    │ EditorContext│
│ Zustand  │    │ (Wrapper)    │
│ Users    │    │              │
└────
[truncated — 2139 more characters]
```

### backend/pyproject.toml

```
[project]
name = "backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13.1"
dependencies = []

```

### backend/requirements.txt

```
annotated-types==0.7.0
anyio==4.12.1
certifi==2026.1.4
charset-normalizer==3.4.4
click==8.3.1
fastapi==0.115.8
google-auth==2.47.0
google-genai==0.8.0
h11==0.16.0
httpcore==1.0.9
httptools==0.7.1
httpx==0.28.1
idna==3.11
pyasn1==0.6.2
pyasn1_modules==0.4.2
pydantic==2.10.6
pydantic-settings==2.7.1
pydantic_core==2.27.2
python-dotenv==1.2.1
python-multipart==0.0.9
PyYAML==6.0.3
requests==2.32.3
rsa==4.9.1
starlette==0.45.3
typing_extensions==4.15.0
urllib3==2.6.3
uvicorn==0.34.0
uvloop==0.22.1
watchfiles==1.1.1
websockets==14.2

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@ffmpeg/ffmpeg": "^0.12.15",
    "@ffmpeg/util": "^0.12.2",
    "@lexical/react": "^0.39.0",
    "@twick/browser-render": "^0.15.7",
    "@twick/studio": "^0.15.7",
    "@types/crypto-js": "^4.2.2",
    "assemblyai": "^4.22.1",
    "crypto-js": "^4.2.0",
    "form-data": "^4.0.5",
    "lexical": "^0.39.0",
    "next": "16.1.4",
    "react": "19.2.3",
    "react-dom": "19.2.3",
    "zustand": "^5.0.10"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.4",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### backend/main.py

```python
def main():
    print("Hello from backend!")


if __name__ == "__main__":
    main()

```

### frontend/app/page.tsx

```typescript
import Studio from '../components/Studio';

export default function Home() {
  return (
    <Studio/>
  );
}

```

### frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Vibe Video Editor",
  description: "Create your own video content with ease",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### backend/app/main.py

```python
from fastapi import FastAPI, File, HTTPException, Query, UploadFile
from fastapi.middleware.cors import CORSMiddleware

from app.config import settings
from app.transcription.service import TranscriptionError, transcribe
from app.transcription.models import TranscriptionResult
from app.analysis.service import AnalysisError, analyze
from app.analysis.models import AnalysisResult
from app.sequencing.service import analyze_sequence, SequencingError
from app.sequencing.models import SequenceResult
from app.editing.service import edit_clips, refine_edit, EditingError
from app.editing.models import EditResult, RefineRequest

app = FastAPI(title="Vibe-Video API", version="0.1.0")

# Configure CORS to allow frontend to access backend
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # Next.js dev server
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/health")
def health():
    return {"status": "ok"}


@app.post("/api/subtitle", response_model=TranscriptionResult)
async def subtitle(file: UploadFile = File(...)):
    if not file.filename:
        raise HTTPException(status_code=400, detail="Missing file")
    if not settings.assemblyai_api_key:
        raise HTTPException(status_code=500, detail="ASSEMBLYAI_API_KEY is not set")
    if not settings.gemini_api_key:
        raise HTTPException(status_code=500, detail="GEMINI_API_KEY is not set")

    try:
        result = await transcribe(
            file.file,
            file.filename,
            assemblyai_api_key=settings.assemblyai_api_key,
            gemini_api_key=settings.gemini_api_key,
        )
    except TranscriptionError as e:
        raise HTTPException(status_code=502, detail=str(e))
    finally:
        await file.close()

    return result


@app.post("/api/analyze", response_model=AnalysisResult)
async def analyze_video(file: UploadFile = File(...)):
    """Analyze a video: generate summary and transcription in parallel."""
    if not file.filename:
        raise HTTPException(status_code=400, detail="Missing file")
    if not settings.assemblyai_api_key:
        raise HTTPException(status_code=500, detail="ASSEMBLYAI_API_KEY is not set")
    if not settings.gemini_api_key:
        raise HTTPException(status_code=500, detail="GEMINI_API_KEY is not set")

    try:
        result = await analyze(
            file.file,
            file.filename,
            assemblyai_api_key=settings.assemblyai_api_key,
            gemini_api_key=settings.gemini_api_key,
        )
    except AnalysisError as e:
        raise HTTPException(status_code=502, detail=str(e))
    finally:
        await file.close()

    return result


@app.post("/api/sequence", response_model=SequenceResult)
async def sequence_analyze(
    files: list[UploadFile] = File(...),
    max_concurrent: int | None = Query(None, description="Max concurrent analyses"),
):
    """Analyze multiple video clips in parallel."""
    if not files:
        raise HTTPException(status_code=400, detail="No files provided")
    if not settings.assemblyai_api_key:
        raise HTTPException(status_code=500, detail="ASSEMBLYAI_API_KEY is not set")
    if not settings.gemini_api_key:
        raise HTTPException(status_code=500, detail="GEMINI_API_KEY is not set")

    # Build clip list: (clip_id, file_obj, filename)
    clips = []
    for i, f in enumerate(files):
        if not f.filename:
            raise HTTPException(status_code=400, detail=f"File at index {i} has no filename")
        clips.append((f.filename, f.file, f.filename))

    try:
        result = await analyze_sequence(
            clips,
            assemblyai_api_key=settings.assemblyai_api_key,
            gemini_api_key=settings.gemini_api_key,
            max_concurrent=max_concurrent,
        )
    except SequencingError as e:
        raise HTTPException(status_code=502, detail=str(e))
    finally:
        for f in files:
            await f.close()

    return result


@app.post("/api/edit", response_model=EditResult)
async def edit_video(
    files: list[UploadFile] = File(...),
    max_concurrent: int | None = Query(None, description="Max concurrent analyses"),
):
    """
    Full editing pipeline: analyze clips → convert to editor format → Claude edits.
    
    Accepts multiple video clips, analyzes them in parallel, then sends the
    formatted data to Claude Opus 4.5 for intelligent editing decisions.
    
    Returns:
        - sequencing: Claude's editing decisions (final_transcript + timeline)
        - metadata: Full clip data with transcriptions, segments, and word timestamps
    """
    if not files:
        raise HTTPException(status_code=400, detail="No files provided")
    if not settings.assemblyai_api_key:
        raise HTTPException(status_code=500, detail="ASSEMBLYAI_API_KEY is not set")
    if not settings.gemini_api_key:
        raise HTTPException(status_code=500, detail="GEMINI_API_KEY is not set")
    if not settings.anthropic_api_key:
        raise HTTPException(status_code=500, detail="ANTHROPIC_API_KEY is not set")

    # Build clip list: (clip_id, file_obj, filename)
    clips = []
    for i, f in enumerate(files):
        if not f.filename:
            raise HTTPException(status_code=400, detail=f"File at index {i} has no filename")
        clips.append((f.filename, f.file, f.filename))

    try:
        result = await edit_clips(
            clips,
            assemblyai_api_key=settings.assemblyai_api_key,
            gemini_api_key=settings.gemini_api_key,
            anthropic_api_key=settings.anthropic_api_key,
            max_concurrent=max_concurrent,
        )
    except EditingError as e:
        raise HTTPException(status_code=502, detail=str(e))
    finally:
        for f in files:
            await f.close()

    return result


@app.post("/api/ai", response_model=EditResult)
async def refine_video_edit(request: RefineRequest):
    """
    Refine an existing edit with user feedback.
    
    Cont
[truncated — 612 more characters]
```

### frontend/lib/export/index.ts

```typescript
/**
 * Export Module Barrel Exports
 *
 * Provides a single entry point for all export-related functionality.
 */

export * from "./render-surface";
export * from "./ffmpeg-helper";
export * from "./frame-capture";
export * from "./export-video";
export * from "./audio-processor";



```

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