2026-10-28 01:20:01 +08:00
|
|
|
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,
|
2026-10-28 10:44:28 +08:00
|
|
|
allowUpload: false,
|
2026-10-28 01:20:01 +08:00
|
|
|
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 [];
|
|
|
|
|
}
|