mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
329 lines
11 KiB
TypeScript
329 lines
11 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)
|
|
watermark_url?: string; // URL du logo
|
|
store_name?: string; // Nom du magasin pour l'outro
|
|
enable_ending_effect?: boolean; // Activer l'effet de fin (logo+fondu)
|
|
}
|
|
|
|
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;
|
|
watermarkUrl?: string;
|
|
storeName?: string;
|
|
enableEndingEffect?: 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,
|
|
watermark_url: options.watermarkUrl,
|
|
store_name: options.storeName,
|
|
enable_ending_effect: options.enableEndingEffect ?? true,
|
|
};
|
|
|
|
// 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;
|
|
watermarkUrl?: string;
|
|
storeName?: string;
|
|
enableEndingEffect?: 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,
|
|
watermark_url: options.watermarkUrl,
|
|
store_name: options.storeName,
|
|
enable_ending_effect: options.enableEndingEffect ?? true,
|
|
};
|
|
|
|
// 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();
|