Files

125 lines
4.4 KiB
TypeScript
Raw Permalink Normal View History

import { BoothConfig, BoothStyle } from "@/types/booth";
import { generateWithGemini } from "./gemini";
import { generateWithOpenRouter } from "./openrouter";
import {
isProviderAvailable,
triggerCooldown,
resetProvider,
} from "./circuit-breaker";
import { ContentSafetyError, isContentSafetyError } from "./safety";
export interface OrchestrationRequest {
config: BoothConfig;
style: BoothStyle;
imageBase64: string;
mimeType?: string;
}
export interface OrchestrationResult {
imageUrl: string;
providerUsed: "gemini" | "openrouter" | "demo_mock";
modelUsed: string;
durationMs: number;
fallbackTriggered: boolean;
warnings?: string[];
}
export async function orchestrateGeneration(
request: OrchestrationRequest
): Promise<OrchestrationResult> {
const startTime = Date.now();
const { config, style, imageBase64 } = request;
const mimeType = request.mimeType || "image/jpeg";
const warnings: string[] = [];
const hasGeminiKey = Boolean(process.env.GEMINI_API_KEY);
const hasOpenRouterKey = Boolean(process.env.OPENROUTER_API_KEY);
// Demo / Offline Mode fallback when no API keys are provided yet
if (!hasGeminiKey && !hasOpenRouterKey) {
console.log("[AI Orchestrator] No API keys configured. Using Demo Mode sample image.");
// Simulate generation delay (2 seconds)
await new Promise((r) => setTimeout(r, 2000));
return {
imageUrl: style.sampleResult || style.thumbnail || "/assets/styles/jack_o_lantern_sensei.webp",
providerUsed: "demo_mock",
modelUsed: "demo-simulator",
durationMs: Date.now() - startTime,
fallbackTriggered: false,
warnings: ["Demo Mode active: Set GEMINI_API_KEY or OPENROUTER_API_KEY in .env.local to enable live AI generation."],
};
}
// 1. Try Primary Provider (OpenRouter)
if (isProviderAvailable("openrouter") && hasOpenRouterKey) {
try {
console.log(`[AI Orchestrator] Attempting Primary (OpenRouter: ${config.ai.backup.model})...`);
const result = await generateWithOpenRouter({
model: config.ai.backup.model,
prompt: style.prompt,
negativePrompt: style.negativePrompt,
imageBase64,
mimeType,
timeoutMs: config.ai.backup.timeoutMs,
});
resetProvider("openrouter");
return {
imageUrl: result.imageUrl,
providerUsed: "openrouter",
modelUsed: result.model,
durationMs: Date.now() - startTime,
fallbackTriggered: false,
};
} catch (primaryError: any) {
if (isContentSafetyError(primaryError)) {
console.warn(`[AI Orchestrator] Primary provider (OpenRouter) rejected image for safety:`, primaryError.message);
throw new ContentSafetyError(primaryError.message);
}
console.warn(`[AI Orchestrator] Primary provider (OpenRouter) failed:`, primaryError.message);
warnings.push(`Primary (OpenRouter) failed: ${primaryError.message}`);
}
} else {
warnings.push("Primary provider key (OpenRouter) missing or in cooldown. Skipping to fallback.");
}
// 2. Try Fallback Provider (Google Gemini)
if (isProviderAvailable("gemini") && hasGeminiKey) {
try {
console.log(`[AI Orchestrator] Attempting Fallback (Gemini: ${config.ai.primary.model})...`);
const result = await generateWithGemini({
model: config.ai.primary.model,
prompt: style.prompt,
negativePrompt: style.negativePrompt,
imageBase64,
mimeType,
timeoutMs: config.ai.primary.timeoutMs,
enableSfwCheck: config.ai.enableSfwCheck,
});
resetProvider("gemini");
return {
imageUrl: result.imageUrl,
providerUsed: "gemini",
modelUsed: result.model,
durationMs: Date.now() - startTime,
fallbackTriggered: true,
warnings,
};
} catch (fallbackError: any) {
if (isContentSafetyError(fallbackError)) {
console.warn(`[AI Orchestrator] Fallback provider (Gemini) rejected image for safety:`, fallbackError.message);
throw new ContentSafetyError(fallbackError.message);
}
console.error(`[AI Orchestrator] Fallback provider (Gemini) failed:`, fallbackError.message);
warnings.push(`Fallback (Gemini) failed: ${fallbackError.message}`);
}
}
// If both providers failed or were unavailable, throw descriptive error
throw new Error(
`AI Generation failed on all available providers. Details: ${warnings.join(" | ")}`
);
}