name: batch-format-fit description: Batch format suitability analysis using GPT-4o across 5 content formats (Film, TV Series, Animation, Microdrama, Audio Drama). This skill should be used for analyzing format fit on multiple titles, generating catalog reports, or running overnight batch processing.
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:
- Collects title data (synopsis, genre, content analysis)
- Deconstructs story with format-specific attributes using GPT-4o
- Scores suitability across 5 content formats
- Provides 7-dimension analysis per format
- Saves to
title_format_fittable
5 Content Formats:
| Format | Description | Key Factors |
|---|---|---|
| Film | 90-150 min feature | Self-contained story, production scale |
| TV Series | 8-16 episodes | Character depth, arc potential |
| Animation | Animated adaptation | Visual complexity, world-building |
| Microdrama | 60-120s vertical video | Cliffhangers, trope alignment |
| Audio Drama | Podcast/audio fiction | Dialogue-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 Size | Est. Time | Est. 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
| Error | Cause | Resolution |
|---|---|---|
Insufficient data |
Missing synopsis/genre | Collect data first via /batch-intelligence |
API timeout |
GPT-4o slow response | Retry with longer timeout |
Rate limit |
Too many requests | Increase 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