feat: add adaptive TTS text-to-speech synchronization for Reels with real-time sync info widget

Calculate optimal word_duration based on actual TTS audio duration and text punctuation.

- server/services/ttsSync.ts: new service to measure TTS audio and compute sync timing

- server/routes/reels.ts: integrate sync into preview and background processing

- client: auto-calculate sync info widget in desktop and mobile Reel creation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 4.7 committed 2026-05-04 12:35:04 +02:00
1 parent 9913c307ed
commit 9ce735dc23
5 files changed
+1359 -3

No files matched your search

+45 -1
View File
@@ -1,4 +1,4 @@
import { useState, useCallback, useRef } from "react";
import { useState, useCallback, useRef, useEffect } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useLocation } from "wouter";
import {
@@ -65,6 +65,33 @@ export default function MobileNewReel() {
const [stabilize, setStabilize] = useState(true); // Activé par défaut pour les Reels
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
// TTS Sync state
const [syncInfo, setSyncInfo] = useState<{
wordDuration: number;
audioDuration: number;
wordCount: number;
isHealthy: boolean;
warnings: string[];
} | null>(null);
// Auto-calculate TTS sync
useEffect(() => {
if (!ttsEnabled || !overlayText.trim()) {
setSyncInfo(null);
return;
}
const timer = setTimeout(() => {
apiRequest('POST', '/api/reels/sync-info', {
text: overlayText,
voice: ttsVoice,
})
.then(r => r.json())
.then(data => setSyncInfo(data))
.catch(() => setSyncInfo(null));
}, 800);
return () => clearTimeout(timer);
}, [overlayText, ttsVoice, ttsEnabled]);
const { data: pages = [] } = useQuery<SocialPage[]>({
queryKey: ['/api/pages'],
@@ -475,6 +502,23 @@ export default function MobileNewReel() {
<Play className="w-3 h-3 mr-1" /> Tester la voix
</Button>
</div>
{syncInfo && (
<div className={`p-2 rounded-lg border mt-2 ${syncInfo.isHealthy ? 'bg-green-500/10 border-green-500/30' : 'bg-yellow-500/10 border-yellow-500/30'}`}>
<div className="flex items-center justify-between text-xs">
<span className="font-medium">Sync</span>
<span className={syncInfo.isHealthy ? 'text-green-600' : 'text-yellow-600'}>
{syncInfo.wordCount} mots · {syncInfo.audioDuration.toFixed(1)}s
</span>
</div>
{syncInfo.warnings.map((w, i) => (
<p key={i} className="text-[10px] text-yellow-600 mt-0.5">⚠️ {w}</p>
))}
{syncInfo.isHealthy && syncInfo.warnings.length === 0 && (
<p className="text-[10px] text-green-600 mt-0.5">✅ Timing OK</p>
)}
</div>
)}
</div>
)}
</div>
+44
View File
@@ -70,6 +70,15 @@ export default function NewReel() {
const [stabilize, setStabilize] = useState(true); // default to true
const [enableEndingEffect, setEnableEndingEffect] = useState(true);
// TTS Sync state
const [syncInfo, setSyncInfo] = useState<{
wordDuration: number;
audioDuration: number;
wordCount: number;
isHealthy: boolean;
warnings: string[];
} | null>(null);
// Enable TTS by default on mobile
useEffect(() => {
const isMobile = window.innerWidth < 768;
@@ -78,6 +87,24 @@ export default function NewReel() {
}
}, []);
// Auto-calculate TTS sync when text or voice changes
useEffect(() => {
if (!ttsEnabled || !overlayText.trim()) {
setSyncInfo(null);
return;
}
const timer = setTimeout(() => {
apiRequest('POST', '/api/reels/sync-info', {
text: overlayText,
voice: ttsVoice,
})
.then(r => r.json())
.then(data => setSyncInfo(data))
.catch(() => setSyncInfo(null));
}, 800);
return () => clearTimeout(timer);
}, [overlayText, ttsVoice, ttsEnabled]);
// État audio preview
const [isPlaying, setIsPlaying] = useState<string | null>(null);
@@ -796,6 +823,23 @@ export default function NewReel() {
</Button>
</div>
{syncInfo && (
<div className={`p-3 rounded-lg border mt-3 ${syncInfo.isHealthy ? 'bg-green-500/10 border-green-500/30' : 'bg-yellow-500/10 border-yellow-500/30'}`}>
<div className="flex items-center justify-between text-sm">
<span className="font-medium">Sync texte/voix</span>
<span className={syncInfo.isHealthy ? 'text-green-600' : 'text-yellow-600'}>
{syncInfo.wordCount} mots · {syncInfo.audioDuration.toFixed(1)}s · {syncInfo.wordDuration.toFixed(2)}s/mot
</span>
</div>
{syncInfo.warnings.map((w, i) => (
<p key={i} className="text-xs text-yellow-600 mt-1">⚠️ {w}</p>
))}
{syncInfo.isHealthy && syncInfo.warnings.length === 0 && (
<p className="text-xs text-green-600 mt-1">✅ Timing calculé automatiquement</p>
)}
</div>
)}
<p className="text-xs text-muted-foreground mt-2">
Le texte sera automatiquement synchronisé avec la voix.
Les #hashtags et émojis ne seront pas lus.