From 59bc56aedcd27ed6e4c78698629074cdadded816 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Tue, 19 May 2026 10:55:01 +0200 Subject: [PATCH] refactor: remove Piper TTS, keep edge_tts only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- client/src/pages/mobile/new-reel.tsx | 2 +- client/src/pages/mobile/remotion-video.tsx | 2 +- client/src/pages/new-reel.tsx | 2 +- client/src/pages/remotion-video.tsx | 2 +- client/src/pages/settings.tsx | 72 ----------------- ffmpeg-service/main.py | 93 +++++++--------------- server/migrate.ts | 12 --- server/routes.ts | 39 +-------- server/routes/reels.ts | 21 +---- server/services/ffmpeg.ts | 15 ++-- server/storage.ts | 27 +------ shared/schema.ts | 24 ------ 12 files changed, 46 insertions(+), 265 deletions(-) diff --git a/client/src/pages/mobile/new-reel.tsx b/client/src/pages/mobile/new-reel.tsx index 4c0f6f7..bad2c05 100644 --- a/client/src/pages/mobile/new-reel.tsx +++ b/client/src/pages/mobile/new-reel.tsx @@ -448,7 +448,7 @@ export default function MobileNewReel() {
- Piper TTS — voix configurée sur le serveur + TTS — voix activée
diff --git a/client/src/pages/new-reel.tsx b/client/src/pages/new-reel.tsx index c3aebdb..0c823e9 100644 --- a/client/src/pages/new-reel.tsx +++ b/client/src/pages/new-reel.tsx @@ -762,7 +762,7 @@ export default function NewReel() {
- Piper TTS — voix configurée sur le serveur + TTS — voix activée
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() {
{ttsEnabled && (
-

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;