mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-17 20:26:24 +02:00
Add Google Gemini TTS as alternative to Edge TTS
- Store Gemini API key globally in appConfig (single key for all users)
- Add /api/settings/gemini GET/POST/DELETE routes for API key management
- Backend: add tts_engine parameter ("edge" or "gemini") to FFmpeg service
- Backend: add generate_tts_gemini() using Google Cloud TTS REST API
- Frontend: Settings page shows Google Gemini API key input card
- Frontend: new-reel, mobile/new-reel, remotion-video, mobile/remotion-video
pages now have Edge/Gemini engine toggle and French voice selector
- Fix tts-preview route to extract ttsVoice from req.body instead of
undefined voice variable
This commit is contained in:
1 parent
59bc56aedc
commit
72b13b3e42
12 files changed
+605
-29
No files matched your search
@@ -420,6 +420,41 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// Google Gemini API Key (TTS)
|
||||
app.get("/api/settings/gemini", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const config = await storage.getAppConfig();
|
||||
res.json({ configured: !!(config?.geminiApiKey) });
|
||||
} catch (error) {
|
||||
console.error("Error fetching Gemini config:", error);
|
||||
res.status(500).json({ error: "Erreur lors de la récupération de la configuration" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/settings/gemini", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { apiKey } = req.body;
|
||||
if (!apiKey || typeof apiKey !== "string" || apiKey.trim() === "") {
|
||||
return res.status(400).json({ error: "La clé API est requise" });
|
||||
}
|
||||
await storage.upsertAppConfig({ geminiApiKey: apiKey.trim() });
|
||||
res.json({ success: true, configured: true });
|
||||
} catch (error) {
|
||||
console.error("Error saving Gemini config:", error);
|
||||
res.status(500).json({ error: "Erreur lors de la sauvegarde de la configuration" });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/settings/gemini", requireAdmin, async (req, res) => {
|
||||
try {
|
||||
await storage.upsertAppConfig({ geminiApiKey: null });
|
||||
res.json({ success: true, configured: false });
|
||||
} catch (error) {
|
||||
console.error("Error deleting Gemini config:", error);
|
||||
res.status(500).json({ error: "Erreur lors de la suppression de la clé" });
|
||||
}
|
||||
});
|
||||
|
||||
// Analytics Routes
|
||||
app.use("/api/analytics", requireAuth, analyticsRouter);
|
||||
|
||||
|
||||
+29
-3
@@ -270,6 +270,8 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
musicUrl,
|
||||
overlayText,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsEngine,
|
||||
wordDuration = 0.6,
|
||||
fontSize = 64,
|
||||
musicVolume = 0.25,
|
||||
@@ -318,6 +320,13 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
console.error('Error fetching watermark configuration', e);
|
||||
}
|
||||
|
||||
// Get Gemini API key if using Gemini TTS
|
||||
let geminiApiKey: string | undefined = undefined;
|
||||
if (ttsEngine === "gemini") {
|
||||
const appCfg = await storage.getAppConfig();
|
||||
geminiApiKey = appCfg?.geminiApiKey ?? undefined;
|
||||
}
|
||||
|
||||
const finalWordDuration = wordDuration;
|
||||
|
||||
// Traiter la vidéo via FFmpeg
|
||||
@@ -326,6 +335,8 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
musicUrl: finalMusicUrl,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsEngine,
|
||||
geminiApiKey,
|
||||
wordDuration: finalWordDuration,
|
||||
fontSize,
|
||||
musicVolume,
|
||||
@@ -358,13 +369,13 @@ 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 } = req.body;
|
||||
const { text, ttsVoice, ttsEngine } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: 'Texte requis' });
|
||||
}
|
||||
|
||||
const result = await ffmpegService.previewTTS(text, voice);
|
||||
const result = await ffmpegService.previewTTS(text, ttsVoice, ttsEngine, undefined);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
|
||||
@@ -455,6 +466,8 @@ async function processReelBackground(
|
||||
overlayText?: string;
|
||||
description?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsEngine?: string;
|
||||
pageIds: string[];
|
||||
scheduledFor?: string;
|
||||
wordDuration?: number;
|
||||
@@ -473,6 +486,8 @@ async function processReelBackground(
|
||||
overlayText,
|
||||
description,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsEngine,
|
||||
pageIds,
|
||||
scheduledFor,
|
||||
wordDuration,
|
||||
@@ -541,15 +556,24 @@ async function processReelBackground(
|
||||
console.error('Error fetching watermark configuration', e);
|
||||
}
|
||||
|
||||
// Get Gemini API key if using Gemini TTS
|
||||
let geminiApiKey: string | undefined = undefined;
|
||||
if (ttsEngine === "gemini") {
|
||||
const appCfg = await storage.getAppConfig();
|
||||
geminiApiKey = appCfg?.geminiApiKey ?? undefined;
|
||||
}
|
||||
|
||||
const finalWordDuration = wordDuration ?? 0.6;
|
||||
|
||||
console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsVoice });
|
||||
console.log('🔊 [Background] TTS config:', { ttsEnabled, ttsVoice, ttsEngine });
|
||||
|
||||
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||
text: overlayText,
|
||||
musicUrl: finalMusicUrl,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsEngine,
|
||||
geminiApiKey,
|
||||
wordDuration: finalWordDuration,
|
||||
fontSize,
|
||||
musicVolume,
|
||||
@@ -713,6 +737,8 @@ reelsRouter.post('/reels', async (req: Request, res: Response) => {
|
||||
overlayText,
|
||||
description,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsEngine,
|
||||
pageIds,
|
||||
scheduledFor,
|
||||
wordDuration = 0.6,
|
||||
|
||||
@@ -13,6 +13,8 @@ interface FFmpegReelRequest {
|
||||
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_engine?: string; // Moteur TTS: "edge" ou "gemini"
|
||||
gemini_api_key?: string; // Clé API Google Gemini pour 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)
|
||||
@@ -72,6 +74,8 @@ export class FFmpegService {
|
||||
musicUrl?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsEngine?: string;
|
||||
geminiApiKey?: string;
|
||||
wordDuration?: number;
|
||||
fontSize?: number;
|
||||
musicVolume?: number;
|
||||
@@ -91,6 +95,8 @@ export class FFmpegService {
|
||||
music_url: options.musicUrl,
|
||||
tts_enabled: options.ttsEnabled,
|
||||
tts_voice: options.ttsVoice,
|
||||
tts_engine: options.ttsEngine,
|
||||
gemini_api_key: options.geminiApiKey,
|
||||
word_duration: options.wordDuration ?? 0.6,
|
||||
font_size: options.fontSize ?? 64,
|
||||
music_volume: options.musicVolume ?? 0.25,
|
||||
@@ -173,6 +179,8 @@ export class FFmpegService {
|
||||
musicUrl?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsEngine?: string;
|
||||
geminiApiKey?: string;
|
||||
wordDuration?: number;
|
||||
fontSize?: number;
|
||||
musicVolume?: number;
|
||||
@@ -192,6 +200,8 @@ export class FFmpegService {
|
||||
music_url: options.musicUrl,
|
||||
tts_enabled: options.ttsEnabled,
|
||||
tts_voice: options.ttsVoice,
|
||||
tts_engine: options.ttsEngine,
|
||||
gemini_api_key: options.geminiApiKey,
|
||||
word_duration: options.wordDuration ?? 0.6,
|
||||
font_size: options.fontSize ?? 64,
|
||||
music_volume: options.musicVolume ?? 0.25,
|
||||
@@ -290,7 +300,9 @@ export class FFmpegService {
|
||||
}
|
||||
async previewTTS(
|
||||
text: string,
|
||||
ttsVoice?: string
|
||||
ttsVoice?: string,
|
||||
ttsEngine?: string,
|
||||
geminiApiKey?: string
|
||||
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
|
||||
const config = this.ensureConfigured();
|
||||
|
||||
@@ -305,6 +317,8 @@ export class FFmpegService {
|
||||
text,
|
||||
tts_enabled: true,
|
||||
tts_voice: ttsVoice,
|
||||
tts_engine: ttsEngine,
|
||||
gemini_api_key: geminiApiKey,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -124,7 +124,7 @@ export interface IStorage {
|
||||
|
||||
// App Config
|
||||
getAppConfig(): Promise<AppConfig | undefined>;
|
||||
upsertAppConfig(data: Partial<Pick<AppConfig, 'externalApiKey'>>): Promise<AppConfig>;
|
||||
upsertAppConfig(data: Partial<Pick<AppConfig, 'externalApiKey' | 'geminiApiKey'>>): Promise<AppConfig>;
|
||||
}
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
@@ -571,7 +571,7 @@ export class DatabaseStorage implements IStorage {
|
||||
return config || undefined;
|
||||
}
|
||||
|
||||
async upsertAppConfig(data: Partial<Pick<AppConfig, 'externalApiKey'>>): Promise<AppConfig> {
|
||||
async upsertAppConfig(data: Partial<Pick<AppConfig, 'externalApiKey' | 'geminiApiKey'>>): Promise<AppConfig> {
|
||||
const existing = await this.getAppConfig();
|
||||
if (existing) {
|
||||
const [updated] = await db.update(appConfig)
|
||||
|
||||
Reference in new issue
Block a user