feat: integrate Sensei Spook mascot in 4 stances, speech bubbles, and style updates
This commit is contained in:
commit
e04672db2a
104 files changed
+8262
No files matched your search
@@ -0,0 +1,125 @@
|
||||
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/sample_clay_pink.png",
|
||||
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 (Google Gemini)
|
||||
if (isProviderAvailable("gemini") && hasGeminiKey) {
|
||||
try {
|
||||
console.log(`[AI Orchestrator] Attempting Primary (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: false,
|
||||
};
|
||||
} catch (primaryError: any) {
|
||||
// Content safety rejections are caused by user input, NOT provider downtime
|
||||
if (isContentSafetyError(primaryError)) {
|
||||
console.warn(`[AI Orchestrator] Primary provider (Gemini) rejected image for safety:`, primaryError.message);
|
||||
throw new ContentSafetyError(primaryError.message);
|
||||
}
|
||||
|
||||
console.warn(`[AI Orchestrator] Primary provider (Gemini) failed:`, primaryError.message);
|
||||
warnings.push(`Primary failed: ${primaryError.message}`);
|
||||
}
|
||||
} else {
|
||||
warnings.push("Primary provider key missing. Skipping to backup.");
|
||||
}
|
||||
|
||||
// 2. Try Backup Provider (OpenRouter)
|
||||
if (isProviderAvailable("openrouter") && hasOpenRouterKey) {
|
||||
try {
|
||||
console.log(`[AI Orchestrator] Attempting Backup (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,
|
||||
});
|
||||
|
||||
return {
|
||||
imageUrl: result.imageUrl,
|
||||
providerUsed: "openrouter",
|
||||
modelUsed: result.model,
|
||||
durationMs: Date.now() - startTime,
|
||||
fallbackTriggered: true,
|
||||
warnings,
|
||||
};
|
||||
} catch (backupError: any) {
|
||||
if (isContentSafetyError(backupError)) {
|
||||
console.warn(`[AI Orchestrator] Backup provider (OpenRouter) rejected image for safety:`, backupError.message);
|
||||
throw new ContentSafetyError(backupError.message);
|
||||
}
|
||||
|
||||
console.error(`[AI Orchestrator] Backup provider (OpenRouter) failed:`, backupError.message);
|
||||
warnings.push(`Backup failed: ${backupError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If both providers failed or were unavailable, throw descriptive error
|
||||
throw new Error(
|
||||
`AI Generation failed on all available providers. Details: ${warnings.join(" | ")}`
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user