mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
Service ffmpeg-api réécrit (ffmpeg-service/app/) : - Appels FFmpeg asynchrones : le service ne se fige plus pendant un rendu - Vidéo récupérée par téléchargement (GET /files/…) au lieu de base64 en JSON - Vraies erreurs HTTP ; échec explicite si la voix demandée est impossible - 30 voix Gemini + ton de lecture (dynamique, chaleureux, promo, calme), clé en en-tête, modèle configurable avec repli - Edge TTS 7 (minutage des mots restauré), secours en voix françaises - Voix traitée : filtre, compression, niveau constant ; musique bouclée et baissée automatiquement sous la voix ; mix final à -14 LUFS - Sous-titres calés mot à mot (Whisper pour Gemini et la voix d'origine, à la place de ffsubsync), style Montserrat, placés hors des boutons Reels - Vidéo : plus de retouche luminosité forcée, scaling lanczos, HDR iPhone converti, BT.709, AAC 48 kHz 192k ; la vidéo s'allonge si la voix dépasse - Grand logo de fin affiché après la voix ; FFmpeg 7.0.2 épinglé, polices et modèle Whisper intégrés à l'image ; tests pytest et ruff Application : - Sélecteur de voix partagé (4 pages) : moteur, 30 voix, ton, écoute - Reel images : minutage réel des mots, interrupteur voix respecté - sync-info ne génère plus de voix à chaque frappe (estimation locale) - Stabilisation désactivée par défaut, route /reels/preview inutilisée retirée - Log « [ReelQueue] Worker démarré » pour vérifier la version déployée Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ze4bs7tpF1KGWUk6ZZSZ4
50 lines
1.6 KiB
TypeScript
50 lines
1.6 KiB
TypeScript
/**
|
|
* Estimation de la durée de lecture d'un texte, affichée pendant la saisie.
|
|
*
|
|
* Auparavant, chaque pause de frappe générait une voix complète pour la
|
|
* mesurer (un appel Gemini facturé à chaque fois). Le minutage réel des mots
|
|
* vient désormais de la voix au moment du rendu ; ici, une estimation suffit.
|
|
*/
|
|
|
|
/** Débit moyen d'une voix de synthèse française, en mots par seconde. */
|
|
const WORDS_PER_SECOND = 2.6;
|
|
/** Au-delà, la voix dépasse la durée confortable d'un Reel. */
|
|
const MAX_COMFORTABLE_SECONDS = 45;
|
|
|
|
export interface SyncTiming {
|
|
wordDuration: number;
|
|
audioDuration: number;
|
|
wordCount: number;
|
|
punctuationPause: number;
|
|
isHealthy: boolean;
|
|
warnings: string[];
|
|
}
|
|
|
|
export function estimateVoiceTiming(text: string): SyncTiming {
|
|
const words = text
|
|
.replace(/#[\wÀ-ÿ]+/g, '')
|
|
.replace(/https?:\/\/\S+/g, '')
|
|
.split(/\s+/)
|
|
.filter((w) => /[A-Za-z0-9À-ÿ]/.test(w));
|
|
const pauses = (text.match(/[.!?;:]/g) ?? []).length;
|
|
const punctuationPause = 0.35;
|
|
const audioDuration = words.length / WORDS_PER_SECOND + pauses * punctuationPause;
|
|
|
|
const warnings: string[] = [];
|
|
if (audioDuration > MAX_COMFORTABLE_SECONDS) {
|
|
warnings.push(`Texte long : environ ${Math.round(audioDuration)} s de voix. Visez moins de ${MAX_COMFORTABLE_SECONDS} s.`);
|
|
}
|
|
if (words.length > 0 && words.length < 4) {
|
|
warnings.push('Texte très court : la voix ne durera que quelques secondes.');
|
|
}
|
|
|
|
return {
|
|
wordDuration: words.length ? audioDuration / words.length : 0,
|
|
audioDuration,
|
|
wordCount: words.length,
|
|
punctuationPause,
|
|
isHealthy: warnings.length === 0,
|
|
warnings,
|
|
};
|
|
}
|