191 lines
5.7 KiB
TypeScript
191 lines
5.7 KiB
TypeScript
/**
|
|
* Video Processor Service
|
|
* Handles video ingestion: frame extraction, audio isolation, scene detection
|
|
*
|
|
* NOTE: In the deployed environment, heavy video processing would be delegated
|
|
* to an external service. This module provides the API interface and simulation
|
|
* for the web application layer.
|
|
*/
|
|
|
|
import { invokeLLM } from "./_core/llm";
|
|
|
|
export interface VideoMetadata {
|
|
fps: number;
|
|
totalFrames: number;
|
|
width: number;
|
|
height: number;
|
|
duration: number; // ms
|
|
codec: string;
|
|
}
|
|
|
|
export interface SceneCut {
|
|
frameIndex: number;
|
|
confidence: number;
|
|
type: "hard_cut" | "dissolve" | "fade";
|
|
}
|
|
|
|
/**
|
|
* Simulate video metadata extraction
|
|
* In production, this would use FFprobe or a media analysis service
|
|
*/
|
|
export async function extractVideoMetadata(videoUrl: string): Promise<VideoMetadata> {
|
|
// Simulated metadata - in production would call FFprobe via external service
|
|
return {
|
|
fps: 24,
|
|
totalFrames: 576, // ~24s at 24fps
|
|
width: 720,
|
|
height: 480,
|
|
duration: 24000,
|
|
codec: "h264",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Simulate frame extraction from video
|
|
* In production, this would use FFmpeg via an external processing service
|
|
* Returns URLs to extracted frame images stored in S3
|
|
*/
|
|
export async function extractFrames(
|
|
videoUrl: string,
|
|
startFrame: number,
|
|
endFrame: number,
|
|
outputPrefix: string
|
|
): Promise<string[]> {
|
|
// Simulate frame extraction - returns placeholder URLs
|
|
const frameUrls: string[] = [];
|
|
for (let i = startFrame; i <= endFrame; i++) {
|
|
frameUrls.push(`/manus-storage/frames/${outputPrefix}/frame_${String(i).padStart(6, "0")}.png`);
|
|
}
|
|
return frameUrls;
|
|
}
|
|
|
|
/**
|
|
* Simulate audio extraction from video
|
|
* In production, this would use FFmpeg to extract audio track
|
|
*/
|
|
export async function extractAudio(videoUrl: string, outputKey: string): Promise<string> {
|
|
// Returns placeholder URL for extracted audio
|
|
return `/manus-storage/audio/${outputKey}.wav`;
|
|
}
|
|
|
|
/**
|
|
* Detect scene cuts using frame difference analysis
|
|
* Uses LLM vision to analyze frame transitions when available
|
|
*/
|
|
export async function detectSceneCuts(
|
|
projectId: number,
|
|
totalFrames: number,
|
|
fps: number
|
|
): Promise<SceneCut[]> {
|
|
// Simulate scene detection based on typical animation patterns
|
|
// In production, this would analyze actual frame histograms and pixel differences
|
|
const cuts: SceneCut[] = [];
|
|
const avgSceneLength = fps * 3; // Average 3 seconds per scene in animation
|
|
|
|
let currentFrame = 0;
|
|
while (currentFrame < totalFrames) {
|
|
// Add some randomness to simulate real scene detection
|
|
const sceneLength = Math.floor(avgSceneLength * (0.5 + Math.random() * 1.5));
|
|
currentFrame += sceneLength;
|
|
|
|
if (currentFrame < totalFrames) {
|
|
cuts.push({
|
|
frameIndex: currentFrame,
|
|
confidence: 0.85 + Math.random() * 0.15,
|
|
type: Math.random() > 0.8 ? "dissolve" : "hard_cut",
|
|
});
|
|
}
|
|
}
|
|
|
|
return cuts;
|
|
}
|
|
|
|
/**
|
|
* Analyze a frame using LLM vision to determine:
|
|
* - Whether the background is static
|
|
* - What characters/objects are present
|
|
* - Quality score for background reference selection
|
|
*/
|
|
export async function analyzeFrame(frameUrl: string, context: string): Promise<{
|
|
isStaticBackground: boolean;
|
|
characters: string[];
|
|
objects: string[];
|
|
qualityScore: number;
|
|
description: string;
|
|
}> {
|
|
try {
|
|
const response = await invokeLLM({
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: `Tu es un analyste d'animation professionnelle. Analyse cette frame de dessin animé et fournis:
|
|
1. Si l'arrière-plan semble statique (typique des dessins animés des années 80)
|
|
2. Les personnages visibles
|
|
3. Les objets en mouvement
|
|
4. Un score de qualité (0-100) pour utiliser cette frame comme référence de fond
|
|
Contexte: ${context}
|
|
Réponds en JSON.`,
|
|
},
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{
|
|
type: "text" as const,
|
|
text: "Analyse cette frame d'animation.",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
response_format: {
|
|
type: "json_schema",
|
|
json_schema: {
|
|
name: "frame_analysis",
|
|
strict: true,
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
isStaticBackground: { type: "boolean" },
|
|
characters: { type: "array", items: { type: "string" } },
|
|
objects: { type: "array", items: { type: "string" } },
|
|
qualityScore: { type: "number" },
|
|
description: { type: "string" },
|
|
},
|
|
required: ["isStaticBackground", "characters", "objects", "qualityScore", "description"],
|
|
additionalProperties: false,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const content = response.choices?.[0]?.message?.content;
|
|
if (content && typeof content === "string") {
|
|
return JSON.parse(content);
|
|
}
|
|
} catch (error) {
|
|
console.error("[VideoProcessor] Frame analysis failed:", error);
|
|
}
|
|
|
|
// Fallback response
|
|
return {
|
|
isStaticBackground: true,
|
|
characters: ["Personnage principal"],
|
|
objects: [],
|
|
qualityScore: 75,
|
|
description: "Frame d'animation avec fond statique et personnage en mouvement",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Select the best reference frame for background extraction
|
|
* Criteria: least character occlusion, highest quality, most representative
|
|
*/
|
|
export function selectBestReferenceFrame(
|
|
frameAnalyses: Array<{ frameIndex: number; qualityScore: number; isStaticBackground: boolean }>
|
|
): number {
|
|
const staticFrames = frameAnalyses.filter((f) => f.isStaticBackground);
|
|
if (staticFrames.length === 0) return frameAnalyses[0]?.frameIndex || 0;
|
|
|
|
// Sort by quality score descending
|
|
staticFrames.sort((a, b) => b.qualityScore - a.qualityScore);
|
|
return staticFrames[0].frameIndex;
|
|
}
|