diff --git a/client/src/pages/remotion-video.tsx b/client/src/pages/remotion-video.tsx
index 98fad1f..2dbd4e4 100644
--- a/client/src/pages/remotion-video.tsx
+++ b/client/src/pages/remotion-video.tsx
@@ -252,7 +252,7 @@ export default function RemotionVideoPage() {
-
Piper TTS — voix configurée sur le serveur
+
TTS — voix activée
diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx
index a972fa7..6e20e8c 100644
--- a/client/src/pages/settings.tsx
+++ b/client/src/pages/settings.tsx
@@ -30,7 +30,6 @@ export default function Settings() {
const [ffmpegApiUrl, setFfmpegApiUrl] = useState("");
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
const [externalApiKey, setExternalApiKey] = useState("");
- const [piperUrl, setPiperUrl] = useState("");
const { toast } = useToast();
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
@@ -61,10 +60,6 @@ export default function Settings() {
enabled: isAdmin,
});
- const { data: piperConfig } = useQuery({
- queryKey: ['/api/piper/config'],
- });
-
useEffect(() => {
if (cloudinaryConfig) {
setCloudName((cloudinaryConfig as any).cloudName || "");
@@ -78,12 +73,6 @@ export default function Settings() {
}
}, [cloudinaryConfig]);
- useEffect(() => {
- if (piperConfig) {
- setPiperUrl((piperConfig as any).url || "");
- }
- }, [piperConfig]);
-
useEffect(() => {
if (openrouterConfig) {
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 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({
mutationFn: () => apiRequest('POST', '/api/settings/external-api', { apiKey: externalApiKey }),
@@ -493,46 +461,6 @@ export default function Settings() {
)}
-
-
-
-
- Configuration Piper TTS (Voix)
-
-
- Configurez l'URL de votre serveur Piper TTS pour la synthèse vocale des Reels
-
-
-
-
-
-
setPiperUrl(e.target.value)}
- placeholder="http://piper-server:5000/api/tts"
- data-testid="input-piper-url"
- />
-
- {hasExistingPiperConfig ? (
- ✓ Configuré
- ) : (
- "URL de l'endpoint HTTP GET de votre serveur Piper (ex: http://piper:5000/api/tts)"
- )}
-
-
-
-
-
-
diff --git a/ffmpeg-service/main.py b/ffmpeg-service/main.py
index 322bffa..d90ca0d 100644
--- a/ffmpeg-service/main.py
+++ b/ffmpeg-service/main.py
@@ -158,7 +158,7 @@ class ReelRequest(BaseModel):
font_size: int = 64
music_volume: float = 0.25
tts_enabled: bool = False
- piper_url: Optional[str] = None
+ tts_voice: str = "fr-FR-VivienneMultilingualNeural"
draw_text: bool = True
stabilize: bool = False # Stabilisation vidéo via vidstab
enable_ending_effect: bool = True
@@ -192,68 +192,19 @@ def clean_text_for_tts(text: str) -> str:
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(
text: str,
+ voice: str,
audio_path: Path,
ass_path: Path,
display_text: Optional[str] = None,
delay: float = 0.0,
- piper_url: Optional[str] = None,
):
"""Generate TTS audio with word-level synchronized subtitles.
- Uses Piper TTS when piper_url is set, otherwise falls back to edge_tts.
- Piper uses ffsubsync for subtitle sync (no word boundaries available).
+ Uses edge_tts.Communicate.stream() to capture WordBoundary events,
+ 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
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)
has_tts = False
tts_clean_text = ""
- tts_error_msg = None
if request.tts_enabled and request.text:
try:
# Clean text for TTS (remove hashtags/emojis)
tts_clean_text = clean_text_for_tts(request.text)
- print(f"🔊 TTS enabled. Piper URL: {bool(request.piper_url)}")
- print(f"🔊 TTS cleaned: '{tts_clean_text[:80]}...'")
+ print(f"🔊 TTS enabled. Original: '{request.text}'")
+ 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:
print(f"🔊 Generating TTS audio to: {tts_audio_path}")
await generate_tts_with_subs(
tts_clean_text,
+ voice,
tts_audio_path,
tts_ass_path,
display_text=clean_text_for_display(request.text),
delay=2.0,
- piper_url=request.piper_url,
)
# Verify files were created
@@ -863,14 +823,11 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
)
has_tts = True
else:
- tts_error_msg = "TTS audio file missing or empty after generation"
- print(f"❌ {tts_error_msg}")
+ print("❌ TTS audio file missing or empty!")
else:
- tts_error_msg = "TTS text empty after cleaning (hashtags/emojis removed)"
- print(f"⚠️ {tts_error_msg}")
+ print("⚠️ TTS text is empty after cleaning, skipping.")
except Exception as e:
import traceback
- tts_error_msg = str(e)
print(f"❌ Failed to generate TTS: {e}")
traceback.print_exc()
@@ -1199,7 +1156,6 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
"output_base64": out_b64,
"duration": duration,
"processing_stats": stats,
- "tts_error": tts_error_msg,
}
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)
+ # 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(
clean_text,
+ voice,
tts_audio_path,
tts_srt_path,
display_text=clean_text_for_display(request.text),
- piper_url=request.piper_url,
)
if not tts_audio_path.exists():
diff --git a/server/migrate.ts b/server/migrate.ts
index f5445ac..eca3cb5 100644
--- a/server/migrate.ts
+++ b/server/migrate.ts
@@ -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.");
} catch (error) {
console.error("[Migration] Error during migration:", error);
diff --git a/server/routes.ts b/server/routes.ts
index 33bc22a..dd42b18 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -10,7 +10,7 @@ import passport from "./auth";
import { z } from "zod";
import { openRouterService } from "./services/openrouter";
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 { analyticsRouter } from "./routes/analytics";
import { reelsRouter } from "./routes/reels";
@@ -423,43 +423,6 @@ export async function registerRoutes(app: Express): Promise {
// Analytics Routes
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
app.use("/api", requireAuth, reelsRouter);
app.use("/api/remotion", requireAuth, remotionRouter);
diff --git a/server/routes/reels.ts b/server/routes/reels.ts
index eab169f..82af644 100644
--- a/server/routes/reels.ts
+++ b/server/routes/reels.ts
@@ -320,18 +320,12 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
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
const result = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
text: overlayText,
musicUrl: finalMusicUrl,
ttsEnabled,
- piperUrl,
+ ttsVoice,
wordDuration: finalWordDuration,
fontSize,
musicVolume,
@@ -370,8 +364,7 @@ reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => {
return res.status(400).json({ error: 'Texte requis' });
}
- const piperCfg = await storage.getPiperConfig(user.id);
- const result = await ffmpegService.previewTTS(text, piperCfg?.url);
+ const result = await ffmpegService.previewTTS(text, voice);
if (!result.success) {
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;
- let piperUrlBg: string | undefined;
- if (ttsEnabled) {
- const piperCfg = await storage.getPiperConfig(userId);
- piperUrlBg = piperCfg?.url ?? undefined;
- }
-
- console.log('🔊 [Background] TTS config:', { ttsEnabled, hasPiperUrl: !!piperUrlBg });
+ console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsVoice });
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
text: overlayText,
musicUrl: finalMusicUrl,
ttsEnabled,
- piperUrl: piperUrlBg,
+ ttsVoice,
wordDuration: finalWordDuration,
fontSize,
musicVolume,
diff --git a/server/services/ffmpeg.ts b/server/services/ffmpeg.ts
index 3c785af..6bd6cd6 100644
--- a/server/services/ffmpeg.ts
+++ b/server/services/ffmpeg.ts
@@ -12,7 +12,7 @@ interface FFmpegReelRequest {
music_id?: string; // ID de la musique (catalogue FFmpeg)
music_url?: string; // OU URL directe de la musique
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)
font_size?: number; // Taille police (default: 24)
music_volume?: number; // Volume musique (default: 0.25)
@@ -71,7 +71,7 @@ export class FFmpegService {
musicId?: string;
musicUrl?: string;
ttsEnabled?: boolean;
- piperUrl?: string;
+ ttsVoice?: string;
wordDuration?: number;
fontSize?: number;
musicVolume?: number;
@@ -90,7 +90,7 @@ export class FFmpegService {
music_id: options.musicId,
music_url: options.musicUrl,
tts_enabled: options.ttsEnabled,
- piper_url: options.piperUrl,
+ tts_voice: options.ttsVoice,
word_duration: options.wordDuration ?? 0.6,
font_size: options.fontSize ?? 64,
music_volume: options.musicVolume ?? 0.25,
@@ -172,7 +172,7 @@ export class FFmpegService {
musicId?: string;
musicUrl?: string;
ttsEnabled?: boolean;
- piperUrl?: string;
+ ttsVoice?: string;
wordDuration?: number;
fontSize?: number;
musicVolume?: number;
@@ -191,7 +191,7 @@ export class FFmpegService {
music_id: options.musicId,
music_url: options.musicUrl,
tts_enabled: options.ttsEnabled,
- piper_url: options.piperUrl,
+ tts_voice: options.ttsVoice,
word_duration: options.wordDuration ?? 0.6,
font_size: options.fontSize ?? 64,
music_volume: options.musicVolume ?? 0.25,
@@ -216,7 +216,6 @@ export class FFmpegService {
hasMusicId: !!options.musicId,
hasMusicUrl: !!options.musicUrl,
ttsEnabled: options.ttsEnabled,
- hasPiperUrl: !!options.piperUrl,
drawText: options.drawText,
});
@@ -291,7 +290,7 @@ export class FFmpegService {
}
async previewTTS(
text: string,
- piperUrl?: string
+ ttsVoice?: string
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
const config = this.ensureConfigured();
@@ -305,7 +304,7 @@ export class FFmpegService {
body: JSON.stringify({
text,
tts_enabled: true,
- piper_url: piperUrl,
+ tts_voice: ttsVoice,
}),
});
diff --git a/server/storage.ts b/server/storage.ts
index 73d7f35..9457e47 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -31,9 +31,6 @@ import {
musicFavorites,
type MusicFavorite,
type InsertMusicFavorite,
- piperConfig,
- type PiperConfig,
- type InsertPiperConfig,
audioTracks,
type AudioTrack,
type InsertAudioTrack,
@@ -120,15 +117,11 @@ export interface IStorage {
// Audio Tracks
getAudioTracks(): Promise;
+ // Audio Tracks
getAudioTrack(id: string): Promise;
createAudioTrack(track: InsertAudioTrack): Promise;
deleteAudioTrack(id: string): Promise;
- // Piper Config
- getPiperConfig(userId: string): Promise;
- createPiperConfig(config: InsertPiperConfig): Promise;
- updatePiperConfig(userId: string, config: Partial): Promise;
-
// App Config
getAppConfig(): Promise;
upsertAppConfig(data: Partial>): Promise;
@@ -590,24 +583,6 @@ export class DatabaseStorage implements IStorage {
const [created] = await db.insert(appConfig).values(data).returning();
return created;
}
- // Piper Config
- async getPiperConfig(userId: string): Promise {
- const [config] = await db.select().from(piperConfig).where(eq(piperConfig.userId, userId));
- return config || undefined;
- }
-
- async createPiperConfig(config: InsertPiperConfig): Promise {
- const [newConfig] = await db.insert(piperConfig).values(config).returning();
- return newConfig;
- }
-
- async updatePiperConfig(userId: string, config: Partial): Promise {
- const [updated] = await db.update(piperConfig)
- .set({ ...config, updatedAt: new Date() })
- .where(eq(piperConfig.userId, userId))
- .returning();
- return updated;
- }
}
export const storage = new DatabaseStorage();
diff --git a/shared/schema.ts b/shared/schema.ts
index c3c82a5..507a438 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -133,14 +133,6 @@ export const openrouterConfig = pgTable("openrouter_config", {
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", {
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
userId: varchar("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
@@ -470,22 +462,6 @@ export type InsertMusicFavorite = z.infer;
export type AudioTrack = typeof audioTracks.$inferSelect;
export type InsertAudioTrack = z.infer;
-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;
-export type UpdatePiperConfig = z.infer;
-
export type AppConfig = typeof appConfig.$inferSelect;