mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-18 04:36:32 +02:00
refactor(tts): replace Minimax+Freesound with Piper TTS
Remove Minimax Speech API and Freesound integrations entirely. Add Piper TTS as the sole TTS provider via configurable HTTP URL. - Add piper_config DB table (url field, per-user) - Add GET/POST /api/piper/config routes - Python: replace generate_tts_minimax with generate_tts_piper (GET ?text=, WAV→MP3) - Simplify generate_tts_with_subs: piper_url param replaces tts_provider+minimax fields - Drop ttsVoice/ttsProvider from all UI, API, and background job params - Settings page: replace Minimax+Freesound cards with single Piper URL input - Remove freeSoundService init from server startup and CSP headers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0209c3eacf
commit
797e12dde5
14 files changed
+152
-725
No files matched your search
+2
-30
@@ -15,7 +15,6 @@ import { ensureAdminUserExists } from "./init-admin";
|
||||
import { startTokenCron } from "./cron";
|
||||
import { migrate } from "./migrate";
|
||||
import { jamendoService } from "./services/jamendo";
|
||||
import { freeSoundService } from "./services/freesound";
|
||||
import { ffmpegService } from "./services/ffmpeg";
|
||||
|
||||
const app = express();
|
||||
@@ -34,8 +33,8 @@ app.use(helmet({
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||
connectSrc: ["'self'", "https://graph.facebook.com", "https://openrouter.ai", "https://res.cloudinary.com", "https://freesound.org", "wss:", "ws:"],
|
||||
mediaSrc: ["'self'", "https://cdn.freesound.org", "https:", "data:"],
|
||||
connectSrc: ["'self'", "https://graph.facebook.com", "https://openrouter.ai", "https://res.cloudinary.com", "wss:", "ws:"],
|
||||
mediaSrc: ["'self'", "https:", "data:"],
|
||||
}
|
||||
},
|
||||
crossOriginEmbedderPolicy: false,
|
||||
@@ -209,33 +208,6 @@ app.use((req, res, next) => {
|
||||
const jamendoClientId = process.env.JAMENDO_CLIENT_ID || '5b6bba49';
|
||||
jamendoService.configure(jamendoClientId);
|
||||
|
||||
// Configurer le service FreeSound pour les Reels
|
||||
// 1. Essayer de charger depuis la DB (suppose un utilisateur admin ou premier utilisateur)
|
||||
// Note: Dans un contexte multi-utilisateur, cela devrait être fait par requête.
|
||||
// Pour l'instant, on charge la config du premier user trouvé ou via variable d'env.
|
||||
|
||||
let freesoundConfigured = false;
|
||||
try {
|
||||
const { storage } = await import("./storage");
|
||||
const allUsers = await storage.getAllUsers();
|
||||
if (allUsers.length > 0) {
|
||||
const dbConfig = await storage.getFreesoundConfig(allUsers[0].id);
|
||||
if (dbConfig && dbConfig.clientId && dbConfig.clientSecret) {
|
||||
freeSoundService.configure(dbConfig.clientId, dbConfig.clientSecret);
|
||||
freesoundConfigured = true;
|
||||
log('🎵 FreeSound configured from Database');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load Freesound config from DB:', err);
|
||||
}
|
||||
|
||||
if (!freesoundConfigured && process.env.FREESOUND_CLIENT_ID && process.env.FREESOUND_CLIENT_SECRET) {
|
||||
freeSoundService.configure(process.env.FREESOUND_CLIENT_ID, process.env.FREESOUND_CLIENT_SECRET);
|
||||
log('🎵 FreeSound configured from Environment Variables');
|
||||
} else if (!freesoundConfigured) {
|
||||
log('⚠️ FreeSound credentials not found - music search will be disabled');
|
||||
}
|
||||
|
||||
// Configurer le service FFmpeg pour le traitement vidéo (si configuré)
|
||||
if (process.env.FFMPEG_API_URL && process.env.FFMPEG_API_KEY) {
|
||||
|
||||
+4
-10
@@ -152,24 +152,18 @@ export async function migrate() {
|
||||
);
|
||||
`);
|
||||
|
||||
// minimax_config
|
||||
// piper_config
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS "minimax_config" (
|
||||
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,
|
||||
"api_key" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now(),
|
||||
CONSTRAINT minimax_config_user_id_unique UNIQUE ("user_id")
|
||||
CONSTRAINT piper_config_user_id_unique UNIQUE ("user_id")
|
||||
);
|
||||
`);
|
||||
|
||||
// minimax_config.group_id
|
||||
await client.query(`
|
||||
ALTER TABLE "minimax_config"
|
||||
ADD COLUMN IF NOT EXISTS "group_id" text;
|
||||
`);
|
||||
|
||||
console.log("[Migration] Safe migration completed.");
|
||||
} catch (error) {
|
||||
console.error("[Migration] Error during migration:", error);
|
||||
|
||||
+15
-78
@@ -10,9 +10,8 @@ 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, insertFreesoundConfigSchema, updateFreesoundConfigSchema, updateMinimaxConfigSchema, insertMinimaxConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
||||
import type { User, InsertUser, ScheduledPost, FreesoundConfig } from "@shared/schema";
|
||||
import { freeSoundService } from "./services/freesound";
|
||||
import { insertPostSchema, insertScheduledPostSchema, insertSocialPageSchema, insertAiGenerationSchema, insertCloudinaryConfigSchema, updateCloudinaryConfigSchema, insertOpenrouterConfigSchema, updateOpenrouterConfigSchema, insertPiperConfigSchema, updatePiperConfigSchema, 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";
|
||||
import { remotionRouter } from "./routes/remotion";
|
||||
@@ -424,101 +423,39 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Analytics Routes
|
||||
app.use("/api/analytics", requireAuth, analyticsRouter);
|
||||
|
||||
// Freesound Configuration Routes
|
||||
app.get("/api/freesound/config", requireAuth, async (req, res) => {
|
||||
// Piper TTS Configuration Routes
|
||||
app.get("/api/piper/config", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const config = await storage.getFreesoundConfig(user.id);
|
||||
const config = await storage.getPiperConfig(user.id);
|
||||
if (!config) {
|
||||
return res.json(null);
|
||||
}
|
||||
return res.json({
|
||||
...config,
|
||||
clientSecret: config.clientSecret ? "********" : "",
|
||||
});
|
||||
return res.json(config);
|
||||
} catch (error) {
|
||||
console.error("Error fetching Freesound config:", error);
|
||||
console.error("Error fetching Piper config:", error);
|
||||
res.status(500).json({ error: "Failed to fetch configuration" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/freesound/config", requireAuth, async (req, res) => {
|
||||
app.post("/api/piper/config", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const data = updateFreesoundConfigSchema.parse(req.body);
|
||||
|
||||
let config = await storage.getFreesoundConfig(user.id);
|
||||
const updateData: any = { ...data };
|
||||
|
||||
if (!updateData.clientSecret || updateData.clientSecret === "********") {
|
||||
delete updateData.clientSecret;
|
||||
}
|
||||
const data = updatePiperConfigSchema.parse(req.body);
|
||||
|
||||
let config = await storage.getPiperConfig(user.id);
|
||||
if (config) {
|
||||
config = await storage.updateFreesoundConfig(user.id, updateData);
|
||||
config = await storage.updatePiperConfig(user.id, data);
|
||||
} else {
|
||||
if (!data.clientId || !data.clientSecret) {
|
||||
return res.status(400).json({ error: "Client ID and Secret are required" });
|
||||
if (!data.url) {
|
||||
return res.status(400).json({ error: "URL is required" });
|
||||
}
|
||||
config = await storage.createFreesoundConfig({
|
||||
...data as any,
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
|
||||
if (config && config.clientId && config.clientSecret) {
|
||||
freeSoundService.configure(config.clientId, config.clientSecret);
|
||||
config = await storage.createPiperConfig({ ...data as any, userId: user.id });
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error saving Freesound config:", error);
|
||||
res.status(500).json({ error: "Failed to save configuration" });
|
||||
}
|
||||
});
|
||||
|
||||
// Minimax Configuration Routes
|
||||
app.get("/api/minimax/config", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const config = await storage.getMinimaxConfig(user.id);
|
||||
if (!config) {
|
||||
return res.json(null);
|
||||
}
|
||||
return res.json({
|
||||
...config,
|
||||
apiKey: config.apiKey ? "********" : "",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching Minimax config:", error);
|
||||
res.status(500).json({ error: "Failed to fetch configuration" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/minimax/config", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const data = updateMinimaxConfigSchema.parse(req.body);
|
||||
|
||||
let config = await storage.getMinimaxConfig(user.id);
|
||||
const updateData: any = { ...data };
|
||||
|
||||
if (!updateData.apiKey || updateData.apiKey === "********") {
|
||||
delete updateData.apiKey;
|
||||
}
|
||||
|
||||
if (config) {
|
||||
config = await storage.updateMinimaxConfig(user.id, updateData);
|
||||
} else {
|
||||
if (!data.apiKey) {
|
||||
return res.status(400).json({ error: "API Key is required" });
|
||||
}
|
||||
config = await storage.createMinimaxConfig({ ...data as any, userId: user.id });
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error saving Minimax config:", error);
|
||||
console.error("Error saving Piper config:", error);
|
||||
res.status(500).json({ error: "Failed to save configuration" });
|
||||
}
|
||||
});
|
||||
|
||||
+23
-98
@@ -5,7 +5,6 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import type { User, Media, SocialPage } from '@shared/schema';
|
||||
import { storage } from '../storage';
|
||||
import { freeSoundService, type MusicTrack } from '../services/freesound';
|
||||
import { ffmpegService } from '../services/ffmpeg';
|
||||
import { facebookService } from '../services/facebook';
|
||||
import { minioService as cloudinaryService, buildMinioUrl, resolveInternalUrl } from '../services/minio';
|
||||
@@ -36,21 +35,12 @@ reelsRouter.get('/music/search', async (req: Request, res: Response) => {
|
||||
search,
|
||||
} = req.query;
|
||||
|
||||
const tracks = await freeSoundService.searchMusicByDuration({
|
||||
minDuration: parseInt(minDuration as string),
|
||||
maxDuration: parseInt(maxDuration as string),
|
||||
genre: genre as string | undefined,
|
||||
limit: parseInt(limit as string),
|
||||
offset: parseInt(offset as string),
|
||||
search: search as string | undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
tracks,
|
||||
tracks: [],
|
||||
pagination: {
|
||||
offset: parseInt(offset as string),
|
||||
limit: parseInt(limit as string),
|
||||
hasMore: tracks.length === parseInt(limit as string),
|
||||
hasMore: false,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -74,21 +64,12 @@ reelsRouter.get('/music/more', async (req: Request, res: Response) => {
|
||||
search,
|
||||
} = req.query;
|
||||
|
||||
const tracks = await freeSoundService.searchMusicByDuration({
|
||||
minDuration: parseInt(minDuration as string),
|
||||
maxDuration: parseInt(maxDuration as string),
|
||||
genre: genre as string | undefined,
|
||||
limit: parseInt(limit as string),
|
||||
offset: parseInt(offset as string),
|
||||
search: search as string | undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
tracks,
|
||||
tracks: [],
|
||||
pagination: {
|
||||
offset: parseInt(offset as string),
|
||||
limit: parseInt(limit as string),
|
||||
hasMore: tracks.length === parseInt(limit as string),
|
||||
hasMore: false,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -103,9 +84,7 @@ reelsRouter.get('/music/more', async (req: Request, res: Response) => {
|
||||
*/
|
||||
reelsRouter.get('/music/popular', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
const tracks = await freeSoundService.getPopularTracks(limit);
|
||||
res.json({ tracks });
|
||||
res.json({ tracks: [] });
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching popular music:', error);
|
||||
res.status(500).json({ error: 'Erreur lors de la récupération des musiques populaires' });
|
||||
@@ -229,13 +208,7 @@ reelsRouter.delete('/music/favorites/:trackId', async (req: Request, res: Respon
|
||||
reelsRouter.get('/music/:trackId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { trackId } = req.params;
|
||||
const track = await freeSoundService.getMusicDetails(trackId);
|
||||
|
||||
if (!track) {
|
||||
return res.status(404).json({ error: 'Musique non trouvée' });
|
||||
}
|
||||
|
||||
res.json(track);
|
||||
return res.status(404).json({ error: 'Musique non trouvée' });
|
||||
} catch (error) {
|
||||
console.error('❌ Error fetching music details:', error);
|
||||
res.status(500).json({ error: 'Erreur lors de la récupération des détails' });
|
||||
@@ -297,8 +270,6 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
musicUrl,
|
||||
overlayText,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
wordDuration = 0.6,
|
||||
fontSize = 64,
|
||||
musicVolume = 0.25,
|
||||
@@ -334,11 +305,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
console.log(`🎵 Audio URL for ffmpeg: ${finalMusicUrl}`);
|
||||
}
|
||||
} else {
|
||||
const track = await freeSoundService.getMusicDetails(musicTrackId);
|
||||
if (track) {
|
||||
finalMusicUrl = track.downloadUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let watermarkUrl: string | undefined = undefined;
|
||||
@@ -353,15 +320,10 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
|
||||
const finalWordDuration = wordDuration;
|
||||
|
||||
let minimaxApiKey: string | undefined;
|
||||
let minimaxGroupId: string | undefined;
|
||||
if (ttsEnabled && ttsProvider === 'minimax') {
|
||||
const minimaxCfg = await storage.getMinimaxConfig(user.id);
|
||||
if (!minimaxCfg?.apiKey) {
|
||||
return res.status(400).json({ error: 'Clé API Minimax non configurée. Allez dans les Paramètres.' });
|
||||
}
|
||||
minimaxApiKey = minimaxCfg.apiKey;
|
||||
minimaxGroupId = minimaxCfg.groupId ?? undefined;
|
||||
let piperUrl: string | undefined;
|
||||
if (ttsEnabled) {
|
||||
const piperCfg = await storage.getPiperConfig(user.id);
|
||||
piperUrl = piperCfg?.url ?? undefined;
|
||||
}
|
||||
|
||||
// Traiter la vidéo via FFmpeg
|
||||
@@ -369,10 +331,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
text: overlayText,
|
||||
musicUrl: finalMusicUrl,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
minimaxApiKey,
|
||||
minimaxGroupId,
|
||||
piperUrl,
|
||||
wordDuration: finalWordDuration,
|
||||
fontSize,
|
||||
musicVolume,
|
||||
@@ -405,24 +364,14 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const user = req.user as User;
|
||||
const { text, voice, ttsProvider } = req.body;
|
||||
const { text } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: 'Texte requis' });
|
||||
}
|
||||
|
||||
let minimaxApiKey: string | undefined;
|
||||
let minimaxGroupId: string | undefined;
|
||||
if (ttsProvider === 'minimax') {
|
||||
const minimaxCfg = await storage.getMinimaxConfig(user.id);
|
||||
if (!minimaxCfg?.apiKey) {
|
||||
return res.status(400).json({ error: 'Clé API Minimax non configurée. Allez dans les Paramètres.' });
|
||||
}
|
||||
minimaxApiKey = minimaxCfg.apiKey;
|
||||
minimaxGroupId = minimaxCfg.groupId ?? undefined;
|
||||
}
|
||||
|
||||
const result = await ffmpegService.previewTTS(text, voice, ttsProvider, minimaxApiKey, minimaxGroupId);
|
||||
const piperCfg = await storage.getPiperConfig(user.id);
|
||||
const result = await ffmpegService.previewTTS(text, piperCfg?.url);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
|
||||
@@ -513,8 +462,6 @@ async function processReelBackground(
|
||||
overlayText?: string;
|
||||
description?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsProvider?: string;
|
||||
pageIds: string[];
|
||||
scheduledFor?: string;
|
||||
wordDuration?: number;
|
||||
@@ -533,8 +480,6 @@ async function processReelBackground(
|
||||
overlayText,
|
||||
description,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
pageIds,
|
||||
scheduledFor,
|
||||
wordDuration,
|
||||
@@ -579,15 +524,6 @@ async function processReelBackground(
|
||||
console.log(`🎵 [Background] Audio URL for ffmpeg: ${finalMusicUrl}`);
|
||||
}
|
||||
} catch (e) { console.error('Error fetching internal track', e); }
|
||||
} else {
|
||||
try {
|
||||
const track = await freeSoundService.getMusicDetails(musicTrackId);
|
||||
if (track) {
|
||||
finalMusicUrl = track.downloadUrl;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`⚠️ [Background] Failed to fetch music details for ${musicTrackId}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,28 +550,19 @@ async function processReelBackground(
|
||||
|
||||
const finalWordDuration = wordDuration ?? 0.6;
|
||||
|
||||
let minimaxApiKeyBg: string | undefined;
|
||||
let minimaxGroupIdBg: string | undefined;
|
||||
if (ttsEnabled && ttsProvider === 'minimax') {
|
||||
const minimaxCfg = await storage.getMinimaxConfig(userId);
|
||||
if (minimaxCfg?.apiKey) {
|
||||
minimaxApiKeyBg = minimaxCfg.apiKey;
|
||||
minimaxGroupIdBg = minimaxCfg.groupId ?? undefined;
|
||||
} else {
|
||||
console.error('❌ [Background] Minimax API key not found in DB for user:', userId);
|
||||
}
|
||||
let piperUrlBg: string | undefined;
|
||||
if (ttsEnabled) {
|
||||
const piperCfg = await storage.getPiperConfig(userId);
|
||||
piperUrlBg = piperCfg?.url ?? undefined;
|
||||
}
|
||||
|
||||
console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsProvider, ttsVoice, hasMinimaxKey: !!minimaxApiKeyBg, hasGroupId: !!minimaxGroupIdBg });
|
||||
console.log('🔊 [Background] TTS config:', { ttsEnabled, hasPiperUrl: !!piperUrlBg });
|
||||
|
||||
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||
text: overlayText,
|
||||
musicUrl: finalMusicUrl,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
minimaxApiKey: minimaxApiKeyBg,
|
||||
minimaxGroupId: minimaxGroupIdBg,
|
||||
piperUrl: piperUrlBg,
|
||||
wordDuration: finalWordDuration,
|
||||
fontSize,
|
||||
musicVolume,
|
||||
@@ -799,7 +726,6 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => {
|
||||
overlayText,
|
||||
description,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
pageIds,
|
||||
scheduledFor,
|
||||
wordDuration = 0.6,
|
||||
@@ -889,12 +815,11 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => {
|
||||
*/
|
||||
reelsRouter.get('/reels/config', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const freesoundConfigured = await freeSoundService.testConnection().catch(() => false);
|
||||
const ffmpegConfigured = await ffmpegService.healthCheck().catch(() => false);
|
||||
|
||||
res.json({
|
||||
jamendo: { configured: freesoundConfigured }, // Keep legacy key for frontend compatibility
|
||||
freesound: { configured: freesoundConfigured },
|
||||
jamendo: { configured: false },
|
||||
freesound: { configured: false },
|
||||
ffmpeg: { configured: ffmpegConfigured },
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -199,7 +199,6 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, {
|
||||
? req.body.overlayText.trim()
|
||||
: undefined;
|
||||
|
||||
const ttsVoice: string = req.body.ttsVoice || "fr-FR-VivienneMultilingualNeural";
|
||||
const musicVolume: number = parseFloat(req.body.musicVolume ?? "0.3");
|
||||
|
||||
// Music: HTTP URLs are fine for audio (Remotion uses Web Audio API, not <Img>)
|
||||
@@ -248,7 +247,7 @@ remotionRouter.post("/render", upload.fields([{ name: "images", maxCount: 4 }, {
|
||||
if (ttsText) {
|
||||
console.log("🎙️ Generating TTS for:", ttsText);
|
||||
try {
|
||||
const ttsResult = await ffmpegService.previewTTS(ttsText, ttsVoice);
|
||||
const ttsResult = await ffmpegService.previewTTS(ttsText);
|
||||
if (ttsResult.success && ttsResult.audioBase64) {
|
||||
const audioFilename = `tts-${Date.now()}.mp3`;
|
||||
const audioPath = path.join(uploadDir, audioFilename);
|
||||
|
||||
@@ -12,10 +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
|
||||
tts_voice?: string; // Voix TTS (ex: fr-FR-VivienneNeural)
|
||||
tts_provider?: string; // Provider TTS: "edge_tts" (défaut) ou "minimax"
|
||||
minimax_api_key?: string; // Clé API Minimax (requis si tts_provider=minimax)
|
||||
minimax_group_id?: string; // Group ID Minimax (requis pour quotas corrects)
|
||||
piper_url?: string; // URL du serveur Piper TTS
|
||||
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)
|
||||
@@ -74,10 +71,7 @@ export class FFmpegService {
|
||||
musicId?: string;
|
||||
musicUrl?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsProvider?: string;
|
||||
minimaxApiKey?: string;
|
||||
minimaxGroupId?: string;
|
||||
piperUrl?: string;
|
||||
wordDuration?: number;
|
||||
fontSize?: number;
|
||||
musicVolume?: number;
|
||||
@@ -96,10 +90,7 @@ export class FFmpegService {
|
||||
music_id: options.musicId,
|
||||
music_url: options.musicUrl,
|
||||
tts_enabled: options.ttsEnabled,
|
||||
tts_voice: options.ttsVoice,
|
||||
tts_provider: options.ttsProvider,
|
||||
minimax_api_key: options.minimaxApiKey,
|
||||
minimax_group_id: options.minimaxGroupId,
|
||||
piper_url: options.piperUrl,
|
||||
word_duration: options.wordDuration ?? 0.6,
|
||||
font_size: options.fontSize ?? 64,
|
||||
music_volume: options.musicVolume ?? 0.25,
|
||||
@@ -181,10 +172,7 @@ export class FFmpegService {
|
||||
musicId?: string;
|
||||
musicUrl?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsProvider?: string;
|
||||
minimaxApiKey?: string;
|
||||
minimaxGroupId?: string;
|
||||
piperUrl?: string;
|
||||
wordDuration?: number;
|
||||
fontSize?: number;
|
||||
musicVolume?: number;
|
||||
@@ -203,10 +191,7 @@ export class FFmpegService {
|
||||
music_id: options.musicId,
|
||||
music_url: options.musicUrl,
|
||||
tts_enabled: options.ttsEnabled,
|
||||
tts_voice: options.ttsVoice,
|
||||
tts_provider: options.ttsProvider,
|
||||
minimax_api_key: options.minimaxApiKey,
|
||||
minimax_group_id: options.minimaxGroupId,
|
||||
piper_url: options.piperUrl,
|
||||
word_duration: options.wordDuration ?? 0.6,
|
||||
font_size: options.fontSize ?? 64,
|
||||
music_volume: options.musicVolume ?? 0.25,
|
||||
@@ -231,12 +216,11 @@ export class FFmpegService {
|
||||
hasMusicId: !!options.musicId,
|
||||
hasMusicUrl: !!options.musicUrl,
|
||||
ttsEnabled: options.ttsEnabled,
|
||||
ttsVoice: options.ttsVoice,
|
||||
hasPiperUrl: !!options.piperUrl,
|
||||
drawText: options.drawText,
|
||||
});
|
||||
|
||||
const debugBody = { ...requestBody };
|
||||
if (debugBody.minimax_api_key) debugBody.minimax_api_key = '***';
|
||||
console.log('📤 Sending to FFmpeg API:', JSON.stringify({ ...debugBody, text: debugBody.text ? `[${debugBody.text.length} chars]` : undefined }));
|
||||
|
||||
try {
|
||||
@@ -307,10 +291,7 @@ export class FFmpegService {
|
||||
}
|
||||
async previewTTS(
|
||||
text: string,
|
||||
voice: string,
|
||||
ttsProvider?: string,
|
||||
minimaxApiKey?: string,
|
||||
minimaxGroupId?: string
|
||||
piperUrl?: string
|
||||
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
|
||||
const config = this.ensureConfigured();
|
||||
|
||||
@@ -323,10 +304,8 @@ export class FFmpegService {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
tts_voice: voice,
|
||||
tts_provider: ttsProvider,
|
||||
minimax_api_key: minimaxApiKey,
|
||||
minimax_group_id: minimaxGroupId,
|
||||
tts_enabled: true,
|
||||
piper_url: piperUrl,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
+15
-37
@@ -31,17 +31,14 @@ import {
|
||||
musicFavorites,
|
||||
type MusicFavorite,
|
||||
type InsertMusicFavorite,
|
||||
freesoundConfig,
|
||||
type FreesoundConfig,
|
||||
type InsertFreesoundConfig,
|
||||
piperConfig,
|
||||
type PiperConfig,
|
||||
type InsertPiperConfig,
|
||||
audioTracks,
|
||||
type AudioTrack,
|
||||
type InsertAudioTrack,
|
||||
appConfig,
|
||||
type AppConfig,
|
||||
minimaxConfig,
|
||||
type MinimaxConfig,
|
||||
type InsertMinimaxConfig,
|
||||
} from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, and, gte, lte, desc, asc, isNull, inArray } from "drizzle-orm";
|
||||
@@ -127,10 +124,10 @@ export interface IStorage {
|
||||
createAudioTrack(track: InsertAudioTrack): Promise<AudioTrack>;
|
||||
deleteAudioTrack(id: string): Promise<void>;
|
||||
|
||||
// Freesound Config
|
||||
getFreesoundConfig(userId: string): Promise<FreesoundConfig | undefined>;
|
||||
createFreesoundConfig(config: InsertFreesoundConfig): Promise<FreesoundConfig>;
|
||||
updateFreesoundConfig(userId: string, config: Partial<InsertFreesoundConfig>): Promise<FreesoundConfig>;
|
||||
// Piper Config
|
||||
getPiperConfig(userId: string): Promise<PiperConfig | undefined>;
|
||||
createPiperConfig(config: InsertPiperConfig): Promise<PiperConfig>;
|
||||
updatePiperConfig(userId: string, config: Partial<InsertPiperConfig>): Promise<PiperConfig>;
|
||||
|
||||
// App Config
|
||||
getAppConfig(): Promise<AppConfig | undefined>;
|
||||
@@ -575,25 +572,6 @@ export class DatabaseStorage implements IStorage {
|
||||
await db.delete(audioTracks).where(eq(audioTracks.id, id));
|
||||
}
|
||||
|
||||
// Freesound Config
|
||||
async getFreesoundConfig(userId: string): Promise<FreesoundConfig | undefined> {
|
||||
const [config] = await db.select().from(freesoundConfig).where(eq(freesoundConfig.userId, userId));
|
||||
return config || undefined;
|
||||
}
|
||||
|
||||
async createFreesoundConfig(config: InsertFreesoundConfig): Promise<FreesoundConfig> {
|
||||
const [newConfig] = await db.insert(freesoundConfig).values(config).returning();
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
async updateFreesoundConfig(userId: string, config: Partial<InsertFreesoundConfig>): Promise<FreesoundConfig> {
|
||||
const [updated] = await db.update(freesoundConfig)
|
||||
.set({ ...config, updatedAt: new Date() })
|
||||
.where(eq(freesoundConfig.userId, userId))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
// App Config
|
||||
async getAppConfig(): Promise<AppConfig | undefined> {
|
||||
const [config] = await db.select().from(appConfig).limit(1);
|
||||
@@ -612,21 +590,21 @@ export class DatabaseStorage implements IStorage {
|
||||
const [created] = await db.insert(appConfig).values(data).returning();
|
||||
return created;
|
||||
}
|
||||
// Minimax Config
|
||||
async getMinimaxConfig(userId: string): Promise<MinimaxConfig | undefined> {
|
||||
const [config] = await db.select().from(minimaxConfig).where(eq(minimaxConfig.userId, userId));
|
||||
// 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 createMinimaxConfig(config: InsertMinimaxConfig): Promise<MinimaxConfig> {
|
||||
const [newConfig] = await db.insert(minimaxConfig).values(config).returning();
|
||||
async createPiperConfig(config: InsertPiperConfig): Promise<PiperConfig> {
|
||||
const [newConfig] = await db.insert(piperConfig).values(config).returning();
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
async updateMinimaxConfig(userId: string, config: Partial<InsertMinimaxConfig>): Promise<MinimaxConfig> {
|
||||
const [updated] = await db.update(minimaxConfig)
|
||||
async updatePiperConfig(userId: string, config: Partial<InsertPiperConfig>): Promise<PiperConfig> {
|
||||
const [updated] = await db.update(piperConfig)
|
||||
.set({ ...config, updatedAt: new Date() })
|
||||
.where(eq(minimaxConfig.userId, userId))
|
||||
.where(eq(piperConfig.userId, userId))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user