mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-15 03:08:44 +02:00
L'ancienne file comptait les posts « processing » : un redémarrage pendant un rendu la bloquait définitivement. Les rendus passent désormais par une table reel_jobs, réservée avec FOR UPDATE SKIP LOCKED, avec heartbeat et reprise des jobs interrompus au démarrage (2 tentatives maximum). - Pipeline vidéo et pipeline images (Remotion) extraits des routes vers server/services/reels/, les routes ne font que valider (zod) et mettre en file - Rendus d'images suivis en base au lieu d'une Map en mémoire - storeName conservé pour les jobs mis en attente - Publication Facebook en binaire : l'URL relative /uploads/... transmise auparavant n'était pas téléchargeable par Facebook - Job en échec si toutes les pages échouent ou si la voix demandée manque (le service Python renseigne enfin tts_error) - La voix choisie sur la page images est réellement utilisée - sync-info mesure la voix Gemini avec sa clé - Timeouts sur les appels au service FFmpeg, vignettes via execFile, /debug-ffmpeg protégé par la clé API, plus de voix anglaise en secours Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
77 lines
2.3 KiB
TypeScript
77 lines
2.3 KiB
TypeScript
import { ffmpegService } from './ffmpeg';
|
||
import * as musicMetadata from 'music-metadata';
|
||
|
||
export interface SyncTiming {
|
||
wordDuration: number;
|
||
audioDuration: number;
|
||
wordCount: number;
|
||
punctuationPause: number;
|
||
isHealthy: boolean;
|
||
warnings: string[];
|
||
}
|
||
|
||
export class TtsSyncService {
|
||
/**
|
||
* Calcule le word_duration optimal pour synchroniser l'affichage du texte
|
||
* avec la durée réelle de la voix TTS générée.
|
||
*/
|
||
async calculateSyncTiming(
|
||
text: string,
|
||
voice: string,
|
||
ttsEngine?: string,
|
||
geminiApiKey?: string
|
||
): Promise<SyncTiming> {
|
||
const cleanText = this.cleanText(text);
|
||
|
||
// 1. Générer le TTS preview (avec la même voix que le rendu) et mesurer sa durée exacte
|
||
const ttsResult = await ffmpegService.previewTTS(cleanText, voice, ttsEngine, geminiApiKey);
|
||
if (!ttsResult.success || !ttsResult.audioBase64) {
|
||
throw new Error('TTS preview failed: ' + (ttsResult.error || 'unknown'));
|
||
}
|
||
|
||
const audioBuffer = Buffer.from(ttsResult.audioBase64, 'base64');
|
||
const metadata = await musicMetadata.parseBuffer(audioBuffer, 'audio/mpeg');
|
||
const audioDuration = metadata.format.duration || 0;
|
||
|
||
// 2. Analyser le texte (compte les mots réellement lus par la voix)
|
||
const wordCount = this.calculateWordCount(cleanText);
|
||
|
||
// 3. Calculer le word_duration
|
||
const wordDuration = wordCount > 0 ? audioDuration / wordCount : 0.6;
|
||
|
||
// 4. Validation
|
||
const warnings: string[] = [];
|
||
const isHealthy = wordDuration >= 0.25 && wordDuration <= 1.5;
|
||
if (wordDuration < 0.25) {
|
||
warnings.push('Texte trop long : les mots défileront très vite. Envisagez de raccourcir.');
|
||
}
|
||
if (wordDuration > 1.2) {
|
||
warnings.push('Texte très court : affichage lent.');
|
||
}
|
||
|
||
return {
|
||
wordDuration,
|
||
audioDuration,
|
||
wordCount,
|
||
punctuationPause: 0,
|
||
isHealthy,
|
||
warnings,
|
||
};
|
||
}
|
||
|
||
private cleanText(text: string): string {
|
||
return text
|
||
.replace(/#\w+/g, '')
|
||
.replace(/https?:\/\/\S+/g, '')
|
||
.replace(/[\uD83C-\uD83E][\uDC00-\uDFFF]|[☀-⛿✀-➿]/g, '')
|
||
.trim();
|
||
}
|
||
|
||
private calculateWordCount(text: string): number {
|
||
const tokens = text.split(/\s+/).filter(w => w.length > 0);
|
||
return tokens.filter(w => /[a-zA-Z0-9À-ſ]/.test(w)).length;
|
||
}
|
||
}
|
||
|
||
export const ttsSyncService = new TtsSyncService();
|