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
+253
@@ -0,0 +1,253 @@
|
||||
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 [];
|
||||
}
|
||||
Reference in new issue
Block a user