Files
Socialflow/server/services/ffmpeg.ts
T

314 lines
10 KiB
TypeScript

/**
* FFmpeg Docker API Service
*
* Intégration avec l'API FFmpeg Docker locale pour le traitement vidéo des Reels.
* L'API attend une vidéo en base64 et retourne la vidéo traitée en base64.
*/
interface FFmpegReelRequest {
video_base64?: string; // Vidéo source en base64
video_url?: string; // OU URL de la vidéo source
text?: string; // Texte overlay style TikTok
music_id?: string; // ID de la musique (catalogue FFmpeg)
music_url?: string; // OU URL directe de la musique
tts_enabled?: boolean; // Activation du TTS
tts_voice?: string; // Voix TTS (ex: fr-FR-VivienneNeural)
word_duration?: number; // Durée par mot (default: 0.6s)
font_size?: number; // Taille police (default: 24)
music_volume?: number; // Volume musique (default: 0.25)
draw_text?: boolean; // Dessiner le texte sur la vidéo (default: true)
stabilize?: boolean; // Stabilisation vidéo via vidstab (default: false)
}
interface FFmpegReelResponse {
success: boolean;
output_base64?: string; // Vidéo traitée en base64
duration?: number; // Durée de la vidéo en secondes
detail?: string; // Message d'erreur si échec
}
interface FFmpegConfig {
apiUrl: string;
apiKey: string;
}
export class FFmpegService {
private config: FFmpegConfig | null = null;
/**
* Configure le service avec l'URL et la clé API
*/
configure(apiUrl: string, apiKey: string): void {
this.config = { apiUrl, apiKey };
console.log('🎬 FFmpeg Service configured:', apiUrl);
}
/**
* Vérifie que le service est configuré
*/
private ensureConfigured(): FFmpegConfig {
if (!this.config) {
throw new Error('FFmpeg Service not configured. Call configure() first.');
}
return this.config;
}
/**
* Traite une vidéo pour créer un Reel avec musique et texte overlay
*
* @param videoBase64 - Vidéo source encodée en base64
* @param options - Options de traitement (texte, musique, etc.)
* @returns Vidéo traitée en base64
*/
async processReelVideo(
videoBase64: string,
options: {
text?: string;
musicId?: string;
musicUrl?: string;
ttsEnabled?: boolean;
ttsVoice?: string;
wordDuration?: number;
fontSize?: number;
musicVolume?: number;
drawText?: boolean;
stabilize?: boolean;
} = {}
): Promise<{ success: boolean; videoBase64?: string; duration?: number; error?: string }> {
const config = this.ensureConfigured();
const requestBody: FFmpegReelRequest = {
video_base64: videoBase64,
text: options.text,
music_id: options.musicId,
music_url: options.musicUrl,
tts_enabled: options.ttsEnabled,
tts_voice: options.ttsVoice,
word_duration: options.wordDuration ?? 0.6,
font_size: options.fontSize ?? 64,
music_volume: options.musicVolume ?? 0.25,
draw_text: options.drawText ?? true,
stabilize: options.stabilize ?? false,
};
// Remove undefined values
Object.keys(requestBody).forEach(key => {
if (requestBody[key as keyof FFmpegReelRequest] === undefined) {
delete requestBody[key as keyof FFmpegReelRequest];
}
});
console.log('🎬 Processing Reel video:', {
hasVideo: !!videoBase64,
hasText: !!options.text,
hasMusicId: !!options.musicId,
hasMusicUrl: !!options.musicUrl,
hasTTS: options.ttsEnabled,
drawText: options.drawText,
});
try {
const response = await fetch(`${config.apiUrl}/process-reel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': config.apiKey,
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
console.error('❌ FFmpeg API error:', response.status, errorText);
return {
success: false,
error: `FFmpeg API error: ${response.status} - ${errorText}`,
};
}
const data = await response.json() as FFmpegReelResponse;
if (!data.success) {
console.error('❌ FFmpeg processing failed:', data.detail);
return {
success: false,
error: data.detail || 'Unknown FFmpeg processing error',
};
}
console.log('✅ Reel video processed successfully, duration:', data.duration);
return {
success: true,
videoBase64: data.output_base64,
duration: data.duration,
};
} catch (error) {
console.error('❌ FFmpeg Service error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Traite une vidéo depuis une URL (télécharge, traite, retourne base64)
*/
async processReelFromUrl(
videoUrl: string,
options: {
text?: string;
musicId?: string;
musicUrl?: string;
ttsEnabled?: boolean;
ttsVoice?: string;
wordDuration?: number;
fontSize?: number;
musicVolume?: number;
drawText?: boolean;
stabilize?: boolean;
} = {}
): Promise<{ success: boolean; videoBase64?: string; duration?: number; error?: string }> {
const config = this.ensureConfigured();
const requestBody: FFmpegReelRequest = {
video_url: videoUrl,
text: options.text,
music_id: options.musicId,
music_url: options.musicUrl,
tts_enabled: options.ttsEnabled,
tts_voice: options.ttsVoice,
word_duration: options.wordDuration ?? 0.6,
font_size: options.fontSize ?? 64,
music_volume: options.musicVolume ?? 0.25,
draw_text: options.drawText ?? true,
stabilize: options.stabilize ?? false,
};
// Remove undefined values
Object.keys(requestBody).forEach(key => {
if (requestBody[key as keyof FFmpegReelRequest] === undefined) {
delete requestBody[key as keyof FFmpegReelRequest];
}
});
console.log('🎬 Processing Reel from URL:', {
videoUrl,
hasText: !!options.text,
textLength: options.text?.length || 0,
hasMusicId: !!options.musicId,
hasMusicUrl: !!options.musicUrl,
ttsEnabled: options.ttsEnabled,
ttsVoice: options.ttsVoice,
drawText: options.drawText,
});
console.log('📤 Sending to FFmpeg API:', JSON.stringify(requestBody, null, 2).substring(0, 500) + '...');
try {
const response = await fetch(`${config.apiUrl}/process-reel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': config.apiKey,
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
console.error('❌ FFmpeg API error:', response.status, errorText);
return {
success: false,
error: `FFmpeg API error: ${response.status} - ${errorText}`,
};
}
const data = await response.json() as FFmpegReelResponse;
if (!data.success) {
console.error('❌ FFmpeg processing failed:', data.detail);
return {
success: false,
error: data.detail || 'Unknown FFmpeg processing error',
};
}
console.log('✅ Reel video processed successfully from URL');
return {
success: true,
videoBase64: data.output_base64,
duration: data.duration,
};
} catch (error) {
console.error('❌ FFmpeg Service error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Vérifie la santé de l'API FFmpeg
*/
async healthCheck(): Promise<boolean> {
try {
const config = this.ensureConfigured();
const response = await fetch(`${config.apiUrl}/health`, {
method: 'GET',
headers: {
'X-API-Key': config.apiKey,
},
});
return response.ok;
} catch {
return false;
}
}
async previewTTS(
text: string,
voice: string
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
const config = this.ensureConfigured();
try {
const response = await fetch(`${config.apiUrl}/preview-tts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': config.apiKey,
},
body: JSON.stringify({
text,
tts_voice: voice
}),
});
if (!response.ok) {
const errorText = await response.text();
return { success: false, error: `FFmpeg API error: ${response.status} - ${errorText}` };
}
const data = await response.json();
if (!data.success) {
return { success: false, error: data.detail };
}
return { success: true, audioBase64: data.audio_base64 };
} catch (error) {
console.error('❌ TTS Preview error:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
}
export const ffmpegService = new FFmpegService();