creepyblues avatar

batch-format-fit

Batch format suitability analysis using GPT-4o across 5 content formats (Film, TV Series, Animation,

作者 creepyblues|オープンソース

Batch Format Fit

This skill orchestrates batch format suitability analysis, enabling large-scale format scoring without manual UI interaction.

When to Use This Skill

  • Analyzing format fit for multiple titles at once (20+)
  • Filling gaps (titles without format analysis)
  • Format-specific discovery (finding all titles good for microdrama)
  • Catalog reports (format distribution across catalog)
  • New title processing (recently added titles)
  • Porting the feature to another app

What It Does

For each title, the format fit analyzer:

  1. Collects title data (synopsis, genre, content analysis)
  2. Deconstructs story with format-specific attributes using GPT-4o
  3. Scores suitability across 5 content formats
  4. Provides 7-dimension analysis per format
  5. Saves to title_format_fit table

5 Content Formats:

FormatDescriptionKey Factors
Film90-150 min featureSelf-contained story, production scale
TV Series8-16 episodesCharacter depth, arc potential
AnimationAnimated adaptationVisual complexity, world-building
Microdrama60-120s vertical videoCliffhangers, trope alignment
Audio DramaPodcast/audio fictionDialogue-driven, voice potential

7 Scoring Dimensions per Format:

  • Narrative structure
  • Character suitability
  • Visual requirements
  • Pacing fit
  • Production feasibility
  • Audience alignment
  • Genre fit

Commands

/batch-format-fit --missing                     # Titles without analysis
/batch-format-fit --format=microdrama           # Focus on specific format
/batch-format-fit --limit=50                    # Limit batch size
/batch-format-fit --min-score=80                # Re-analyze low scores
/batch-format-fit --cost-estimate               # Estimate cost before running
/batch-format-fit --dry-run                     # Preview without executing
/batch-format-fit --report                      # Generate format distribution report
/batch-format-fit --recent=7                    # Titles added in last 7 days

Edge Function Reference

Location: supabase/functions/format-fit-engine/index.ts

Endpoint: POST /functions/v1/format-fit-engine

Request Body:

interface FormatFitRequest {
  title_id: string;
  // Optional: force regeneration even if analysis exists
  force?: boolean;
}

Response:

interface FormatFitResponse {
  success: boolean;
  title_id: string;
  scores: {
    film: number;          // 0-100
    tv_series: number;
    animation: number;
    microdrama: number;
    audio_drama: number;
  };
  analyses: {
    film: FormatAnalysis;
    tv_series: FormatAnalysis;
    animation: FormatAnalysis;
    microdrama: MicrodramaAnalysis;
    audio_drama: FormatAnalysis;
  };
  story_deconstruction: {
    narrative_complexity: string;
    character_count: number;
    visual_intensity: string;
    pacing_type: string;
    setting_production_cost: string;
  };
  data_completeness: number;
  mode: 'rich' | 'limited';
  processing_time_ms: number;
  cost_estimate: number;
}

interface FormatAnalysis {
  score: number;
  dimensions: {
    narrative_structure: number;
    character_suitability: number;
    visual_requirements: number;
    pacing_fit: number;
    production_feasibility: number;
    audience_alignment: number;
    genre_fit: number;
  };
  strengths: string[];
  challenges: string[];
  recommendation: string;
}

interface MicrodramaAnalysis extends FormatAnalysis {
  cliffhanger_potential: number;
  trope_alignment: string[];
  episode_structure_fit: number;
  vertical_filming_compatibility: number;
  target_platform_fit: string;  // ReelShort, DramaBox, etc.
}

Cost Estimation

Model: GPT-4o (2 API calls per title) Cost: ~$0.01-0.015 per title

Batch SizeEst. TimeEst. Cost
10 titles~3 min$0.12
50 titles~15 min$0.60
100 titles~30 min$1.20
500 titles~2.5 hours$6.00

Batch Workflow

Step 1: Identify Titles to Process

-- Titles without format analysis
SELECT t.title_id, t.title_name_en, t.views
FROM titles t
LEFT JOIN title_format_fit f ON t.title_id = f.title_id
WHERE f.id IS NULL
ORDER BY t.views DESC NULLS LAST
LIMIT 50;

-- Titles good for specific format (discovery)
SELECT t.title_name_en, f.microdrama_score
FROM titles t
JOIN title_format_fit f ON t.title_id = f.title_id
WHERE f.microdrama_score >= 80
ORDER BY f.microdrama_score DESC;

-- Recently added titles
SELECT title_id, title_name_en
FROM titles
WHERE created_at > NOW() - INTERVAL '7 days';

Step 2: Batch Analysis

const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;

async function analyzeFormatFitBatch(titleIds, options = {}) {
  const results = { success: [], failed: [], skipped: [] };
  let totalCost = 0;

  // Cost estimation
  if (options.costEstimateOnly) {
    const estimatedCost = titleIds.length * 0.012;
    console.log(`Estimated cost: $${estimatedCost.toFixed(2)} for ${titleIds.length} titles`);
    return { estimatedCost, titleCount: titleIds.length };
  }

  for (const titleId of titleIds) {
    try {
      const response = await fetch(`${SUPABASE_URL}/functions/v1/format-fit-engine`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${SUPABASE_ANON_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ title_id: titleId })
      });

      const data = await response.json();

      if (data.success) {
        results.success.push({
          title_id: titleId,
          scores: data.scores,
          best_format: getBestFormat(data.scores),
          cost: data.cost_estimate || 0.012
        });
        totalCost += data.cost_estimate || 0.012;
      } else {
        results.failed.push({
          title_id: titleId,
          error: data.error
        });
      }

      // Rate limiting
      await delay(2000);

    } catch (error) {
      results.failed.push({
        title_id: titleId,
        error: error.message
      });
    }

    // Budget check
    if (options.maxCost && totalCost >= options.maxCost) {
      console.log(`Budget limit reached: $${totalCost.toFixed(2)}`);
      break;
    }
  }

  return { results, totalCost };
}

