mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
refactor: remove Piper TTS, keep edge_tts only
- Remove piper_url from ReelRequest model and all function signatures - Remove generate_tts_piper() function and Piper branch in generate_tts_with_subs() - Remove /api/piper/config routes from server/routes.ts - Remove piperConfig table, schemas and storage methods - Remove piper_config migration - Remove Piper TTS settings UI card - Update "Piper TTS" labels to "TTS — voix activée" edge_tts is now the only TTS engine, using precise word-boundary timing
This commit is contained in:
1 parent
8736e4f6a0
commit
59bc56aedc
12 files changed
+46
-265
No files matched your search
@@ -448,7 +448,7 @@ export default function MobileNewReel() {
|
|||||||
<div className="mt-4 space-y-2 ml-1 p-3 bg-muted/30 rounded-lg border border-border/50">
|
<div className="mt-4 space-y-2 ml-1 p-3 bg-muted/30 rounded-lg border border-border/50">
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Mic className="w-4 h-4" />
|
<Mic className="w-4 h-4" />
|
||||||
<span>Piper TTS — voix configurée sur le serveur</span>
|
<span>TTS — voix activée</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ export default function MobileRemotionVideoPage() {
|
|||||||
</div>
|
</div>
|
||||||
{ttsEnabled && (
|
{ttsEnabled && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-xs text-muted-foreground">Piper TTS — voix configurée sur le serveur</p>
|
<p className="text-xs text-muted-foreground">TTS — voix activée</p>
|
||||||
<Button size="sm" variant="outline" className="w-full" onClick={handleTtsPreview} disabled={!overlayText}>
|
<Button size="sm" variant="outline" className="w-full" onClick={handleTtsPreview} disabled={!overlayText}>
|
||||||
<Volume2 className="mr-2 w-3 h-3" /> Écouter la voix
|
<Volume2 className="mr-2 w-3 h-3" /> Écouter la voix
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -762,7 +762,7 @@ export default function NewReel() {
|
|||||||
<div className="mt-4 space-y-2 ml-12 p-4 bg-muted/30 rounded-lg border border-border/50">
|
<div className="mt-4 space-y-2 ml-12 p-4 bg-muted/30 rounded-lg border border-border/50">
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Mic className="w-4 h-4" />
|
<Mic className="w-4 h-4" />
|
||||||
<span>Piper TTS — voix configurée sur le serveur</span>
|
<span>TTS — voix activée</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 flex gap-2">
|
<div className="mt-3 flex gap-2">
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export default function RemotionVideoPage() {
|
|||||||
</div>
|
</div>
|
||||||
{ttsEnabled && (
|
{ttsEnabled && (
|
||||||
<div className="space-y-3 p-3 bg-muted/30 rounded-lg border">
|
<div className="space-y-3 p-3 bg-muted/30 rounded-lg border">
|
||||||
<p className="text-sm text-muted-foreground">Piper TTS — voix configurée sur le serveur</p>
|
<p className="text-sm text-muted-foreground">TTS — voix activée</p>
|
||||||
<Button size="sm" variant="outline" className="w-full" onClick={handleTtsPreview} disabled={!overlayText}>
|
<Button size="sm" variant="outline" className="w-full" onClick={handleTtsPreview} disabled={!overlayText}>
|
||||||
<Volume2 className="mr-2 w-3 h-3" /> Écouter la voix
|
<Volume2 className="mr-2 w-3 h-3" /> Écouter la voix
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export default function Settings() {
|
|||||||
const [ffmpegApiUrl, setFfmpegApiUrl] = useState("");
|
const [ffmpegApiUrl, setFfmpegApiUrl] = useState("");
|
||||||
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
|
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
|
||||||
const [externalApiKey, setExternalApiKey] = useState("");
|
const [externalApiKey, setExternalApiKey] = useState("");
|
||||||
const [piperUrl, setPiperUrl] = useState("");
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
||||||
@@ -61,10 +60,6 @@ export default function Settings() {
|
|||||||
enabled: isAdmin,
|
enabled: isAdmin,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: piperConfig } = useQuery({
|
|
||||||
queryKey: ['/api/piper/config'],
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cloudinaryConfig) {
|
if (cloudinaryConfig) {
|
||||||
setCloudName((cloudinaryConfig as any).cloudName || "");
|
setCloudName((cloudinaryConfig as any).cloudName || "");
|
||||||
@@ -78,12 +73,6 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
}, [cloudinaryConfig]);
|
}, [cloudinaryConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (piperConfig) {
|
|
||||||
setPiperUrl((piperConfig as any).url || "");
|
|
||||||
}
|
|
||||||
}, [piperConfig]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openrouterConfig) {
|
if (openrouterConfig) {
|
||||||
setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-3.5-sonnet");
|
setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-3.5-sonnet");
|
||||||
@@ -224,27 +213,6 @@ export default function Settings() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
|
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
|
||||||
const hasExistingPiperConfig = !!(piperConfig as any)?.url;
|
|
||||||
|
|
||||||
const savePiperMutation = useMutation({
|
|
||||||
mutationFn: () => {
|
|
||||||
return apiRequest('POST', '/api/piper/config', { url: piperUrl });
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/piper/config'] });
|
|
||||||
toast({
|
|
||||||
title: "Configuration sauvegardée",
|
|
||||||
description: "L'URL du serveur Piper TTS a été enregistrée",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast({
|
|
||||||
title: "Erreur",
|
|
||||||
description: "Impossible de sauvegarder la configuration Piper",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const saveExternalApiMutation = useMutation({
|
const saveExternalApiMutation = useMutation({
|
||||||
mutationFn: () => apiRequest('POST', '/api/settings/external-api', { apiKey: externalApiKey }),
|
mutationFn: () => apiRequest('POST', '/api/settings/external-api', { apiKey: externalApiKey }),
|
||||||
@@ -493,46 +461,6 @@ export default function Settings() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
|
||||||
<CardHeader className="p-6">
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Mic className="w-5 h-5" />
|
|
||||||
Configuration Piper TTS (Voix)
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Configurez l'URL de votre serveur Piper TTS pour la synthèse vocale des Reels
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="piperUrl">URL du serveur Piper</Label>
|
|
||||||
<Input
|
|
||||||
id="piperUrl"
|
|
||||||
type="url"
|
|
||||||
value={piperUrl}
|
|
||||||
onChange={(e) => setPiperUrl(e.target.value)}
|
|
||||||
placeholder="http://piper-server:5000/api/tts"
|
|
||||||
data-testid="input-piper-url"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{hasExistingPiperConfig ? (
|
|
||||||
<span className="text-green-600 dark:text-green-400">✓ Configuré</span>
|
|
||||||
) : (
|
|
||||||
"URL de l'endpoint HTTP GET de votre serveur Piper (ex: http://piper:5000/api/tts)"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => savePiperMutation.mutate()}
|
|
||||||
disabled={savePiperMutation.isPending || !piperUrl.trim()}
|
|
||||||
data-testid="button-save-piper"
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{savePiperMutation.isPending ? "Enregistrement..." : "Enregistrer Piper TTS"}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||||
<CardHeader className="p-6">
|
<CardHeader className="p-6">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
|||||||
+29
-64
@@ -158,7 +158,7 @@ class ReelRequest(BaseModel):
|
|||||||
font_size: int = 64
|
font_size: int = 64
|
||||||
music_volume: float = 0.25
|
music_volume: float = 0.25
|
||||||
tts_enabled: bool = False
|
tts_enabled: bool = False
|
||||||
piper_url: Optional[str] = None
|
tts_voice: str = "fr-FR-VivienneMultilingualNeural"
|
||||||
draw_text: bool = True
|
draw_text: bool = True
|
||||||
stabilize: bool = False # Stabilisation vidéo via vidstab
|
stabilize: bool = False # Stabilisation vidéo via vidstab
|
||||||
enable_ending_effect: bool = True
|
enable_ending_effect: bool = True
|
||||||
@@ -192,68 +192,19 @@ def clean_text_for_tts(text: str) -> str:
|
|||||||
return " ".join(text.split())
|
return " ".join(text.split())
|
||||||
|
|
||||||
|
|
||||||
def generate_tts_piper(text: str, piper_url: str, audio_path: Path) -> None:
|
|
||||||
"""Generate TTS audio using Piper TTS HTTP API."""
|
|
||||||
print(f"🔊 Piper TTS request: url={piper_url}, text_len={len(text)}")
|
|
||||||
response = requests.get(piper_url, params={"text": text}, timeout=60)
|
|
||||||
print(f"🔊 Piper API response: status={response.status_code}, content_type={response.headers.get('Content-Type', '?')}")
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
tmp_path = audio_path.with_suffix(".tmp")
|
|
||||||
with open(tmp_path, "wb") as f:
|
|
||||||
f.write(response.content)
|
|
||||||
|
|
||||||
subprocess.run([
|
|
||||||
"ffmpeg", "-y", "-i", str(tmp_path),
|
|
||||||
"-codec:a", "libmp3lame", "-b:a", "128k",
|
|
||||||
str(audio_path)
|
|
||||||
], check=True, capture_output=True)
|
|
||||||
tmp_path.unlink(missing_ok=True)
|
|
||||||
print(f"✅ Piper TTS audio saved: {audio_path.stat().st_size} bytes")
|
|
||||||
|
|
||||||
|
|
||||||
async def generate_tts_with_subs(
|
async def generate_tts_with_subs(
|
||||||
text: str,
|
text: str,
|
||||||
|
voice: str,
|
||||||
audio_path: Path,
|
audio_path: Path,
|
||||||
ass_path: Path,
|
ass_path: Path,
|
||||||
display_text: Optional[str] = None,
|
display_text: Optional[str] = None,
|
||||||
delay: float = 0.0,
|
delay: float = 0.0,
|
||||||
piper_url: Optional[str] = None,
|
|
||||||
):
|
):
|
||||||
"""Generate TTS audio with word-level synchronized subtitles.
|
"""Generate TTS audio with word-level synchronized subtitles.
|
||||||
|
|
||||||
Uses Piper TTS when piper_url is set, otherwise falls back to edge_tts.
|
Uses edge_tts.Communicate.stream() to capture WordBoundary events,
|
||||||
Piper uses ffsubsync for subtitle sync (no word boundaries available).
|
providing millisecond-accurate subtitle timing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Piper branch: no WordBoundary events, use ffsubsync for subtitle sync
|
|
||||||
if piper_url:
|
|
||||||
text_to_display = display_text if display_text else text
|
|
||||||
generate_tts_piper(text, piper_url, audio_path)
|
|
||||||
|
|
||||||
audio_duration = None
|
|
||||||
try:
|
|
||||||
dur_cmd = [
|
|
||||||
"ffprobe", "-v", "error", "-show_entries", "format=duration",
|
|
||||||
"-of", "default=noprint_wrappers=1:nokey=1", str(audio_path),
|
|
||||||
]
|
|
||||||
dur_proc = subprocess.run(dur_cmd, stdout=subprocess.PIPE, text=True)
|
|
||||||
audio_duration = float(dur_proc.stdout.strip())
|
|
||||||
print(f"⏱️ Piper TTS Duration: {audio_duration:.2f}s")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Could not measure Piper TTS duration: {e}")
|
|
||||||
|
|
||||||
unsynced_srt_path = audio_path.with_suffix(".unsynced.srt")
|
|
||||||
synced_srt_path = audio_path.with_suffix(".synced.srt")
|
|
||||||
generate_unsynced_srt(text_to_display, unsynced_srt_path, total_duration=audio_duration)
|
|
||||||
run_ffsubsync(audio_path, unsynced_srt_path, synced_srt_path)
|
|
||||||
convert_srt_to_ass(synced_srt_path, ass_path, font_size=65, delay=delay)
|
|
||||||
print("✅ Piper TTS synchronisation completed with ffsubsync")
|
|
||||||
return
|
|
||||||
|
|
||||||
# edge_tts fallback branch
|
|
||||||
voice = "fr-FR-VivienneMultilingualNeural"
|
|
||||||
|
|
||||||
# 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 ["Remi", "Henri", "Paul"])
|
is_male = any(name in voice for name in ["Remi", "Henri", "Paul"])
|
||||||
|
|
||||||
@@ -836,24 +787,33 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
# 3. Generate TTS (if enabled)
|
# 3. Generate TTS (if enabled)
|
||||||
has_tts = False
|
has_tts = False
|
||||||
tts_clean_text = ""
|
tts_clean_text = ""
|
||||||
tts_error_msg = None
|
|
||||||
|
|
||||||
if request.tts_enabled and request.text:
|
if request.tts_enabled and request.text:
|
||||||
try:
|
try:
|
||||||
# Clean text for TTS (remove hashtags/emojis)
|
# Clean text for TTS (remove hashtags/emojis)
|
||||||
tts_clean_text = clean_text_for_tts(request.text)
|
tts_clean_text = clean_text_for_tts(request.text)
|
||||||
print(f"🔊 TTS enabled. Piper URL: {bool(request.piper_url)}")
|
print(f"🔊 TTS enabled. Original: '{request.text}'")
|
||||||
print(f"🔊 TTS cleaned: '{tts_clean_text[:80]}...'")
|
print(f"🔊 TTS cleaned: '{tts_clean_text}'")
|
||||||
|
|
||||||
|
# Determine voice
|
||||||
|
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"
|
||||||
|
print(f"🔊 Using voice: {voice}")
|
||||||
|
|
||||||
if tts_clean_text:
|
if tts_clean_text:
|
||||||
print(f"🔊 Generating TTS audio to: {tts_audio_path}")
|
print(f"🔊 Generating TTS audio to: {tts_audio_path}")
|
||||||
await generate_tts_with_subs(
|
await generate_tts_with_subs(
|
||||||
tts_clean_text,
|
tts_clean_text,
|
||||||
|
voice,
|
||||||
tts_audio_path,
|
tts_audio_path,
|
||||||
tts_ass_path,
|
tts_ass_path,
|
||||||
display_text=clean_text_for_display(request.text),
|
display_text=clean_text_for_display(request.text),
|
||||||
delay=2.0,
|
delay=2.0,
|
||||||
piper_url=request.piper_url,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify files were created
|
# Verify files were created
|
||||||
@@ -863,14 +823,11 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
)
|
)
|
||||||
has_tts = True
|
has_tts = True
|
||||||
else:
|
else:
|
||||||
tts_error_msg = "TTS audio file missing or empty after generation"
|
print("❌ TTS audio file missing or empty!")
|
||||||
print(f"❌ {tts_error_msg}")
|
|
||||||
else:
|
else:
|
||||||
tts_error_msg = "TTS text empty after cleaning (hashtags/emojis removed)"
|
print("⚠️ TTS text is empty after cleaning, skipping.")
|
||||||
print(f"⚠️ {tts_error_msg}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
tts_error_msg = str(e)
|
|
||||||
print(f"❌ Failed to generate TTS: {e}")
|
print(f"❌ Failed to generate TTS: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
@@ -1199,7 +1156,6 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
"output_base64": out_b64,
|
"output_base64": out_b64,
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
"processing_stats": stats,
|
"processing_stats": stats,
|
||||||
"tts_error": tts_error_msg,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1226,12 +1182,21 @@ async def preview_tts(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
|
|
||||||
clean_text = clean_text_for_tts(request.text)
|
clean_text = clean_text_for_tts(request.text)
|
||||||
|
|
||||||
|
# Determine voice
|
||||||
|
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(
|
await generate_tts_with_subs(
|
||||||
clean_text,
|
clean_text,
|
||||||
|
voice,
|
||||||
tts_audio_path,
|
tts_audio_path,
|
||||||
tts_srt_path,
|
tts_srt_path,
|
||||||
display_text=clean_text_for_display(request.text),
|
display_text=clean_text_for_display(request.text),
|
||||||
piper_url=request.piper_url,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not tts_audio_path.exists():
|
if not tts_audio_path.exists():
|
||||||
|
|||||||
@@ -152,18 +152,6 @@ export async function migrate() {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// piper_config
|
|
||||||
await client.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS "piper_config" (
|
|
||||||
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"user_id" varchar NOT NULL REFERENCES "users"("id") ON DELETE cascade,
|
|
||||||
"url" text NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT now(),
|
|
||||||
"updated_at" timestamp DEFAULT now(),
|
|
||||||
CONSTRAINT piper_config_user_id_unique UNIQUE ("user_id")
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log("[Migration] Safe migration completed.");
|
console.log("[Migration] Safe migration completed.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Migration] Error during migration:", error);
|
console.error("[Migration] Error during migration:", error);
|
||||||
|
|||||||
+1
-38
@@ -10,7 +10,7 @@ import passport from "./auth";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { openRouterService } from "./services/openrouter";
|
import { openRouterService } from "./services/openrouter";
|
||||||
import { minioService as cloudinaryService, buildMinioUrl } from "./services/minio";
|
import { minioService as cloudinaryService, buildMinioUrl } from "./services/minio";
|
||||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertPiperConfigSchema, updatePiperConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
||||||
import type { User, InsertUser, ScheduledPost } from "@shared/schema";
|
import type { User, InsertUser, ScheduledPost } from "@shared/schema";
|
||||||
import { analyticsRouter } from "./routes/analytics";
|
import { analyticsRouter } from "./routes/analytics";
|
||||||
import { reelsRouter } from "./routes/reels";
|
import { reelsRouter } from "./routes/reels";
|
||||||
@@ -423,43 +423,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Analytics Routes
|
// Analytics Routes
|
||||||
app.use("/api/analytics", requireAuth, analyticsRouter);
|
app.use("/api/analytics", requireAuth, analyticsRouter);
|
||||||
|
|
||||||
// Piper TTS Configuration Routes
|
|
||||||
app.get("/api/piper/config", requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const user = req.user as User;
|
|
||||||
const config = await storage.getPiperConfig(user.id);
|
|
||||||
if (!config) {
|
|
||||||
return res.json(null);
|
|
||||||
}
|
|
||||||
return res.json(config);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching Piper config:", error);
|
|
||||||
res.status(500).json({ error: "Failed to fetch configuration" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/api/piper/config", requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const user = req.user as User;
|
|
||||||
const data = updatePiperConfigSchema.parse(req.body);
|
|
||||||
|
|
||||||
let config = await storage.getPiperConfig(user.id);
|
|
||||||
if (config) {
|
|
||||||
config = await storage.updatePiperConfig(user.id, data);
|
|
||||||
} else {
|
|
||||||
if (!data.url) {
|
|
||||||
return res.status(400).json({ error: "URL is required" });
|
|
||||||
}
|
|
||||||
config = await storage.createPiperConfig({ ...data as any, userId: user.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving Piper config:", error);
|
|
||||||
res.status(500).json({ error: "Failed to save configuration" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reels & Music Routes
|
// Reels & Music Routes
|
||||||
app.use("/api", requireAuth, reelsRouter);
|
app.use("/api", requireAuth, reelsRouter);
|
||||||
app.use("/api/remotion", requireAuth, remotionRouter);
|
app.use("/api/remotion", requireAuth, remotionRouter);
|
||||||
|
|||||||
+4
-17
@@ -320,18 +320,12 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const finalWordDuration = wordDuration;
|
const finalWordDuration = wordDuration;
|
||||||
|
|
||||||
let piperUrl: string | undefined;
|
|
||||||
if (ttsEnabled) {
|
|
||||||
const piperCfg = await storage.getPiperConfig(user.id);
|
|
||||||
piperUrl = piperCfg?.url ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traiter la vidéo via FFmpeg
|
// Traiter la vidéo via FFmpeg
|
||||||
const result = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
const result = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||||
text: overlayText,
|
text: overlayText,
|
||||||
musicUrl: finalMusicUrl,
|
musicUrl: finalMusicUrl,
|
||||||
ttsEnabled,
|
ttsEnabled,
|
||||||
piperUrl,
|
ttsVoice,
|
||||||
wordDuration: finalWordDuration,
|
wordDuration: finalWordDuration,
|
||||||
fontSize,
|
fontSize,
|
||||||
musicVolume,
|
musicVolume,
|
||||||
@@ -370,8 +364,7 @@ reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => {
|
|||||||
return res.status(400).json({ error: 'Texte requis' });
|
return res.status(400).json({ error: 'Texte requis' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const piperCfg = await storage.getPiperConfig(user.id);
|
const result = await ffmpegService.previewTTS(text, voice);
|
||||||
const result = await ffmpegService.previewTTS(text, piperCfg?.url);
|
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
|
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
|
||||||
@@ -550,19 +543,13 @@ async function processReelBackground(
|
|||||||
|
|
||||||
const finalWordDuration = wordDuration ?? 0.6;
|
const finalWordDuration = wordDuration ?? 0.6;
|
||||||
|
|
||||||
let piperUrlBg: string | undefined;
|
console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsVoice });
|
||||||
if (ttsEnabled) {
|
|
||||||
const piperCfg = await storage.getPiperConfig(userId);
|
|
||||||
piperUrlBg = piperCfg?.url ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔊 [Background] TTS config:', { ttsEnabled, hasPiperUrl: !!piperUrlBg });
|
|
||||||
|
|
||||||
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||||
text: overlayText,
|
text: overlayText,
|
||||||
musicUrl: finalMusicUrl,
|
musicUrl: finalMusicUrl,
|
||||||
ttsEnabled,
|
ttsEnabled,
|
||||||
piperUrl: piperUrlBg,
|
ttsVoice,
|
||||||
wordDuration: finalWordDuration,
|
wordDuration: finalWordDuration,
|
||||||
fontSize,
|
fontSize,
|
||||||
musicVolume,
|
musicVolume,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface FFmpegReelRequest {
|
|||||||
music_id?: string; // ID de la musique (catalogue FFmpeg)
|
music_id?: string; // ID de la musique (catalogue FFmpeg)
|
||||||
music_url?: string; // OU URL directe de la musique
|
music_url?: string; // OU URL directe de la musique
|
||||||
tts_enabled?: boolean; // Activation du TTS
|
tts_enabled?: boolean; // Activation du TTS
|
||||||
piper_url?: string; // URL du serveur Piper TTS
|
tts_voice?: string; // Voix TTS (ex: fr-FR-VivienneNeural)
|
||||||
word_duration?: number; // Durée par mot (default: 0.6s)
|
word_duration?: number; // Durée par mot (default: 0.6s)
|
||||||
font_size?: number; // Taille police (default: 24)
|
font_size?: number; // Taille police (default: 24)
|
||||||
music_volume?: number; // Volume musique (default: 0.25)
|
music_volume?: number; // Volume musique (default: 0.25)
|
||||||
@@ -71,7 +71,7 @@ export class FFmpegService {
|
|||||||
musicId?: string;
|
musicId?: string;
|
||||||
musicUrl?: string;
|
musicUrl?: string;
|
||||||
ttsEnabled?: boolean;
|
ttsEnabled?: boolean;
|
||||||
piperUrl?: string;
|
ttsVoice?: string;
|
||||||
wordDuration?: number;
|
wordDuration?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
musicVolume?: number;
|
musicVolume?: number;
|
||||||
@@ -90,7 +90,7 @@ export class FFmpegService {
|
|||||||
music_id: options.musicId,
|
music_id: options.musicId,
|
||||||
music_url: options.musicUrl,
|
music_url: options.musicUrl,
|
||||||
tts_enabled: options.ttsEnabled,
|
tts_enabled: options.ttsEnabled,
|
||||||
piper_url: options.piperUrl,
|
tts_voice: options.ttsVoice,
|
||||||
word_duration: options.wordDuration ?? 0.6,
|
word_duration: options.wordDuration ?? 0.6,
|
||||||
font_size: options.fontSize ?? 64,
|
font_size: options.fontSize ?? 64,
|
||||||
music_volume: options.musicVolume ?? 0.25,
|
music_volume: options.musicVolume ?? 0.25,
|
||||||
@@ -172,7 +172,7 @@ export class FFmpegService {
|
|||||||
musicId?: string;
|
musicId?: string;
|
||||||
musicUrl?: string;
|
musicUrl?: string;
|
||||||
ttsEnabled?: boolean;
|
ttsEnabled?: boolean;
|
||||||
piperUrl?: string;
|
ttsVoice?: string;
|
||||||
wordDuration?: number;
|
wordDuration?: number;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
musicVolume?: number;
|
musicVolume?: number;
|
||||||
@@ -191,7 +191,7 @@ export class FFmpegService {
|
|||||||
music_id: options.musicId,
|
music_id: options.musicId,
|
||||||
music_url: options.musicUrl,
|
music_url: options.musicUrl,
|
||||||
tts_enabled: options.ttsEnabled,
|
tts_enabled: options.ttsEnabled,
|
||||||
piper_url: options.piperUrl,
|
tts_voice: options.ttsVoice,
|
||||||
word_duration: options.wordDuration ?? 0.6,
|
word_duration: options.wordDuration ?? 0.6,
|
||||||
font_size: options.fontSize ?? 64,
|
font_size: options.fontSize ?? 64,
|
||||||
music_volume: options.musicVolume ?? 0.25,
|
music_volume: options.musicVolume ?? 0.25,
|
||||||
@@ -216,7 +216,6 @@ export class FFmpegService {
|
|||||||
hasMusicId: !!options.musicId,
|
hasMusicId: !!options.musicId,
|
||||||
hasMusicUrl: !!options.musicUrl,
|
hasMusicUrl: !!options.musicUrl,
|
||||||
ttsEnabled: options.ttsEnabled,
|
ttsEnabled: options.ttsEnabled,
|
||||||
hasPiperUrl: !!options.piperUrl,
|
|
||||||
drawText: options.drawText,
|
drawText: options.drawText,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,7 +290,7 @@ export class FFmpegService {
|
|||||||
}
|
}
|
||||||
async previewTTS(
|
async previewTTS(
|
||||||
text: string,
|
text: string,
|
||||||
piperUrl?: string
|
ttsVoice?: string
|
||||||
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
|
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
|
||||||
const config = this.ensureConfigured();
|
const config = this.ensureConfigured();
|
||||||
|
|
||||||
@@ -305,7 +304,7 @@ export class FFmpegService {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text,
|
text,
|
||||||
tts_enabled: true,
|
tts_enabled: true,
|
||||||
piper_url: piperUrl,
|
tts_voice: ttsVoice,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-26
@@ -31,9 +31,6 @@ import {
|
|||||||
musicFavorites,
|
musicFavorites,
|
||||||
type MusicFavorite,
|
type MusicFavorite,
|
||||||
type InsertMusicFavorite,
|
type InsertMusicFavorite,
|
||||||
piperConfig,
|
|
||||||
type PiperConfig,
|
|
||||||
type InsertPiperConfig,
|
|
||||||
audioTracks,
|
audioTracks,
|
||||||
type AudioTrack,
|
type AudioTrack,
|
||||||
type InsertAudioTrack,
|
type InsertAudioTrack,
|
||||||
@@ -120,15 +117,11 @@ export interface IStorage {
|
|||||||
|
|
||||||
// Audio Tracks
|
// Audio Tracks
|
||||||
getAudioTracks(): Promise<AudioTrack[]>;
|
getAudioTracks(): Promise<AudioTrack[]>;
|
||||||
|
// Audio Tracks
|
||||||
getAudioTrack(id: string): Promise<AudioTrack | undefined>;
|
getAudioTrack(id: string): Promise<AudioTrack | undefined>;
|
||||||
createAudioTrack(track: InsertAudioTrack): Promise<AudioTrack>;
|
createAudioTrack(track: InsertAudioTrack): Promise<AudioTrack>;
|
||||||
deleteAudioTrack(id: string): Promise<void>;
|
deleteAudioTrack(id: string): Promise<void>;
|
||||||
|
|
||||||
// Piper Config
|
|
||||||
getPiperConfig(userId: string): Promise<PiperConfig | undefined>;
|
|
||||||
createPiperConfig(config: InsertPiperConfig): Promise<PiperConfig>;
|
|
||||||
updatePiperConfig(userId: string, config: Partial<InsertPiperConfig>): Promise<PiperConfig>;
|
|
||||||
|
|
||||||
// App Config
|
// App Config
|
||||||
getAppConfig(): Promise<AppConfig | undefined>;
|
getAppConfig(): Promise<AppConfig | undefined>;
|
||||||
upsertAppConfig(data: Partial<Pick<AppConfig, 'externalApiKey'>>): Promise<AppConfig>;
|
upsertAppConfig(data: Partial<Pick<AppConfig, 'externalApiKey'>>): Promise<AppConfig>;
|
||||||
@@ -590,24 +583,6 @@ export class DatabaseStorage implements IStorage {
|
|||||||
const [created] = await db.insert(appConfig).values(data).returning();
|
const [created] = await db.insert(appConfig).values(data).returning();
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
// Piper Config
|
|
||||||
async getPiperConfig(userId: string): Promise<PiperConfig | undefined> {
|
|
||||||
const [config] = await db.select().from(piperConfig).where(eq(piperConfig.userId, userId));
|
|
||||||
return config || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createPiperConfig(config: InsertPiperConfig): Promise<PiperConfig> {
|
|
||||||
const [newConfig] = await db.insert(piperConfig).values(config).returning();
|
|
||||||
return newConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updatePiperConfig(userId: string, config: Partial<InsertPiperConfig>): Promise<PiperConfig> {
|
|
||||||
const [updated] = await db.update(piperConfig)
|
|
||||||
.set({ ...config, updatedAt: new Date() })
|
|
||||||
.where(eq(piperConfig.userId, userId))
|
|
||||||
.returning();
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storage = new DatabaseStorage();
|
export const storage = new DatabaseStorage();
|
||||||
@@ -133,14 +133,6 @@ export const openrouterConfig = pgTable("openrouter_config", {
|
|||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const piperConfig = pgTable("piper_config", {
|
|
||||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
|
||||||
userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
|
||||||
url: text("url").notNull(),
|
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const media = pgTable("media", {
|
export const media = pgTable("media", {
|
||||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||||
@@ -470,22 +462,6 @@ export type InsertMusicFavorite = z.infer<typeof insertMusicFavoriteSchema>;
|
|||||||
export type AudioTrack = typeof audioTracks.$inferSelect;
|
export type AudioTrack = typeof audioTracks.$inferSelect;
|
||||||
export type InsertAudioTrack = z.infer<typeof insertAudioTrackSchema>;
|
export type InsertAudioTrack = z.infer<typeof insertAudioTrackSchema>;
|
||||||
|
|
||||||
export const insertPiperConfigSchema = createInsertSchema(piperConfig).omit({
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updatePiperConfigSchema = insertPiperConfigSchema.omit({
|
|
||||||
userId: true
|
|
||||||
}).partial({
|
|
||||||
url: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export type PiperConfig = typeof piperConfig.$inferSelect;
|
|
||||||
export type InsertPiperConfig = z.infer<typeof insertPiperConfigSchema>;
|
|
||||||
export type UpdatePiperConfig = z.infer<typeof updatePiperConfigSchema>;
|
|
||||||
|
|
||||||
export type AppConfig = typeof appConfig.$inferSelect;
|
export type AppConfig = typeof appConfig.$inferSelect;
|
||||||
|
|
||||||
|
|
||||||
Reference in new issue
Block a user