Skip to main content

Overview

Video projects progress through multiple status stages as AI clips are generated and compiled. This page explains the status lifecycle, how to track real-time progress, and how to handle errors.

Status Values

status
string
The current state of the video project. Possible values:
  • draft - Initial state, ready to start generation
  • generating - AI clips being created in parallel
  • compiling - FFmpeg merging clips into final video
  • completed - Video ready for download
  • failed - Error occurred during generation

Status Lifecycle

1

draft

Initial state after project creation
  • Clips are configured but not processed
  • clipCount is set
  • estimatedCost is calculated
  • Ready to trigger generation
2

generating

AI clip generation in progress
  • Kling AI model processes each clip (2-5 minutes each)
  • Up to 3 clips generated in parallel
  • completedClipCount increments as clips finish
  • Transition clips generated between sequential clips
3

compiling

FFmpeg compilation in progress
  • All clips are merged into final video
  • Background music mixed at specified volume
  • AI-generated ambient audio added (if enabled)
  • Output encoded as H.264 MP4
4

completed

Video is ready
  • finalVideoUrl contains download link
  • thumbnailUrl contains preview image
  • durationSeconds shows total video length
  • actualCost records final cost in cents
5

failed

Error occurred
  • errorMessage contains failure reason
  • completedClipCount shows how many clips succeeded
  • Manual review required to retry

Querying Project Status

Fetch the current status from the database:
// From source: lib/db/queries.ts
import { db } from "@/lib/db";
import { videoProject } from "@/lib/db/schema";
import { eq } from "drizzle-orm";

async function getVideoProjectStatus(videoProjectId: string) {
  const project = await db.query.videoProject.findFirst({
    where: eq(videoProject.id, videoProjectId),
    columns: {
      id: true,
      name: true,
      status: true,
      clipCount: true,
      completedClipCount: true,
      estimatedCost: true,
      actualCost: true,
      finalVideoUrl: true,
      thumbnailUrl: true,
      durationSeconds: true,
      errorMessage: true,
      triggerRunId: true,
    },
  });
  
  return project;
}

Real-Time Progress Tracking

Use Trigger.dev’s real-time hooks to monitor generation progress:
// From source: components/video/video-progress.tsx
import { useRealtimeRun } from "@trigger.dev/react-hooks";

interface VideoProgressProps {
  videoProjectId: string;
  runId: string;
  publicAccessToken: string;
}

export function VideoProgress({ runId, publicAccessToken }: VideoProgressProps) {
  const { run } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
  });
  
  // Status metadata structure from trigger/video-orchestrator.ts:22-27
  const status = run?.metadata?.status as {
    step: "starting" | "generating" | "compiling" | "completed" | "failed";
    label: string;
    clipIndex?: number;
    totalClips?: number;
    progress?: number; // 0-100
  };
  
  return (
    <div className="space-y-4">
      <div>
        <p className="text-sm font-medium">{status?.label}</p>
        <p className="text-xs text-muted-foreground">
          {status?.step}
        </p>
      </div>
      
      {status?.progress !== undefined && (
        <div className="space-y-2">
          <div className="flex justify-between text-xs">
            <span>Progress</span>
            <span>{status.progress}%</span>
          </div>
          <progress
            value={status.progress}
            max={100}
            className="w-full"
          />
        </div>
      )}
      
      {status?.clipIndex !== undefined && (
        <p className="text-sm">
          Processing clip {status.clipIndex} of {status.totalClips}
        </p>
      )}
    </div>
  );
}

Progress Stages

The metadata.status object updates throughout generation:
StepLabelProgress
starting”Starting video generation…“5%
generating”Generating 8 clips…“10-70%
compiling”Compiling final video…“75-95%
completed”Video ready!“100%
failedError messageN/A

Database Schema

// From source: lib/db/schema.ts:247-302
export const videoProject = pgTable("video_project", {
  id: text("id").primaryKey(),
  status: text("status").notNull().default("draft"),
  
  // Progress tracking
  clipCount: integer("clip_count").notNull().default(0),
  completedClipCount: integer("completed_clip_count").notNull().default(0),
  
  // Cost tracking
  estimatedCost: integer("estimated_cost").notNull().default(0), // cents
  actualCost: integer("actual_cost"), // cents, set when completed
  
  // Output
  finalVideoUrl: text("final_video_url"),
  thumbnailUrl: text("thumbnail_url"),
  durationSeconds: integer("duration_seconds"),
  
  // Error handling
  errorMessage: text("error_message"),
  
  // Trigger.dev integration
  triggerRunId: text("trigger_run_id"),
  triggerAccessToken: text("trigger_access_token"),
});

Polling Pattern

