Files
karate_halloween/lib/config.ts
T

254 lines
6.9 KiB
TypeScript

import fs from "fs";
import path from "path";
import { BoothConfig, BoothStyle } from "@/types/booth";
export const GENERATION_MODEL_MAP: Record<string, { primary: string; backup: string }> = {
"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 [];
}