retrotoon-studio/server/llmConfig.ts
Ubuntu 20a643c4ce fix: audit complet et pipeline fonctionnel RetroToon Studio
Corrections critiques:
- Fix titre HTML {{project_title}} -> %VITE_APP_TITLE%
- Suppression vitePluginManusRuntime (360KB -> 4KB index.html)
- Upload vidéo: multer au lieu du parsing binary maison (anti-corruption)
- Extraction audio ffmpeg + sauvegarde sourceAudioUrl en DB
- Page /login dédiée + correction redirect auth
- Test moteurs IA: vrai HEAD request avec latence
- Suppression spam logs [Auth] Missing session cookie
- Fix fuite passwordHash dans auth.me
- Cookie sameSite: none -> lax (CSRF)

Sécurité:
- Endpoints admin protégés par adminProcedure (role=admin requis)
- Sidebar admin masquée pour non-admins
- AdminPanel: page accès refusé pour non-admins
- Bootstrap admin optimisé (skip rehash si identique)

Fonctionnalités:
- Export vidéo MP4 réel via ffmpeg local (H.264 + AAC audio)
- Download parallèle par batch de 20 (export 10x plus rapide)
- Détection de scènes réelle via ffmpeg scene detect
- Analyse arrière-plans via Gemini Vision (remplace Math.random)
- Gemini: conservation du role system + support image_url
- Suppression thinking.budget_tokens:128 (LLM config)
- Thumbnails de frames dans la timeline
- Toast export avec bouton télécharger
- Endpoint extraction audio à la demande

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-21 01:37:08 +00:00

140 lines
4.2 KiB
TypeScript

/**
* LLM Configuration Module
* Reads the admin-configured LLM model from the database and provides
* a unified invokeLLM wrapper that routes to the correct provider.
*
* Supported providers:
* - "built-in": Uses the default Manus Forge API (default)
* - "gemini-flash": Uses Google Gemini 2.5 Flash via the Gemini API
* - "gpt-4o", "claude-sonnet", "gemini-pro": Placeholder for future providers
*/
import { ENV } from "./_core/env";
import { invokeLLM as forgeInvokeLLM, type InvokeParams, type InvokeResult } from "./_core/llm";
export interface LlmConfig {
model: string;
behavior: string;
}
let cachedConfig: LlmConfig | null = null;
let cacheTimestamp = 0;
const CACHE_TTL = 30_000; // 30 seconds
/**
* Get the current LLM configuration from the database
*/
export async function getLlmConfig(): Promise<LlmConfig> {
const now = Date.now();
if (cachedConfig && (now - cacheTimestamp) < CACHE_TTL) {
return cachedConfig;
}
try {
const { getDb } = await import("./db");
const db = await getDb();
if (db) {
const { appConfig } = await import("../drizzle/schema");
const { eq } = await import("drizzle-orm");
const result = await db.select().from(appConfig).where(eq(appConfig.key, "llm_config")).limit(1);
if (result[0]?.value) {
const parsed = JSON.parse(result[0].value);
cachedConfig = {
model: parsed.model || "built-in",
behavior: parsed.behavior || "guided",
};
cacheTimestamp = now;
return cachedConfig;
}
}
} catch (err) {
console.warn("[LLM Config] Failed to load config:", err);
}
// Default config
cachedConfig = { model: "built-in", behavior: "guided" };
cacheTimestamp = now;
return cachedConfig;
}
/**
* Invalidate the cached LLM config (call after admin saves)
*/
export function invalidateLlmConfigCache(): void {
cachedConfig = null;
cacheTimestamp = 0;
}
/**
* Invoke LLM using the configured provider
* Falls back to built-in if the configured provider fails
*/
export async function invokeConfiguredLLM(params: InvokeParams): Promise<InvokeResult> {
const config = await getLlmConfig();
if (config.model === "gemini-flash" && ENV.geminiApiKey) {
return invokeGemini(params);
}
// Default: use built-in Forge LLM
return forgeInvokeLLM(params);
}
/**
* Invoke Google Gemini API directly using the configured API key
*/
async function invokeGemini(params: InvokeParams): Promise<InvokeResult> {
const apiKey = ENV.geminiApiKey;
if (!apiKey) {
throw new Error("GEMINI_API_KEY is not configured");
}
// Use the OpenAI-compatible endpoint for Gemini
const url = `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`;
const messages = params.messages.map(msg => {
if (typeof msg.content === "string") {
return { role: msg.role, content: msg.content };
}
const parts = Array.isArray(msg.content) ? msg.content : [msg.content];
const mapped = parts.map(p => {
if (typeof p === "string") return { type: "text" as const, text: p };
if (p.type === "text") return p;
if (p.type === "image_url") return p;
return { type: "text" as const, text: "" };
}).filter(p => p.type !== "text" || ("text" in p && p.text));
if (mapped.length === 1 && mapped[0].type === "text") {
return { role: msg.role, content: (mapped[0] as any).text };
}
return { role: msg.role, content: mapped };
});
const payload: Record<string, unknown> = {
model: "gemini-2.5-flash",
messages,
max_tokens: 8192,
};
if (params.response_format || params.responseFormat) {
payload.response_format = params.response_format || params.responseFormat;
}
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorText = await response.text();
console.error("[Gemini] API error:", response.status, errorText);
// Fallback to built-in on error
console.warn("[Gemini] Falling back to built-in LLM");
return forgeInvokeLLM(params);
}
return (await response.json()) as InvokeResult;
}