mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-18 20:56:54 +02:00
fix: precise TTS word-boundary timing and remove wasted sync calculations
- ffmpeg-service/main.py: replace ffsubsync path with exact word-boundary timing from edge_tts for TTS subtitles. Karaoke styling preserved. - server/services/ttsSync.ts: fix word count to match TTS-cleaned text, remove artificial punctuationPause subtraction, strip punctuation tokens from count. - server/routes/reels.ts: remove redundant ttsSyncService calls in preview/background; word_duration is ignored by Python, these only wasted TTS generations. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ce735dc23
commit
37aba33425
3 files changed
+43
-68
No files matched your search
+2
-22
@@ -350,17 +350,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
console.error('Error fetching watermark configuration', e);
|
||||
}
|
||||
|
||||
// Calculer le word_duration synchronisé si TTS activé
|
||||
let finalWordDuration = wordDuration;
|
||||
if (ttsEnabled && overlayText) {
|
||||
try {
|
||||
const sync = await ttsSyncService.calculateSyncTiming(overlayText, ttsVoice);
|
||||
finalWordDuration = sync.wordDuration;
|
||||
console.log(`🎯 TTS Sync (preview): ${sync.wordCount} mots, ${sync.audioDuration.toFixed(2)}s audio, word_duration=${finalWordDuration.toFixed(3)}s`);
|
||||
} catch (syncErr) {
|
||||
console.warn('⚠️ TTS sync failed (preview), using default word_duration:', syncErr);
|
||||
}
|
||||
}
|
||||
const finalWordDuration = wordDuration;
|
||||
|
||||
// Traiter la vidéo via FFmpeg
|
||||
const result = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||
@@ -593,17 +583,7 @@ async function processReelBackground(
|
||||
console.error('Error fetching watermark configuration', e);
|
||||
}
|
||||
|
||||
// Calculer le word_duration synchronisé si TTS activé
|
||||
let finalWordDuration = wordDuration ?? 0.6;
|
||||
if (ttsEnabled && overlayText && ttsVoice) {
|
||||
try {
|
||||
const sync = await ttsSyncService.calculateSyncTiming(overlayText, ttsVoice);
|
||||
finalWordDuration = sync.wordDuration;
|
||||
console.log(`🎯 TTS Sync (background): ${sync.wordCount} mots, ${sync.audioDuration.toFixed(2)}s audio, word_duration=${finalWordDuration.toFixed(3)}s`);
|
||||
} catch (syncErr) {
|
||||
console.warn('⚠️ TTS sync failed (background), using default word_duration:', syncErr);
|
||||
}
|
||||
}
|
||||
const finalWordDuration = wordDuration ?? 0.6;
|
||||
|
||||
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||
text: overlayText,
|
||||
|
||||
+11
-29
@@ -16,11 +16,10 @@ export class TtsSyncService {
|
||||
* avec la durée réelle de la voix TTS générée.
|
||||
*/
|
||||
async calculateSyncTiming(text: string, voice: string): Promise<SyncTiming> {
|
||||
const cleanTtsText = this.cleanForTts(text);
|
||||
const cleanDisplayText = this.cleanForDisplay(text);
|
||||
const cleanText = this.cleanText(text);
|
||||
|
||||
// 1. Générer le TTS preview et mesurer sa durée exacte
|
||||
const ttsResult = await ffmpegService.previewTTS(cleanTtsText, voice);
|
||||
const ttsResult = await ffmpegService.previewTTS(cleanText, voice);
|
||||
if (!ttsResult.success || !ttsResult.audioBase64) {
|
||||
throw new Error('TTS preview failed: ' + (ttsResult.error || 'unknown'));
|
||||
}
|
||||
@@ -29,14 +28,11 @@ export class TtsSyncService {
|
||||
const metadata = await musicMetadata.parseBuffer(audioBuffer, 'audio/mpeg');
|
||||
const audioDuration = metadata.format.duration || 0;
|
||||
|
||||
// 2. Analyser le texte
|
||||
const words = cleanDisplayText.split(/\s+/).filter(w => w.length > 0);
|
||||
const wordCount = words.length;
|
||||
const punctuationPause = this.calculatePunctuationPauses(cleanDisplayText);
|
||||
// 2. Analyser le texte (compte les mots réellement lus par la voix)
|
||||
const wordCount = this.calculateWordCount(cleanText);
|
||||
|
||||
// 3. Calculer le word_duration ajusté
|
||||
const effectiveDuration = Math.max(audioDuration - punctuationPause, 0.5);
|
||||
const wordDuration = wordCount > 0 ? effectiveDuration / wordCount : 0.6;
|
||||
// 3. Calculer le word_duration
|
||||
const wordDuration = wordCount > 0 ? audioDuration / wordCount : 0.6;
|
||||
|
||||
// 4. Validation
|
||||
const warnings: string[] = [];
|
||||
@@ -52,13 +48,13 @@ export class TtsSyncService {
|
||||
wordDuration,
|
||||
audioDuration,
|
||||
wordCount,
|
||||
punctuationPause,
|
||||
punctuationPause: 0,
|
||||
isHealthy,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
private cleanForTts(text: string): string {
|
||||
private cleanText(text: string): string {
|
||||
return text
|
||||
.replace(/#\w+/g, '')
|
||||
.replace(/https?:\/\/\S+/g, '')
|
||||
@@ -66,23 +62,9 @@ export class TtsSyncService {
|
||||
.trim();
|
||||
}
|
||||
|
||||
private cleanForDisplay(text: string): string {
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
private calculatePunctuationPauses(text: string): number {
|
||||
let pause = 0;
|
||||
const chars = text.split('');
|
||||
for (const c of chars) {
|
||||
if (',;'.includes(c)) pause += 0.3;
|
||||
else if ('.!?'.includes(c)) pause += 0.6;
|
||||
else if (':'.includes(c)) pause += 0.4;
|
||||
else if (c === '\n') pause += 0.3;
|
||||
}
|
||||
// "..." compte comme un seul point mais pause plus longue
|
||||
const ellipsisCount = (text.match(/\.\.\./g) || []).length;
|
||||
pause += ellipsisCount * 0.3; // bonus pour les points de suspension
|
||||
return pause;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user