function getBestFormat(scores) {
  const formats = ['film', 'tv_series', 'animation', 'microdrama', 'audio_drama'];
  return formats.reduce((best, format) =>
    scores[format] > scores[best] ? format : best
  , formats[0]);
}

Step 3: Generate Format Distribution Report

-- Format distribution across catalog
SELECT
  CASE
    WHEN f.film_score = GREATEST(f.film_score, f.tv_series_score, f.animation_score, f.microdrama_score, f.audio_drama_score)
    THEN 'Film'
    WHEN f.tv_series_score = GREATEST(f.film_score, f.tv_series_score, f.animation_score, f.microdrama_score, f.audio_drama_score)
    THEN 'TV Series'
    WHEN f.animation_score = GREATEST(f.film_score, f.tv_series_score, f.animation_score, f.microdrama_score, f.audio_drama_score)
    THEN 'Animation'
    WHEN f.microdrama_score = GREATEST(f.film_score, f.tv_series_score, f.animation_score, f.microdrama_score, f.audio_drama_score)
    THEN 'Microdrama'
    ELSE 'Audio Drama'
  END as best_format,
  COUNT(*) as title_count,
  ROUND(AVG(GREATEST(f.film_score, f.tv_series_score, f.animation_score, f.microdrama_score, f.audio_drama_score)), 1) as avg_best_score
FROM title_format_fit f
GROUP BY 1
ORDER BY title_count DESC;

-- Microdrama-ready titles (score >= 80)
SELECT
  t.title_name_en,
  f.microdrama_score,
  f.microdrama_analysis->>'target_platform_fit' as target_platform,
  f.microdrama_analysis->>'cliffhanger_potential' as cliffhanger_score
FROM titles t
JOIN title_format_fit f ON t.title_id = f.title_id
WHERE f.microdrama_score >= 80
ORDER BY f.microdrama_score DESC;

Console Output

Starting batch format fit analysis...

Configuration:
  Filter: missing analysis
  Limit: 50
  Budget: $1.00 max

Cost Estimate:
  Titles to process: 50
  Estimated cost: $0.60
  Proceed? (Y/n)

[1/50] Processing "재벌집 막내아들"
       Data completeness: 85%
       ✅ Analyzed
       Scores: Film=78, TV=92, Anim=65, Micro=71, Audio=58
       Best fit: TV Series (92%)
       Cost: $0.012

[2/50] Processing "Solo Leveling"
       Data completeness: 92%
       ✅ Analyzed
       Scores: Film=72, TV=85, Anim=95, Micro=68, Audio=52
       Best fit: Animation (95%)
       Cost: $0.011

...

Summary:
  ✅ Success: 48 titles
  ❌ Failed: 2 titles
  💰 Total cost: $0.58

Format Distribution:
  TV Series: 18 titles (38%)
  Animation: 12 titles (25%)
  Film: 10 titles (21%)
  Microdrama: 6 titles (12%)
  Audio Drama: 2 titles (4%)

High-potential Microdrama Titles (80+):
  - "Title A" (92%)
  - "Title B" (88%)
  - "Title C" (85%)

Database Schema

title_format_fit

