import fs from "fs"; import path from "path"; import { BoothConfig, BoothStyle } from "@/types/booth"; export const GENERATION_MODEL_MAP: Record = { "nanobanana 2": { primary: "gemini-3.1-flash-image", backup: "google/gemini-3.1-flash-image", }, "nanobanana": { primary: "gemini-2.5-flash-image", backup: "google/gemini-2.5-flash-image", }, "lite": { primary: "gemini-3.1-flash-lite-image", backup: "google/gemini-3.1-flash-lite-image", }, "nanobanana 2 lite": { primary: "gemini-3.1-flash-lite-image", backup: "google/gemini-3.1-flash-lite-image", }, "pro": { primary: "gemini-3-pro-image", backup: "google/gemini-3-pro-image", }, "nanobanana pro": { primary: "gemini-3-pro-image", backup: "google/gemini-3-pro-image", }, }; export function resolveGenerationModels(): { primary: string; backup: string } { const envModel = (process.env.GENERATION_MODEL || "").trim().toLowerCase(); if (envModel && GENERATION_MODEL_MAP[envModel]) { return GENERATION_MODEL_MAP[envModel]; } return GENERATION_MODEL_MAP["nanobanana 2"]; } const defaultConfig: BoothConfig = { appName: "Spooky Hand · 万圣空手", kioskMode: true, theme: { backgroundType: "color", backgroundColor: "#0b0714", primaryColor: "#ff6d00", accentColor: "#06b6d4", surfaceColor: "#170f29", textColor: "#ffffff", logoUrl: "/assets/brand/shanghai_karate_logo.png", logoWidth: 260, }, camera: { defaultFacingMode: "user", aspectRatio: "9:16", countdownSeconds: 3, allowUpload: true, maxUploadSizeMb: 10, }, features: { showQrCode: true, showDownloadButton: true, showPrintButton: false, showRetakeButton: true, showInstagramShare: false, }, limits: { maxGenerationsPerDevice: 0, cooldownMinutes: 60, }, ai: { primary: { provider: "gemini", model: "gemini-3.1-flash-image", timeoutMs: 28000, }, backup: { provider: "openrouter", model: "google/gemini-3.1-flash-image", timeoutMs: 30000, }, cooldownDurationMinutes: 10, enableSfwCheck: true, }, kiosk: { enableAutoReset: true, idleTimeoutSeconds: 90, }, labels: { welcomeBadge: { en: "SHANGHAI KARATE CLUB • EST. 2008", zh: "上海健志空手道馆 · 始于2008", }, welcomeTitle: { en: "SPOOKY HAND 🥋🎃", zh: "万圣“空手”道馆 🥋🎃", }, welcomeSubtitle: { en: "Channel Your Spooky Ki & Martial Pose!", zh: "凝聚万圣气合,定格武道英姿!", }, welcomeDesc: { en: "Strike your fiercest martial arts pose! Turn your Halloween costume or karate gi into an epic Shanghai legend.", zh: "摆出你最帅气的武道姿势!无论身穿万圣盛装还是空手道服,一键变身魔都万圣武道传奇!", }, welcomeCta: { en: "Enter The Haunted Dojo! ⚡", zh: "开启万圣武道之旅!⚡", }, cameraInstruction: { en: "Strike a punch, block, or karate victory pose!", zh: "摆出冲拳、格挡或胜利姿态,展现你的气魄!", }, cameraSnap: { en: "Snap Martial Shot", zh: "拍摄武道照", }, cameraUpload: { en: "Upload Photo", zh: "上传照片", }, confirmTitle: { en: "Review Your Martial Shot", zh: "确认你的英姿", }, retakeButton: { en: "Retake", zh: "重拍", }, confirmButton: { en: "Looks Epic! Choose Style", zh: "超帅气!选择风格", }, styleSelectionTitle: { en: "Select Your Halloween Kata", zh: "选择你的万圣奥义风格", }, styleSelectionSubtitle: { en: "Choose your legendary martial transformation", zh: "挑选专属的万圣节武道变身效果", }, confirmStyleButton: { en: "Cast Martial Magic ⚡", zh: "施展武道魔法 ⚡", }, backButton: { en: "Back", zh: "返回", }, processingTitle: { en: "Gathering Martial Ki & Spooky Spirits...", zh: "正在凝聚万圣之气与道馆灵力...", }, processingSubtitle: { en: "Summoning Huangpu mist, pumpkin embers & black belt magic...", zh: "融合黄浦江晨雾、南瓜火光与黑带武道魔法...", }, resultTitle: { en: "Your Spooky Martial Portrait! 🥋🎃", zh: "你的万圣武道神采!🥋🎃", }, scanQrLabel: { en: "Scan to save to your phone", zh: "扫码保存至手机相册", }, downloadLabel: { en: "Save Portrait", zh: "保存高清照", }, wechatSaveTip: { en: "In WeChat: Long press photo above to save directly!", zh: "微信提示:长按上方图片即可直接保存到相册!", }, printLabel: { en: "Print Photo", zh: "打印照片", }, startOverLabel: { en: "Train Again (Start Over)", zh: "再拍一张", }, instagramShareLabel: { en: "Share Photo", zh: "分享照片", }, }, }; export function getBoothConfig(): BoothConfig { try { const configPath = path.join(process.cwd(), "config", "booth.config.json"); const resolvedModels = resolveGenerationModels(); if (fs.existsSync(configPath)) { const fileContent = fs.readFileSync(configPath, "utf-8"); const parsed = JSON.parse(fileContent); const primaryModel = process.env.GENERATION_MODEL || parsed.ai?.primary?.model === "gemini-2.5-flash" || !parsed.ai?.primary?.model ? resolvedModels.primary : parsed.ai.primary.model; const backupModel = process.env.GENERATION_MODEL || parsed.ai?.backup?.model === "google/gemini-2.5-flash" || !parsed.ai?.backup?.model ? resolvedModels.backup : parsed.ai.backup.model; return { ...defaultConfig, ...parsed, ai: { ...defaultConfig.ai, ...(parsed.ai || {}), primary: { ...defaultConfig.ai.primary, ...(parsed.ai?.primary || {}), model: primaryModel, }, backup: { ...defaultConfig.ai.backup, ...(parsed.ai?.backup || {}), model: backupModel, }, }, labels: { ...defaultConfig.labels, ...(parsed.labels || {}), }, }; } } catch (error) { console.warn("Could not load booth.config.json, using fallback configuration:", error); } return defaultConfig; } export function getBoothStyles(): BoothStyle[] { try { const stylesPath = path.join(process.cwd(), "config", "styles.json"); if (fs.existsSync(stylesPath)) { const fileContent = fs.readFileSync(stylesPath, "utf-8"); return JSON.parse(fileContent); } } catch (error) { console.warn("Could not load styles.json, returning empty list:", error); } return []; }