For clients without WebSocket support, poll the database:
async function pollVideoStatus(videoProjectId: string): Promise<void> {
  const MAX_ATTEMPTS = 360; // 30 minutes (5s interval)
  const INTERVAL = 5000; // 5 seconds
  
  for (let i = 0; i < MAX_ATTEMPTS; i++) {
    const project = await getVideoProjectStatus(videoProjectId);
    
    if (project?.status === "completed") {
      console.log("Video ready!", project.finalVideoUrl);
      return;
    }
    
    if (project?.status === "failed") {
      throw new Error(`Video generation failed: ${project.errorMessage}`);
    }
    
    // Wait before next poll
    await new Promise(resolve => setTimeout(resolve, INTERVAL));
  }
  
  throw new Error("Video generation timeout after 30 minutes");
}
Avoid polling more frequently than every 5 seconds to prevent excessive database load.

Progress Calculation

Progress is calculated based on completed clips:
// From source: trigger/video-orchestrator.ts:155-162
function calculateProgress(
  completedClips: number,
  totalClips: number,
  stage: "generating" | "compiling"
): number {
  const clipProgress = (completedClips / totalClips) * 60; // 60% for clips
  const baseProgress = stage === "generating" ? 10 : 75;
  
  return Math.min(100, baseProgress + clipProgress);
}

Clip Status Tracking

Individual clip status can be monitored:
// From source: lib/db/schema.ts:308-366
export const videoClip = pgTable("video_clip", {
  id: text("id").primaryKey(),
  videoProjectId: text("video_project_id").notNull(),
  status: text("status").notNull().default("pending"),
  clipUrl: text("clip_url"), // Set when completed
  errorMessage: text("error_message"),
  sequenceOrder: integer("sequence_order").notNull(),
});

// Query all clips for a project
async function getVideoClipStatuses(videoProjectId: string) {
  const clips = await db.query.videoClip.findMany({
    where: eq(videoClip.videoProjectId, videoProjectId),
    orderBy: [asc(videoClip.sequenceOrder)],
    columns: {
      id: true,
      sequenceOrder: true,
      status: true,
      clipUrl: true,
      errorMessage: true,
    },
  });
  
  return clips;
}

Error Messages

Common error messages and their meanings:
Cause: Invalid videoProjectId or project was deletedSolution: Verify the project ID exists in your workspace
Cause: Video project has zero clips configuredSolution: Add at least one clip before triggering generation
Cause: Kling AI model took longer than 10 minutes to generate clipSolution: Retry generation. Check if source image URL is accessible.
Cause: Source image is not in a supported format (JPG, PNG, WebP)Solution: Re-upload image in a supported format
Cause: Error merging clips (corrupted video files, format mismatch)Solution: Check completedClipCount. If clips are missing, retry generation.
Cause: Supabase Storage is unavailable or quota exceededSolution: Check Supabase dashboard for storage limits and service status

Handling Failed Generations

When a video fails (status: "failed"):
  1. Check error message for the specific cause
  2. Review completed clips via completedClipCount
  3. Inspect individual clip errors in the video_clip table
  4. Delete and recreate the project if needed
  5. Contact support if errors persist
Failed videos do not automatically retry. Manual intervention is required to review and restart generation.

Monitoring Best Practices

Example: Complete Status Check

interface VideoStatusResponse {
  id: string;
  name: string;
  status: "draft" | "generating" | "compiling" | "completed" | "failed";
  progress: {
    completed: number;
    total: number;
    percentage: number;
  };
  output?: {
    videoUrl: string;
    thumbnailUrl: string;
    duration: number;
  };
  cost: {
    estimated: number; // cents
    actual?: number; // cents
  };
  error?: string;
}

async function getCompleteVideoStatus(
  videoProjectId: string
): Promise<VideoStatusResponse> {
  const project = await getVideoProjectStatus(videoProjectId);
  const clips = await getVideoClipStatuses(videoProjectId);
  
  if (!project) {
    throw new Error("Video project not found");
  }
  
  const percentage = project.clipCount > 0
    ? Math.round((project.completedClipCount / project.clipCount) * 100)
    : 0;
  
  return {
    id: project.id,
    name: project.name,
    status: project.status,
    progress: {
      completed: project.completedClipCount,
      total: project.clipCount,
      percentage,
    },
    output: project.finalVideoUrl ? {
      videoUrl: project.finalVideoUrl,
      thumbnailUrl: project.thumbnailUrl!,
      duration: project.durationSeconds!,
    } : undefined,
    cost: {
      estimated: project.estimatedCost,
      actual: project.actualCost ?? undefined,
    },
    error: project.errorMessage ?? undefined,
  };
}

Next Steps

Create Video

Create a new video project

Compile Video

Trigger video generation

Build docs developers (and LLMs) love