mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-22 06:28:10 +02:00
feat(reels): import d'un fichier SRT lu par la voix à son minutage
- Nouveau Reel (bureau et mobile) : bouton « Importer un fichier SRT ». Tant qu'un SRT est chargé, le texte libre et l'assistant IA sont désactivés ; le choix de la voix (moteur, voix, ton) reste disponible. - Service Python : chaque sous-titre est lu séparément, ses silences retirés, puis posé à son instant de début. Une lecture trop longue est accélérée (atempo, sans changer la hauteur) pour tenir dans la durée du sous-titre, avec un avertissement au-delà de ×1,35. Après un repli sur Edge, tous les sous-titres gardent la même voix. - Sans voix, les mots de chaque sous-titre s'affichent sur son intervalle. - Aperçu : sous-titres calés sur le minutage du fichier. - Tests : lecture SRT (TS), fenêtres, accélération et mixage (Python). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WVqEw4Xycgf8BtwmSftE2M
This commit is contained in:
15 files changed
+635
-44
No files matched your search
@@ -19,6 +19,8 @@ import { useToast } from "@/hooks/use-toast";
|
||||
import { VoicePicker, isVoicePreviewCurrent, type VoicePreviewResult, type VoiceSettings } from "@/components/reels/voice-picker";
|
||||
import { CaptionStylePicker } from "@/components/reels/caption-style-picker";
|
||||
import { ReelPreview } from "@/components/reels/reel-preview";
|
||||
import { SrtUpload, type SrtFile } from "@/components/reels/srt-upload";
|
||||
import { srtText } from "@shared/srt";
|
||||
import { DEFAULT_CAPTION_STYLE, type CaptionStyle } from "@shared/captions";
|
||||
import { DEFAULT_TTS_STYLE, DEFAULT_VOICE } from "@shared/voices";
|
||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
||||
@@ -56,6 +58,8 @@ export default function MobileNewReel() {
|
||||
const [selectedVideo, setSelectedVideo] = useState<Media | null>(null);
|
||||
const [selectedTrack, setSelectedTrack] = useState<MusicTrack | null>(null);
|
||||
const [overlayText, setOverlayText] = useState('');
|
||||
// Fichier SRT : remplace le texte libre, la voix suit son minutage
|
||||
const [srtFile, setSrtFile] = useState<SrtFile | null>(null);
|
||||
const [productInfo, setProductInfo] = useState('');
|
||||
const [generatedVariants, setGeneratedVariants] = useState<any[]>([]);
|
||||
const [selectedPages, setSelectedPages] = useState<string[]>([]);
|
||||
@@ -92,7 +96,7 @@ export default function MobileNewReel() {
|
||||
|
||||
// Auto-calculate TTS sync
|
||||
useEffect(() => {
|
||||
if (!ttsEnabled || !overlayText.trim()) {
|
||||
if (!ttsEnabled || !overlayText.trim() || srtFile) {
|
||||
setSyncInfo(null);
|
||||
return;
|
||||
}
|
||||
@@ -107,7 +111,7 @@ export default function MobileNewReel() {
|
||||
.catch(() => setSyncInfo(null));
|
||||
}, 800);
|
||||
return () => clearTimeout(timer);
|
||||
}, [overlayText, ttsEnabled, ttsEngine, ttsVoice]);
|
||||
}, [overlayText, ttsEnabled, ttsEngine, ttsVoice, srtFile]);
|
||||
|
||||
|
||||
const { data: pages = [] } = useQuery<SocialPage[]>({
|
||||
@@ -262,8 +266,9 @@ export default function MobileNewReel() {
|
||||
createReelMutation.mutate({
|
||||
videoMediaId: selectedVideoId,
|
||||
musicTrackId: selectedTrack?.id,
|
||||
overlayText: overlayText,
|
||||
description: overlayText,
|
||||
overlayText: srtFile ? undefined : overlayText,
|
||||
srtCues: srtFile?.cues,
|
||||
description: srtFile ? srtText(srtFile.cues) : overlayText,
|
||||
pageIds: selectedPages,
|
||||
scheduledFor: scheduledDate?.toISOString(),
|
||||
musicVolume: musicVolume[0] / 100,
|
||||
@@ -429,6 +434,7 @@ export default function MobileNewReel() {
|
||||
{/* TEXT STEP */}
|
||||
{currentStep === 'text' && (
|
||||
<div className="space-y-6">
|
||||
{!srtFile && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base flex items-center"><Sparkles className="w-4 h-4 mr-2 text-primary" /> Assistant IA</CardTitle>
|
||||
@@ -446,8 +452,9 @@ export default function MobileNewReel() {
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{generatedVariants.length > 0 && (
|
||||
{!srtFile && generatedVariants.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{generatedVariants.map((v, i) => (
|
||||
<div key={i} className="bg-card p-3 rounded-lg border text-sm" onClick={() => { setOverlayText(v.text); toast({ title: "Texte appliqué" }); }}>
|
||||
@@ -459,13 +466,20 @@ export default function MobileNewReel() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Texte Overlay</label>
|
||||
<Textarea
|
||||
value={overlayText}
|
||||
onChange={(e) => setOverlayText(e.target.value)}
|
||||
placeholder="Texte sur la vidéo..."
|
||||
className="text-lg"
|
||||
rows={3}
|
||||
/>
|
||||
<SrtUpload value={srtFile} onChange={setSrtFile} />
|
||||
{srtFile ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Texte libre désactivé : le fichier SRT est utilisé. Retirez-le pour écrire un texte.
|
||||
</p>
|
||||
) : (
|
||||
<Textarea
|
||||
value={overlayText}
|
||||
onChange={(e) => setOverlayText(e.target.value)}
|
||||
placeholder="Texte sur la vidéo..."
|
||||
className="text-lg"
|
||||
rows={3}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-1.5 mt-3">
|
||||
<Label className="text-xs font-medium">Style des sous-titres</Label>
|
||||
<CaptionStylePicker value={captionStyle} onChange={setCaptionStyle} compact />
|
||||
@@ -503,13 +517,13 @@ export default function MobileNewReel() {
|
||||
<VoicePicker
|
||||
value={voiceSettings}
|
||||
onChange={setVoiceSettings}
|
||||
sampleText={overlayText}
|
||||
sampleText={srtFile ? srtFile.cues[0]?.text : overlayText}
|
||||
onPreview={setVoicePreview}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
|
||||
{syncInfo && (
|
||||
{syncInfo && !srtFile && (
|
||||
<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>
|
||||
@@ -549,7 +563,8 @@ export default function MobileNewReel() {
|
||||
<ReelPreview
|
||||
kind="video"
|
||||
videoUrl={selectedVideo.originalUrl}
|
||||
text={overlayText}
|
||||
text={srtFile ? srtText(srtFile.cues) : overlayText}
|
||||
srtCues={srtFile?.cues}
|
||||
showCaptions
|
||||
captionStyle={captionStyle}
|
||||
ttsEnabled={ttsEnabled}
|
||||
|
||||
@@ -28,6 +28,8 @@ import { useToast } from "@/hooks/use-toast";
|
||||
import { VoicePicker, isVoicePreviewCurrent, type VoicePreviewResult, type VoiceSettings } from "@/components/reels/voice-picker";
|
||||
import { CaptionStylePicker } from "@/components/reels/caption-style-picker";
|
||||
import { ReelPreview } from "@/components/reels/reel-preview";
|
||||
import { SrtUpload, type SrtFile } from "@/components/reels/srt-upload";
|
||||
import { srtText } from "@shared/srt";
|
||||
import { DEFAULT_CAPTION_STYLE, type CaptionStyle } from "@shared/captions";
|
||||
import { DEFAULT_TTS_STYLE, DEFAULT_VOICE } from "@shared/voices";
|
||||
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
|
||||
@@ -65,6 +67,8 @@ export default function NewReel() {
|
||||
const [selectedVideo, setSelectedVideo] = useState<Media | null>(null);
|
||||
const [selectedTrack, setSelectedTrack] = useState<MusicTrack | null>(null);
|
||||
const [overlayText, setOverlayText] = useState('');
|
||||
// Fichier SRT : remplace le texte libre, la voix suit son minutage
|
||||
const [srtFile, setSrtFile] = useState<SrtFile | null>(null);
|
||||
const [productInfo, setProductInfo] = useState('');
|
||||
const [generatedVariants, setGeneratedVariants] = useState<any[]>([]);
|
||||
const [selectedPages, setSelectedPages] = useState<string[]>([]);
|
||||
@@ -108,7 +112,7 @@ export default function NewReel() {
|
||||
|
||||
// Auto-calculate TTS sync when text changes
|
||||
useEffect(() => {
|
||||
if (!ttsEnabled || !overlayText.trim()) {
|
||||
if (!ttsEnabled || !overlayText.trim() || srtFile) {
|
||||
setSyncInfo(null);
|
||||
return;
|
||||
}
|
||||
@@ -123,7 +127,7 @@ export default function NewReel() {
|
||||
.catch(() => setSyncInfo(null));
|
||||
}, 800);
|
||||
return () => clearTimeout(timer);
|
||||
}, [overlayText, ttsEnabled, ttsEngine, ttsVoice]);
|
||||
}, [overlayText, ttsEnabled, ttsEngine, ttsVoice, srtFile]);
|
||||
|
||||
|
||||
// État audio preview
|
||||
@@ -405,8 +409,9 @@ export default function NewReel() {
|
||||
createReelMutation.mutate({
|
||||
videoMediaId: selectedVideoId,
|
||||
musicTrackId: selectedTrack?.id,
|
||||
overlayText: overlayText,
|
||||
description: overlayText,
|
||||
overlayText: srtFile ? undefined : overlayText,
|
||||
srtCues: srtFile?.cues,
|
||||
description: srtFile ? srtText(srtFile.cues) : overlayText,
|
||||
pageIds: selectedPages,
|
||||
scheduledFor: scheduledDate?.toISOString(),
|
||||
musicVolume: musicVolume[0] / 100,
|
||||
@@ -702,6 +707,7 @@ export default function NewReel() {
|
||||
{/* ÉTAPE 3: Texte */}
|
||||
{currentStep === 'text' && (
|
||||
<>
|
||||
{!srtFile && (
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
@@ -729,8 +735,9 @@ export default function NewReel() {
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{generatedVariants.length > 0 && (
|
||||
{!srtFile && generatedVariants.length > 0 && (
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle>Variations générées</CardTitle>
|
||||
@@ -770,16 +777,26 @@ export default function NewReel() {
|
||||
Texte Overlay
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Ce texte s'affichera au centre de votre Reel (style TikTok)
|
||||
Ce texte s'affichera au centre de votre Reel (style TikTok).
|
||||
Vous pouvez aussi importer un fichier SRT : il remplace le texte.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
value={overlayText}
|
||||
onChange={(e) => setOverlayText(e.target.value)}
|
||||
placeholder="Écrivez le texte qui apparaîtra sur votre Reel..."
|
||||
rows={4}
|
||||
/>
|
||||
<div className="mb-4">
|
||||
<SrtUpload value={srtFile} onChange={setSrtFile} />
|
||||
</div>
|
||||
{srtFile ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Texte libre désactivé : le fichier SRT est utilisé. Retirez-le pour écrire un texte.
|
||||
</p>
|
||||
) : (
|
||||
<Textarea
|
||||
value={overlayText}
|
||||
onChange={(e) => setOverlayText(e.target.value)}
|
||||
placeholder="Écrivez le texte qui apparaîtra sur votre Reel..."
|
||||
rows={4}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2 mt-4">
|
||||
<Switch
|
||||
@@ -834,12 +851,12 @@ export default function NewReel() {
|
||||
<VoicePicker
|
||||
value={voiceSettings}
|
||||
onChange={setVoiceSettings}
|
||||
sampleText={overlayText}
|
||||
sampleText={srtFile ? srtFile.cues[0]?.text : overlayText}
|
||||
onPreview={setVoicePreview}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{syncInfo && (
|
||||
{syncInfo && !srtFile && (
|
||||
<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>
|
||||
@@ -857,8 +874,10 @@ export default function NewReel() {
|
||||
)}
|
||||
|
||||
<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.
|
||||
{srtFile
|
||||
? "Chaque sous-titre sera lu à son instant et accéléré si besoin pour respecter la durée du fichier SRT."
|
||||
: "Le texte sera automatiquement synchronisé avec la voix."}
|
||||
{' '}Les #hashtags et émojis ne seront pas lus.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -984,7 +1003,8 @@ export default function NewReel() {
|
||||
<ReelPreview
|
||||
kind="video"
|
||||
videoUrl={selectedVideo.originalUrl}
|
||||
text={overlayText}
|
||||
text={srtFile ? srtText(srtFile.cues) : overlayText}
|
||||
srtCues={srtFile?.cues}
|
||||
showCaptions={drawText}
|
||||
captionStyle={captionStyle}
|
||||
ttsEnabled={ttsEnabled}
|
||||
@@ -1022,7 +1042,7 @@ export default function NewReel() {
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Texte</span>
|
||||
<span>{overlayText ? '✓' : '—'}</span>
|
||||
<span>{srtFile ? `SRT (${srtFile.cues.length})` : overlayText ? '✓' : '—'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Pages</span>
|
||||
|
||||
Reference in new issue
Block a user