mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-24 07:28:11 +02:00
feat: enhance reels with default overlay, more voices, and tts preview
This commit is contained in:
1 parent
8295916915
commit
9ff1c4aa18
5 files changed
+189
-31
No files matched your search
@@ -66,8 +66,8 @@ export default function NewReel() {
|
|||||||
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
|
const [scheduledDate, setScheduledDate] = useState<Date | undefined>(undefined);
|
||||||
const [musicVolume, setMusicVolume] = useState([25]);
|
const [musicVolume, setMusicVolume] = useState([25]);
|
||||||
const [ttsEnabled, setTtsEnabled] = useState(true);
|
const [ttsEnabled, setTtsEnabled] = useState(true);
|
||||||
const [ttsVoice, setTtsVoice] = useState("female");
|
const [ttsVoice, setTtsVoice] = useState("fr-FR-VivienneMultilingualNeural");
|
||||||
const [drawText, setDrawText] = useState(false);
|
const [drawText, setDrawText] = useState(true);
|
||||||
|
|
||||||
// État audio preview
|
// État audio preview
|
||||||
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
const [isPlaying, setIsPlaying] = useState<string | null>(null);
|
||||||
@@ -742,28 +742,63 @@ export default function NewReel() {
|
|||||||
<Mic className="w-4 h-4" />
|
<Mic className="w-4 h-4" />
|
||||||
Voix du narrateur
|
Voix du narrateur
|
||||||
</Label>
|
</Label>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div
|
{[
|
||||||
onClick={() => setTtsVoice("female")}
|
{ id: "fr-FR-VivienneMultilingualNeural", label: "Vivienne", type: "Femme (Défaut)", style: "bg-pink-500/10 border-pink-500/50" },
|
||||||
className={`cursor-pointer p-3 rounded-md border-2 text-center transition-all ${ttsVoice === 'female'
|
{ id: "fr-FR-RemyMultilingualNeural", label: "Rémy", type: "Homme", style: "bg-blue-500/10 border-blue-500/50" },
|
||||||
? 'border-primary bg-primary/10'
|
{ id: "fr-FR-DeniseNeural", label: "Denise", type: "Femme (Calme)", style: "bg-purple-500/10 border-purple-500/50" },
|
||||||
: 'border-transparent bg-background hover:bg-accent'
|
{ id: "fr-FR-HenriNeural", label: "Henri", type: "Homme (Grave)", style: "bg-slate-500/10 border-slate-500/50" },
|
||||||
}`}
|
{ id: "fr-FR-EloiseNeural", label: "Éloïse", type: "Enfant", style: "bg-orange-500/10 border-orange-500/50" }
|
||||||
>
|
].map((voice) => (
|
||||||
<div className="font-semibold">Féminine</div>
|
<div
|
||||||
<div className="text-xs text-muted-foreground">Vivienne</div>
|
key={voice.id}
|
||||||
</div>
|
onClick={() => setTtsVoice(voice.id)}
|
||||||
<div
|
className={`cursor-pointer p-3 rounded-md border-2 transition-all hover:bg-accent ${ttsVoice === voice.id || (ttsVoice === 'female' && voice.id === 'fr-FR-VivienneMultilingualNeural')
|
||||||
onClick={() => setTtsVoice("male")}
|
? `border-primary ${voice.style}`
|
||||||
className={`cursor-pointer p-3 rounded-md border-2 text-center transition-all ${ttsVoice === 'male'
|
: 'border-transparent bg-muted/30'
|
||||||
? 'border-primary bg-primary/10'
|
}`}
|
||||||
: 'border-transparent bg-background hover:bg-accent'
|
>
|
||||||
}`}
|
<div className="font-semibold flex items-center justify-between">
|
||||||
>
|
{voice.label}
|
||||||
<div className="font-semibold">Masculine</div>
|
{(ttsVoice === voice.id || (ttsVoice === 'female' && voice.id === 'fr-FR-VivienneMultilingualNeural')) && <Check className="w-3 h-3 text-primary" />}
|
||||||
<div className="text-xs text-muted-foreground">Rémy</div>
|
</div>
|
||||||
</div>
|
<div className="text-xs text-muted-foreground">{voice.type}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const textToTest = overlayText || "Ceci est un test de voix pour votre vidéo.";
|
||||||
|
try {
|
||||||
|
const response = await apiRequest('POST', '/api/reels/tts-preview', {
|
||||||
|
text: textToTest,
|
||||||
|
voice: ttsVoice
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.audioBase64) {
|
||||||
|
const audio = new Audio(`data:audio/mp3;base64,${data.audioBase64}`);
|
||||||
|
audio.play();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Impossible de tester la voix",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play className="w-3 h-3 mr-2" />
|
||||||
|
Tester la voix
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
Le texte sera automatiquement synchronisé avec la voix.
|
Le texte sera automatiquement synchronisé avec la voix.
|
||||||
Les #hashtags et émojis ne seront pas lus.
|
Les #hashtags et émojis ne seront pas lus.
|
||||||
|
|||||||
+53
-8
@@ -54,23 +54,23 @@ async def generate_tts_with_subs(
|
|||||||
|
|
||||||
# Determine gender of requested voice to choose appropriate fallbacks
|
# Determine gender of requested voice to choose appropriate fallbacks
|
||||||
is_male = any(name in voice for name in ["Remy", "Henri", "Paul"])
|
is_male = any(name in voice for name in ["Remy", "Henri", "Paul"])
|
||||||
|
|
||||||
if is_male:
|
if is_male:
|
||||||
fallback_voices = [
|
fallback_voices = [
|
||||||
voice, # Try requested voice first
|
voice, # Try requested voice first
|
||||||
"fr-FR-HenriNeural", # Primary Male fallback
|
"fr-FR-HenriNeural", # Primary Male fallback
|
||||||
"fr-FR-PaulNeural", # Secondary Male fallback
|
"fr-FR-PaulNeural", # Secondary Male fallback
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
fallback_voices = [
|
fallback_voices = [
|
||||||
voice, # Try requested voice first
|
voice, # Try requested voice first
|
||||||
"fr-FR-VivienneNeural", # Primary Female fallback
|
"fr-FR-VivienneNeural", # Primary Female fallback
|
||||||
"fr-FR-DeniseNeural", # Secondary Female fallback
|
"fr-FR-DeniseNeural", # Secondary Female fallback
|
||||||
]
|
]
|
||||||
|
|
||||||
# Always add English fallback as last resort
|
# Always add English fallback as last resort
|
||||||
fallback_voices.append("en-US-JennyNeural")
|
fallback_voices.append("en-US-JennyNeural")
|
||||||
|
|
||||||
# Remove duplicates while preserving order
|
# Remove duplicates while preserving order
|
||||||
fallback_voices = list(dict.fromkeys(fallback_voices))
|
fallback_voices = list(dict.fromkeys(fallback_voices))
|
||||||
|
|
||||||
@@ -434,5 +434,50 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
return {"success": False, "detail": str(e)}
|
return {"success": False, "detail": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/preview-tts")
|
||||||
|
async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)):
|
||||||
|
if x_api_key != API_KEY:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||||
|
|
||||||
|
try:
|
||||||
|
job_id = str(uuid.uuid4())
|
||||||
|
job_dir = TEMP_DIR / job_id
|
||||||
|
job_dir.mkdir()
|
||||||
|
|
||||||
|
tts_audio_path = job_dir / "preview.mp3"
|
||||||
|
tts_vtt_path = job_dir / "preview.vtt" # Not used but needed for function sig
|
||||||
|
|
||||||
|
if not request.text:
|
||||||
|
raise HTTPException(status_code=400, detail="Text required for preview")
|
||||||
|
|
||||||
|
clean_text = clean_text_for_tts(request.text)
|
||||||
|
|
||||||
|
# Determine voice (reuse logic)
|
||||||
|
voice = request.tts_voice
|
||||||
|
if voice == "male":
|
||||||
|
voice = "fr-FR-RemyMultilingualNeural"
|
||||||
|
elif voice == "female":
|
||||||
|
voice = "fr-FR-VivienneMultilingualNeural"
|
||||||
|
elif not voice or "Neural" not in voice:
|
||||||
|
voice = "fr-FR-VivienneMultilingualNeural"
|
||||||
|
|
||||||
|
await generate_tts_with_subs(clean_text, voice, tts_audio_path, tts_vtt_path)
|
||||||
|
|
||||||
|
if not tts_audio_path.exists():
|
||||||
|
raise Exception("TTS generation failed (file missing)")
|
||||||
|
|
||||||
|
with open(tts_audio_path, "rb") as f:
|
||||||
|
audio_bytes = f.read()
|
||||||
|
audio_b64 = base64.b64encode(audio_bytes).decode("utf-8")
|
||||||
|
|
||||||
|
shutil.rmtree(job_dir)
|
||||||
|
return {"success": True, "audio_base64": audio_b64}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if "job_dir" in locals():
|
||||||
|
shutil.rmtree(job_dir, ignore_errors=True)
|
||||||
|
return {"success": False, "detail": str(e)}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
@@ -237,6 +237,31 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prévisualiser la voix TTS
|
||||||
|
* POST /api/reels/tts-preview
|
||||||
|
*/
|
||||||
|
reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { text, voice } = req.body;
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return res.status(400).json({ error: 'Texte requis' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ffmpegService.previewTTS(text, voice);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, audioBase64: result.audioBase64 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error generating TTS preview:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur lors de la génération de la voix' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Créer et publier un Reel
|
* Créer et publier un Reel
|
||||||
* POST /api/reels
|
* POST /api/reels
|
||||||
|
|||||||
@@ -263,6 +263,46 @@ export class FFmpegService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async previewTTS(
|
||||||
|
text: string,
|
||||||
|
voice: string
|
||||||
|
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
|
||||||
|
const config = this.ensureConfigured();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${config.apiUrl}/preview-tts`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': config.apiKey,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text,
|
||||||
|
tts_voice: voice
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
return { success: false, error: `FFmpeg API error: ${response.status} - ${errorText}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
return { success: false, error: data.detail };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, audioBase64: data.audio_base64 };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ TTS Preview error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ffmpegService = new FFmpegService();
|
export const ffmpegService = new FFmpegService();
|
||||||
@@ -60,6 +60,19 @@ Ajout d'une fonctionnalité complète de création de Reels Facebook permettant
|
|||||||
- [x] Integrate CameraRecorder into `new-reel.tsx`
|
- [x] Integrate CameraRecorder into `new-reel.tsx`
|
||||||
- [x] Add quality settings panel in camera UI
|
- [x] Add quality settings panel in camera UI
|
||||||
|
|
||||||
|
### Phase 5: Reels Enhancements ⚡
|
||||||
|
|
||||||
|
- [x] **Enable Text Overlay by Default** in `new-reel.tsx`
|
||||||
|
- [x] **Expand French Voice Options**
|
||||||
|
- [x] Research available French voices (Edge-TTS)
|
||||||
|
- [x] Update frontend selection list
|
||||||
|
- [x] Update backend validation if needed
|
||||||
|
- [x] **Add Voice Test Button** (Preview TTS)
|
||||||
|
|
||||||
|
### Phase 6: Workflow Initialization 🚫
|
||||||
|
|
||||||
|
- [~] Run `/bmad-bmgd-workflows-workflow-init` (Annulé: Application, pas un Jeu)
|
||||||
|
|
||||||
## Progress Log
|
## Progress Log
|
||||||
|
|
||||||
- **22 Jan 2026** - Analyse complète et PRD créé
|
- **22 Jan 2026** - Analyse complète et PRD créé
|
||||||
|
|||||||
Reference in new issue
Block a user