CREATE TABLE title_format_fit (
  id UUID PRIMARY KEY,
  title_id UUID REFERENCES titles(title_id),
  film_score INTEGER,
  tv_series_score INTEGER,
  animation_score INTEGER,
  microdrama_score INTEGER,
  audio_drama_score INTEGER,
  film_analysis JSONB,
  tv_series_analysis JSONB,
  animation_analysis JSONB,
  microdrama_analysis JSONB,
  audio_drama_analysis JSONB,
  story_deconstruction JSONB,
  data_completeness INTEGER,
  mode_used TEXT,
  processing_time_ms INTEGER,
  cost_estimate NUMERIC,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

Error Handling

Common Errors

ErrorCauseResolution
Insufficient dataMissing synopsis/genreCollect data first via /batch-intelligence
API timeoutGPT-4o slow responseRetry with longer timeout
Rate limitToo many requestsIncrease delay between requests

Data Quality Pre-Check

-- Check data completeness for target titles
SELECT
  title_name_en,
  CASE
    WHEN synopsis IS NOT NULL AND LENGTH(synopsis) > 100
         AND genre IS NOT NULL
    THEN 'ready'
    ELSE 'needs data'
  END as status
FROM titles t
LEFT JOIN title_format_fit f ON t.title_id = f.title_id
WHERE f.id IS NULL;

Porting Guide

To port this feature to another app (e.g., Creator):

1. Service File Template

Create apps/[app]/src/services/formatFitService.ts:

import { supabase } from '@/integrations/supabase/client';

const FUNCTION_URL = `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/format-fit-engine`;

export interface FormatScores {
  film: number;
  tv_series: number;
  animation: number;
  microdrama: number;
  audio_drama: number;
}

export interface FormatFitResult {
  success: boolean;
  scores: FormatScores;
  analyses: Record<string, any>;
  data_completeness: number;
  mode: 'rich' | 'limited';
  processing_time_ms: number;
  cost_estimate: number;
  error?: string;
}

export async function analyzeFormatFit(titleId: string): Promise<FormatFitResult> {
  const { data: { session } } = await supabase.auth.getSession();

  const response = await fetch(FUNCTION_URL, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${session?.access_token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ title_id: titleId })
  });

  return response.json();
}

export function getBestFormat(scores: FormatScores): string {
  const formats = Object.entries(scores);
  formats.sort((a, b) => b[1] - a[1]);
  return formats[0][0];
}

export function estimateCost(titleCount: number): number {
  return titleCount * 0.012;
}

2. UI Component Template

Create apps/[app]/src/components/FormatFitButton.tsx:

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { analyzeFormatFit } from '@/services/formatFitService';
import { BarChart3, Loader2 } from 'lucide-react';
import { toast } from 'sonner';

interface Props {
  titleId: string;
  hasExistingAnalysis?: boolean;
  onSuccess?: (scores: FormatScores) => void;
}

export function FormatFitButton({ titleId, hasExistingAnalysis, onSuccess }: Props) {
  const [loading, setLoading] = useState(false);

  const handleAnalyze = async () => {
    setLoading(true);
    try {
      const result = await analyzeFormatFit(titleId);

      if (result.success) {
        const bestFormat = getBestFormat(result.scores);
        toast.success(`Analysis complete: Best fit is ${bestFormat}`, {
          description: `Score: ${result.scores[bestFormat]}%`
        });
        onSuccess?.(result.scores);
      } else {
        toast.error(result.error || 'Analysis failed');
      }
    } catch (error) {
      toast.error('Failed to analyze format fit');
    } finally {
      setLoading(false);
    }
  };

  return (
    <Button
      variant={hasExistingAnalysis ? 'outline' : 'default'}
      size="sm"
      onClick={handleAnalyze}
      disabled={loading}
      className={hasExistingAnalysis ? 'border-blue-500' : ''}
    >
      {loading ? (
        <Loader2 className="w-4 h-4 mr-2 animate-spin" />
      ) : (
        <BarChart3 className="w-4 h-4 mr-2" />
      )}
      {loading ? 'Analyzing...' : hasExistingAnalysis ? 'Re-analyze' : 'Format Fit'}
    </Button>
  );
}

3. Scores Display Component

import { FormatScores } from '@/services/formatFitService';

interface Props {
  scores: FormatScores;
}

export function FormatScoresDisplay({ scores }: Props) {
  const formats = [
    { key: 'film', label: 'Film', icon: '🎬' },
    { key: 'tv_series', label: 'TV Series', icon: '📺' },
    { key: 'animation', label: 'Animation', icon: '🎨' },
    { key: 'microdrama', label: 'Microdrama', icon: '📱' },
    { key: 'audio_drama', label: 'Audio Drama', icon: '🎧' },
  ];

  return (
    <div className="grid grid-cols-5 gap-4">
      {formats.map(({ key, label, icon }) => (
        <div key={key} className="text-center p-4 bg-gray-50 rounded-lg">
          <div className="text-2xl mb-2">{icon}</div>
          <div className="text-sm font-medium">{label}</div>
          <div className="text-2xl font-bold mt-1">
            {scores[key as keyof FormatScores]}%
          </div>
        </div>
      ))}
    </div>
  );
}

Related Skills

  • /batch-intelligence - Collect data before analysis for better results
  • /batch-comps - Generate comps alongside format analysis
  • /title-pipeline - Orchestrate full workflow
batch-format-fit - Claude Code・Cursor 対応の AIエージェント Skill | Agent Skills