mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-16 11:49:30 +02:00
feat: add Minimax Speech as alternative TTS provider for Reels
- Add minimax_config table (migration + schema + storage CRUD) - Add GET/POST /api/minimax/config routes - Pass tts_provider + minimax_api_key through ffmpeg service - Add generate_tts_minimax() in Python using Minimax T2A v2 API - Fall back to ffsubsync for subtitle sync (no WordBoundary events) - Add Minimax config card in Settings page - Add provider toggle (Edge TTS / Minimax) + French voices in new-reel
This commit is contained in:
1 parent
9932561050
commit
b7ef4940f7
9 files changed
+364
-29
No files matched your search
@@ -152,6 +152,18 @@ export async function migrate() {
|
||||
);
|
||||
`);
|
||||
|
||||
// minimax_config
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS "minimax_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,
|
||||
"created_at" timestamp DEFAULT now(),
|
||||
"updated_at" timestamp DEFAULT now(),
|
||||
CONSTRAINT minimax_config_user_id_unique UNIQUE ("user_id")
|
||||
);
|
||||
`);
|
||||
|
||||
console.log("[Migration] Safe migration completed.");
|
||||
} catch (error) {
|
||||
console.error("[Migration] Error during migration:", error);
|
||||
|
||||
+47
-1
@@ -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, insertFreesoundConfigSchema, updateFreesoundConfigSchema, insertUserSchema, postMedia, type SocialPage } from "@shared/schema";
|
||||
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 { analyticsRouter } from "./routes/analytics";
|
||||
@@ -477,6 +477,52 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
res.status(500).json({ error: "Failed to save configuration" });
|
||||
}
|
||||
});
|
||||
|
||||
// Reels & Music Routes
|
||||
app.use("/api", requireAuth, reelsRouter);
|
||||
app.use("/api/remotion", requireAuth, remotionRouter);
|
||||
|
||||
+36
-2
@@ -298,6 +298,7 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
overlayText,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
wordDuration = 0.6,
|
||||
fontSize = 64,
|
||||
musicVolume = 0.25,
|
||||
@@ -352,12 +353,23 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
|
||||
const finalWordDuration = wordDuration;
|
||||
|
||||
let minimaxApiKey: 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;
|
||||
}
|
||||
|
||||
// Traiter la vidéo via FFmpeg
|
||||
const result = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||
text: overlayText,
|
||||
musicUrl: finalMusicUrl,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
minimaxApiKey,
|
||||
wordDuration: finalWordDuration,
|
||||
fontSize,
|
||||
musicVolume,
|
||||
@@ -389,13 +401,23 @@ reelsRouter.post('/reels/preview', async (req: Request, res: Response) => {
|
||||
*/
|
||||
reelsRouter.post('/reels/tts-preview', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { text, voice } = req.body;
|
||||
const user = req.user as User;
|
||||
const { text, voice, ttsProvider } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: 'Texte requis' });
|
||||
}
|
||||
|
||||
const result = await ffmpegService.previewTTS(text, voice);
|
||||
let minimaxApiKey: 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;
|
||||
}
|
||||
|
||||
const result = await ffmpegService.previewTTS(text, voice, ttsProvider, minimaxApiKey);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(500).json({ error: result.error || 'Erreur de génération TTS' });
|
||||
@@ -487,6 +509,7 @@ async function processReelBackground(
|
||||
description?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsProvider?: string;
|
||||
pageIds: string[];
|
||||
scheduledFor?: string;
|
||||
wordDuration?: number;
|
||||
@@ -506,6 +529,7 @@ async function processReelBackground(
|
||||
description,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
pageIds,
|
||||
scheduledFor,
|
||||
wordDuration,
|
||||
@@ -585,11 +609,21 @@ async function processReelBackground(
|
||||
|
||||
const finalWordDuration = wordDuration ?? 0.6;
|
||||
|
||||
let minimaxApiKeyBg: string | undefined;
|
||||
if (ttsEnabled && ttsProvider === 'minimax') {
|
||||
const minimaxCfg = await storage.getMinimaxConfig(userId);
|
||||
if (minimaxCfg?.apiKey) {
|
||||
minimaxApiKeyBg = minimaxCfg.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
const ffmpegResult = await ffmpegService.processReelFromUrl(resolveInternalUrl(media.originalUrl), {
|
||||
text: overlayText,
|
||||
musicUrl: finalMusicUrl,
|
||||
ttsEnabled,
|
||||
ttsVoice,
|
||||
ttsProvider,
|
||||
minimaxApiKey: minimaxApiKeyBg,
|
||||
wordDuration: finalWordDuration,
|
||||
fontSize,
|
||||
musicVolume,
|
||||
|
||||
@@ -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_provider?: string; // Provider TTS: "edge_tts" (défaut) ou "minimax"
|
||||
minimax_api_key?: string; // Clé API Minimax (requis si tts_provider=minimax)
|
||||
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,6 +73,8 @@ export class FFmpegService {
|
||||
musicUrl?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsProvider?: string;
|
||||
minimaxApiKey?: string;
|
||||
wordDuration?: number;
|
||||
fontSize?: number;
|
||||
musicVolume?: number;
|
||||
@@ -90,6 +94,8 @@ export class FFmpegService {
|
||||
music_url: options.musicUrl,
|
||||
tts_enabled: options.ttsEnabled,
|
||||
tts_voice: options.ttsVoice,
|
||||
tts_provider: options.ttsProvider,
|
||||
minimax_api_key: options.minimaxApiKey,
|
||||
word_duration: options.wordDuration ?? 0.6,
|
||||
font_size: options.fontSize ?? 64,
|
||||
music_volume: options.musicVolume ?? 0.25,
|
||||
@@ -172,6 +178,8 @@ export class FFmpegService {
|
||||
musicUrl?: string;
|
||||
ttsEnabled?: boolean;
|
||||
ttsVoice?: string;
|
||||
ttsProvider?: string;
|
||||
minimaxApiKey?: string;
|
||||
wordDuration?: number;
|
||||
fontSize?: number;
|
||||
musicVolume?: number;
|
||||
@@ -191,6 +199,8 @@ export class FFmpegService {
|
||||
music_url: options.musicUrl,
|
||||
tts_enabled: options.ttsEnabled,
|
||||
tts_voice: options.ttsVoice,
|
||||
tts_provider: options.ttsProvider,
|
||||
minimax_api_key: options.minimaxApiKey,
|
||||
word_duration: options.wordDuration ?? 0.6,
|
||||
font_size: options.fontSize ?? 64,
|
||||
music_volume: options.musicVolume ?? 0.25,
|
||||
@@ -285,7 +295,9 @@ export class FFmpegService {
|
||||
}
|
||||
async previewTTS(
|
||||
text: string,
|
||||
voice: string
|
||||
voice: string,
|
||||
ttsProvider?: string,
|
||||
minimaxApiKey?: string
|
||||
): Promise<{ success: boolean; audioBase64?: string; error?: string }> {
|
||||
const config = this.ensureConfigured();
|
||||
|
||||
@@ -298,7 +310,9 @@ export class FFmpegService {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text,
|
||||
tts_voice: voice
|
||||
tts_voice: voice,
|
||||
tts_provider: ttsProvider,
|
||||
minimax_api_key: minimaxApiKey,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ import {
|
||||
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";
|
||||
@@ -609,6 +612,24 @@ 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));
|
||||
return config || undefined;
|
||||
}
|
||||
|
||||
async createMinimaxConfig(config: InsertMinimaxConfig): Promise<MinimaxConfig> {
|
||||
const [newConfig] = await db.insert(minimaxConfig).values(config).returning();
|
||||
return newConfig;
|
||||
}
|
||||
|
||||
async updateMinimaxConfig(userId: string, config: Partial<InsertMinimaxConfig>): Promise<MinimaxConfig> {
|
||||
const [updated] = await db.update(minimaxConfig)
|
||||
.set({ ...config, updatedAt: new Date() })
|
||||
.where(eq(minimaxConfig.userId, userId))
|
||||
.returning();
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new DatabaseStorage();
|
||||
Reference in new issue
Block a user