diff --git a/app/api/images/[filename]/route.ts b/app/api/images/[filename]/route.ts index b589346..e52e7f4 100644 --- a/app/api/images/[filename]/route.ts +++ b/app/api/images/[filename]/route.ts @@ -41,7 +41,7 @@ export async function GET( "Content-Type": contentType, "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "default-src 'none'; sandbox", - "Content-Disposition": `inline; filename="${filename}"`, + "Content-Disposition": `${req.nextUrl.searchParams.get("download") === "1" ? "attachment" : "inline"}; filename="${filename}"`, "Cache-Control": "public, max-age=31536000, immutable", }, }); diff --git a/app/globals.css b/app/globals.css index aeb7a0d..1435974 100644 --- a/app/globals.css +++ b/app/globals.css @@ -171,6 +171,24 @@ body { border-width: 0 !important; box-shadow: none !important; } + + /* Tailwind's 12px text-xs was too small for the touch-first booth screens. */ + .kiosk-viewport .text-xs { + font-size: 0.875rem; + line-height: 1.25rem; + } + + .kiosk-viewport [class~="text-[10px]"], + .kiosk-viewport [class~="text-[11px]"], + .kiosk-viewport [class~="text-[12px]"] { + font-size: 0.875rem; + line-height: 1.25rem; + } + + .kiosk-viewport .kiosk-footer-label { + font-size: 0.625rem !important; + line-height: 0.875rem !important; + } } /* Fullscreen Mode URL parameter reset (?mode=fullscreen) */ diff --git a/app/layout.tsx b/app/layout.tsx index e3863f4..a22d593 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,15 +6,14 @@ export const metadata: Metadata = { description: "上海健志空手道馆万圣节专属照相馆 (Shanghai Karate Club Halloween Photobooth) - 凝聚万圣气合,定格武道英姿!", icons: { - icon: "/assets/brand/shanghai_karate_logo.png", + icon: "/assets/brand/shanghai_karate_logo.webp", }, }; export const viewport: Viewport = { width: "device-width", initialScale: 1, - maximumScale: 1, - userScalable: false, + userScalable: true, viewportFit: "cover", themeColor: "#0b0714", }; diff --git a/app/page.tsx b/app/page.tsx index 33deee5..cdcc03b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -9,6 +9,7 @@ import { ConfirmScreen } from "@/components/screens/ConfirmScreen"; import { StyleScreen } from "@/components/screens/StyleScreen"; import { LoadingScreen } from "@/components/screens/LoadingScreen"; import { ResultScreen } from "@/components/screens/ResultScreen"; +import { FinishedScreen } from "@/components/screens/FinishedScreen"; import { RateLimitedScreen } from "@/components/screens/RateLimitedScreen"; import { BoothConfig, BoothStyle, ScreenState } from "@/types/booth"; import { I18nProvider, useI18n } from "@/lib/i18n"; @@ -50,9 +51,11 @@ function PhotoBoothApp() { const params = new URLSearchParams(window.location.search); const previewScreen = params.get("preview") as ScreenState | null; if (previewScreen === "result") { - const previewImg = params.get("image") || "/assets/styles/jack_o_lantern_sensei.jpg"; + const previewImg = params.get("image") || "/assets/styles/jack_o_lantern_sensei.webp"; setResultImage(previewImg); setScreen("result"); + } else if (previewScreen === "finished") { + setScreen("finished"); } } }) @@ -63,7 +66,7 @@ function PhotoBoothApp() { if (idleTimerRef.current) { clearTimeout(idleTimerRef.current); } - if (!config?.kiosk?.enableAutoReset || screen === "standby") { + if (!config?.kiosk?.enableAutoReset || screen === "standby" || screen === "finished") { return; } const timeoutMs = (config.kiosk.idleTimeoutSeconds || 90) * 1000; @@ -178,6 +181,15 @@ function PhotoBoothApp() { setScreen("standby"); }; + const handleFinish = () => { + setCapturedPhoto(null); + setSelectedStyle(null); + setResultImage(null); + setErrorMessage(null); + setSafetyWarning(null); + setScreen("finished"); + }; + if (!config) { return (
@@ -246,10 +258,12 @@ function PhotoBoothApp() { config={config} resultImageUrl={resultImage} providerInfo={providerInfo} - onStartOver={handleStartOver} + onFinish={handleFinish} /> )} + {screen === "finished" && } + {screen === "rate_limited" && ( = ({ href="https://www.shanghaikarate.com" target="_blank" rel="noopener noreferrer" - className="text-[8.5px] sm:text-[9.5px] text-white/50 hover:text-amber-300 transition-colors whitespace-nowrap overflow-hidden text-ellipsis tracking-wide" + className="kiosk-footer-label text-[10px] sm:text-[11px] text-white/70 hover:text-amber-300 transition-colors whitespace-nowrap overflow-hidden text-ellipsis tracking-wide" title="Shanghai Karate Club (shanghaikarate.com)" > 上海健志空手道馆 · Shanghai Karate Club · Est. 2008 diff --git a/components/MascotGuide.tsx b/components/MascotGuide.tsx index 56dc3e0..e76f1b3 100644 --- a/components/MascotGuide.tsx +++ b/components/MascotGuide.tsx @@ -19,10 +19,10 @@ interface MascotGuideProps { } const POSE_IMAGES: Record = { - bow: "/assets/theme/mascot_bow.png", - punch: "/assets/theme/mascot_punch.png", - kick: "/assets/theme/mascot_kick.png", - zen: "/assets/theme/mascot_zen.png", + bow: "/assets/theme/mascot_bow.webp", + punch: "/assets/theme/mascot_punch.webp", + kick: "/assets/theme/mascot_kick.webp", + zen: "/assets/theme/mascot_zen.webp", }; export const MascotGuide: React.FC = ({ @@ -94,7 +94,7 @@ export const MascotGuide: React.FC = ({ )}

diff --git a/components/SpookyAtmosphere.tsx b/components/SpookyAtmosphere.tsx index f7d3a3e..c775033 100644 --- a/components/SpookyAtmosphere.tsx +++ b/components/SpookyAtmosphere.tsx @@ -18,7 +18,7 @@ export const SpookyAtmosphere: React.FC = ({ {/* 1. Shanghai Dojo Courtyard Night Background Artwork */} {/* eslint-disable-next-line @next/next/no-img-element */} Shanghai Dojo Night Background diff --git a/components/screens/ConfirmScreen.tsx b/components/screens/ConfirmScreen.tsx index 61c5db6..807f2fd 100644 --- a/components/screens/ConfirmScreen.tsx +++ b/components/screens/ConfirmScreen.tsx @@ -27,7 +27,7 @@ export const ConfirmScreen: React.FC = ({

+ + {onFinish && ( + + )}
diff --git a/components/screens/StandbyScreen.tsx b/components/screens/StandbyScreen.tsx index acbe04d..f0a73e9 100644 --- a/components/screens/StandbyScreen.tsx +++ b/components/screens/StandbyScreen.tsx @@ -29,7 +29,7 @@ export const StandbyScreen: React.FC = ({

{t(labels.welcomeTitle)}

-

+

{t(labels.welcomeSubtitle)}

@@ -51,7 +51,7 @@ export const StandbyScreen: React.FC = ({
diff --git a/components/screens/StyleScreen.tsx b/components/screens/StyleScreen.tsx index f4f57d1..bc44649 100644 --- a/components/screens/StyleScreen.tsx +++ b/components/screens/StyleScreen.tsx @@ -37,7 +37,7 @@ export const StyleScreen: React.FC = ({
@@ -88,7 +88,7 @@ export const StyleScreen: React.FC = ({ {/* Title & Tag */}
- + {styleName}
diff --git a/config/booth.config.json b/config/booth.config.json index 8dcd23d..b1ad716 100644 --- a/config/booth.config.json +++ b/config/booth.config.json @@ -5,12 +5,12 @@ "backgroundType": "image", "backgroundColor": "#0b0714", "backgroundVideoUrl": "", - "backgroundImageUrl": "/assets/theme/shanghai_dojo_night_bg.jpg", + "backgroundImageUrl": "/assets/theme/shanghai_dojo_night_bg.webp", "primaryColor": "#ff6d00", "accentColor": "#06b6d4", "surfaceColor": "#170f29", "textColor": "#ffffff", - "logoUrl": "/assets/brand/shanghai_karate_logo.png", + "logoUrl": "/assets/brand/shanghai_karate_logo.webp", "logoWidth": 310 }, "camera": { @@ -127,20 +127,20 @@ "zh": "扫码保存至手机相册" }, "downloadLabel": { - "en": "Save Portrait", - "zh": "保存高清照" + "en": "Save", + "zh": "保存" }, "wechatSaveTip": { - "en": "In WeChat: Long press photo above to save directly!", - "zh": "微信提示:长按上方图片即可直接保存到相册!" + "en": "In WeChat, long-press the photo to save it.", + "zh": "微信中长按照片即可保存。" }, "printLabel": { "en": "Print Photo", "zh": "打印照片" }, "startOverLabel": { - "en": "Train Again (Start Over)", - "zh": "再拍一张" + "en": "Finish", + "zh": "完成" }, "instagramShareLabel": { "en": "Share Photo", diff --git a/config/styles.json b/config/styles.json index e052187..44d92ef 100644 --- a/config/styles.json +++ b/config/styles.json @@ -9,8 +9,8 @@ "en": "Extrapolates your posture into an airborne crane stance or soaring crescent kick with a flowing crimson cape.", "zh": "将你的姿态外推升华为兼具美感与爆发力的仙鹤独立型或腾空飞踢,天鹅绒斗篷随风狂舞,夜眺黄浦江画舫。" }, - "thumbnail": "/assets/styles/bund_vampire_master.jpg", - "sampleResult": "/assets/styles/bund_vampire_master.jpg", + "thumbnail": "/assets/styles/bund_vampire_master.webp", + "sampleResult": "/assets/styles/bund_vampire_master.webp", "prompt": "CRITICAL STRICT IDENTITY & DYNAMIC POSE EXTRAPOLATION (ABSOLUTE HIGHEST PRIORITY): 1. DYNAMIC KARATE EXTRAPOLATION (AVOID STATIC / IDENTICAL POSES): Deeply analyze the person's posture, weight distribution, arm reach, and inclination in the photo, then BOLDLY EXTRAPOLATE and exaggerate it into an athletic, cinematographic, highly dynamic karate action move matching this style's soaring aerial theme: - If an arm or leg is lifted/stepping -> extrapolate into a soaring aerial karate crane stance (Tsuruashi-dachi) or high flying crescent kick (Mikazuki-geri), one foot perched lightly with martial elegance. - If reaching forward or punching -> extrapolate into a deeply lunged forward strike with their crimson vampire cape billowing dramatically in the river wind. - If arms are bent or neutral -> extrapolate into an intense, crouching cat stance (Nekoashi-dachi) with open knife-hands (Shuto) held in razor-sharp vampire guard. Make the pose feel ALIVE, athletic, and full of cinematic kinetic momentum—NEVER stiff or repetitive. 2. FACIAL IDENTITY: Strictly preserve their exact recognizable facial features, eye shape, smile, nose, chin, and hair. Friends and parents MUST instantly recognize this exact person. STYLING: Translate them into a high-end 3D animated feature film character (Pixar/Hotel Transylvania/DreamWorks). COSTUME: Preserve their costume or gi, adding an elegant flowing crimson velvet vampire cape over their shoulders and an Obi belt around the waist. Genuine friendly smile with cute mini-fangs. Background: Moonlit wooden rooftop dojo overlooking the misty Huangpu River with traditional lantern-lit vintage Shanghai boats beneath an enormous golden full moon and cute cartoon bats fluttering overhead. Warm whimsical cartoon lighting, zero weapons, zero headbands, zero flags, family-friendly.", "negativePrompt": "boring static standing, flat stiff posture, repetitive generic pose, awkward stiffness, unrecognizable person, different face, altered facial features, generic cartoon face, changed eye shape, changed skin tone, changed nose, bad likeness, photorealistic flesh, creepy horror, scary, bloody, blood, gore, violent, deformed, extra fingers, weapons, swords, katana, japanese flag, headband, text watermark" }, @@ -24,8 +24,8 @@ "en": "Extrapolates your posture into a thunderous forward punch or horse stance radiating swirling pumpkin flame orbs.", "zh": "将你的姿态推演为极具爆发力的骑马步轰拳或旋风格挡,双拳汇聚万圣流火气合波,背景为圆月东方明珠。" }, - "thumbnail": "/assets/styles/jack_o_lantern_sensei.jpg", - "sampleResult": "/assets/styles/jack_o_lantern_sensei.jpg", + "thumbnail": "/assets/styles/jack_o_lantern_sensei.webp", + "sampleResult": "/assets/styles/jack_o_lantern_sensei.webp", "prompt": "CRITICAL STRICT IDENTITY & DYNAMIC POSE EXTRAPOLATION (ABSOLUTE HIGHEST PRIORITY): 1. DYNAMIC KARATE EXTRAPOLATION (AVOID STATIC / IDENTICAL POSES): Extrapolate the user's posture into an explosive, powerful, deeply-grounded karate technique: - If any forward lean or arm push -> extrapolate into an explosive lunging front-punch (Oi-tsuki) with low front stance (Zenkutsu-dachi), driving forward with earth-shattering force. - If hands near waist or body -> extrapolate into a wide, powerful Horse Stance (Kiba-dachi) with dual chambered fists firing outward. - If standing casually or neutral -> extrapolate into an energetic diagonal guard with shoulders turned, knees bent in athletic anticipation, ready to unleash a flame blast. Bursting from their hands are dynamic swirling ribbons of glowing pumpkin embers and magical flame ki shields. The pose must have intense kinetic energy, sharp angles, and dramatic martial action lines. 2. FACIAL IDENTITY: Strictly preserve their exact recognizable face, eye shape, eyelid fold, iris color, nose, smile, hair texture, and glasses. STYLING: High-end 3D animated character (Pixar/DreamWorks aesthetic). COSTUME: Preserve outfit, adding a crisp karate martial belt (Obi). Beside them is a cute animated smiling jack-o'-lantern mimicking their powerful stance. Background: Authentic wooden courtyard dojo with warm lanterns overlooking the glowing Oriental Pearl Tower in Shanghai under a purple twilight harvest moon. Family-friendly, zero weapons, zero headbands.", "negativePrompt": "boring static standing, stiff arms, identical repetitive pose, frozen mannequin, unrecognizable person, different face, altered facial features, generic cartoon face, changed eye shape, changed skin tone, changed nose, bad likeness, photorealistic composite, flat image, distorted face, monster, scary, blood, gore, weapons, sword, katana, imperial flag, headband, text watermark" }, @@ -39,8 +39,8 @@ "en": "Extrapolates your posture into a high-speed reverse punch or sweeping kick trailed by neon-cyan & purple calligraphic ink.", "zh": "将你的身形化作极速反弹冲拳或旋风扫腿,青紫电光气合波与水墨书法笔锋撕裂夜空,外滩夜色璀璨。" }, - "thumbnail": "/assets/styles/ink_wash_cyber_shinobi.jpg", - "sampleResult": "/assets/styles/ink_wash_cyber_shinobi.jpg", + "thumbnail": "/assets/styles/ink_wash_cyber_shinobi.webp", + "sampleResult": "/assets/styles/ink_wash_cyber_shinobi.webp", "prompt": "CRITICAL STRICT IDENTITY & DYNAMIC POSE EXTRAPOLATION (ABSOLUTE HIGHEST PRIORITY): 1. DYNAMIC KARATE EXTRAPOLATION (AVOID STATIC / IDENTICAL POSES): Extrapolate the user's body position into a high-velocity, cinematic anime/comic action shot with exaggerated perspective (Spider-Verse kinetic camera angle): - If arms or torso show any lateral movement -> extrapolate into a devastating reverse-punch (Gyaku-tsuki) or back-fist strike (Uraken) with dynamic foreshortening where the front fist appears large and impactful. - If any leg or foot movement -> extrapolate into a mid-air sweeping roundhouse kick (Mawashi-geri) or side kick (Yoko-geri). - If casual standing -> extrapolate into a tense, coiled ready-to-strike crouching guard (Kumite Dachi) with hair and belt fluttering in the shockwave. Dynamic swirling calligraphy ink splashes and electric cyan/purple neon chi lines slice through the air along their movement path. 2. FACIAL IDENTITY: Faithfully reproduce their recognizable face, eyes, smile, and hairstyle. STYLING: High-end 3D animated feature film character in dynamic Spider-Verse meets Pixar style. COSTUME: Maintain their clothing/costume, adding a sleek black karate martial belt (Obi) at the waist. Background: The historic illuminated architecture of the Shanghai Bund along the misty Huangpu River under a dramatic moonlit Halloween sky with playful origami paper bats. High energy, heroic, zero weapons, zero headbands.", "negativePrompt": "boring static standing, flat stiffness, identical repetitive pose, wooden dummy, unrecognizable person, different face, altered facial features, generic cartoon face, changed eye shape, changed skin tone, changed nose, bad likeness, distorted face, dull colors, weapons, katana, swords, japanese imperial flag, rising sun, headband, hachimaki, scary, bloody, ugly, deformed limbs, watermark" }, @@ -54,8 +54,8 @@ "en": "Extrapolates your posture into an expressive, joyful Kata form with chubby mini ghosts comically exaggerating your move.", "zh": "将你的动作生动外推为欢快神气的经典型演武,胖嘟嘟的小幽灵们夸张地模仿你的招式在榻榻米上翻滚喝彩。" }, - "thumbnail": "/assets/styles/ghostly_kiai_dojo.jpg", - "sampleResult": "/assets/styles/ghostly_kiai_dojo.jpg", + "thumbnail": "/assets/styles/ghostly_kiai_dojo.webp", + "sampleResult": "/assets/styles/ghostly_kiai_dojo.webp", "prompt": "CRITICAL STRICT IDENTITY & DYNAMIC POSE EXTRAPOLATION (ABSOLUTE HIGHEST PRIORITY): 1. DYNAMIC KARATE EXTRAPOLATION (AVOID STATIC / IDENTICAL POSES): Extrapolate the person's pose into an expressive, theatrical, and joyful karate Kata demonstration: - If hands are up or cheering -> extrapolate into a victorious dual-fist skyward Kiai finish with high chin and proud champion posture. - If hands are forward or pointing -> extrapolate into a theatrical knife-hand strike (Shuto-uchi) or double palm thrust (Morote-teisho). - If casual standing -> extrapolate into an exaggerated, theatrical Kata opening salute (Rei / chambered fists) with feet planted wide and a spirited martial expression. GHOST BUDDIES: 3 to 4 chubby cartoon baby ghosts enthusiastically mimic this exaggerated move, with one ghost comically tumbling over trying to copy the stance. Floating candies and glowing jack-o'-lanterns drift in mid-air. 2. FACIAL IDENTITY: Strictly preserve their exact recognizable face, eye shape, smile, teeth, and hair. STYLING: Warm, lovable 3D animated character (Pixar/Hotel Transylvania aesthetic). COSTUME: Tie a martial arts karate belt over their outfit. Background: Inside a cozy traditional wooden Shanghai dojo with glowing carved pumpkin lanterns, paper screens, and a night window showing the Shanghai skyline. Heartwarming, funny, high quality 3D cartoon render, zero weapons, zero flags.", "negativePrompt": "boring static standing, dull stiffness, identical repetitive pose, frozen expression, unrecognizable person, different face, altered facial features, generic cartoon face, changed eye shape, changed skin tone, changed nose, bad likeness, terrifying, scary ghost, evil demon, blood, gore, horror, distorted face, changed identity, weapons, swords, katana, rising sun, headband, blurry, deformed limbs, watermark" } diff --git a/lib/ai/orchestrator.ts b/lib/ai/orchestrator.ts index 096b33d..ae1a17c 100644 --- a/lib/ai/orchestrator.ts +++ b/lib/ai/orchestrator.ts @@ -41,7 +41,7 @@ export async function orchestrateGeneration( // Simulate generation delay (2 seconds) await new Promise((r) => setTimeout(r, 2000)); return { - imageUrl: style.sampleResult || style.thumbnail || "/assets/styles/sample_clay_pink.png", + imageUrl: style.sampleResult || style.thumbnail || "/assets/styles/jack_o_lantern_sensei.webp", providerUsed: "demo_mock", modelUsed: "demo-simulator", durationMs: Date.now() - startTime, diff --git a/lib/config.ts b/lib/config.ts index 9c6dbd6..ddb1c66 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -47,7 +47,7 @@ const defaultConfig: BoothConfig = { accentColor: "#06b6d4", surfaceColor: "#170f29", textColor: "#ffffff", - logoUrl: "/assets/brand/shanghai_karate_logo.png", + logoUrl: "/assets/brand/shanghai_karate_logo.webp", logoWidth: 260, }, camera: { @@ -164,20 +164,20 @@ const defaultConfig: BoothConfig = { zh: "扫码保存至手机相册", }, downloadLabel: { - en: "Save Portrait", - zh: "保存高清照", + en: "Save", + zh: "保存", }, wechatSaveTip: { - en: "In WeChat: Long press photo above to save directly!", - zh: "微信提示:长按上方图片即可直接保存到相册!", + en: "In WeChat, long-press the photo to save it.", + zh: "微信中长按照片即可保存。", }, printLabel: { en: "Print Photo", zh: "打印照片", }, startOverLabel: { - en: "Train Again (Start Over)", - zh: "再拍一张", + en: "Finish", + zh: "完成", }, instagramShareLabel: { en: "Share Photo", diff --git a/lib/storage.ts b/lib/storage.ts index fade29f..4ac8db6 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -1,5 +1,6 @@ import fs from "fs"; import path from "path"; +import sharp from "sharp"; import { validateAndSanitizeImage } from "@/lib/security"; export interface GenerationRecord { @@ -202,26 +203,34 @@ export async function persistImageFile( return { imageUrl: imageInput, filename: "" }; } - // If it's a data URL or raw base64 - if (imageInput.startsWith("data:image/") || imageInput.startsWith("data:application/")) { - const validation = validateAndSanitizeImage(imageInput, 15 * 1024 * 1024); - if (validation.valid && validation.extension) { - const ext = validation.extension; // Strictly "jpg" | "png" | "webp" - const filename = `${safeId}.${ext}`; - const filePath = path.join(IMAGES_DIR, filename); + const persistJpeg = async (inputBuffer: Buffer) => { + const sourcePath = path.join(IMAGES_DIR, `${safeId}.source`); + const filename = `${safeId}.jpg`; + const jpegPath = path.join(IMAGES_DIR, filename); + fs.writeFileSync(sourcePath, inputBuffer); - const base64Data = validation.sanitizedBase64!.split(",")[1]; - fs.writeFileSync(filePath, Buffer.from(base64Data, "base64")); - - // Automatically apply school branding: logo (top-left), WeChat QR (bottom-right), and footer text + try { + // Apply the existing branding before encoding the final downloadable image. try { const { applyWatermarkToFile } = await import("@/lib/watermark"); - await applyWatermarkToFile(filePath); + await applyWatermarkToFile(sourcePath); } catch (wmErr) { console.warn("[Storage] Watermark application skipped:", wmErr); } + await sharp(sourcePath).jpeg({ quality: 92, mozjpeg: true }).toFile(jpegPath); return { imageUrl: `/api/images/${filename}`, filename }; + } finally { + if (fs.existsSync(sourcePath)) fs.unlinkSync(sourcePath); + } + }; + + // If it's a data URL or raw base64 + if (imageInput.startsWith("data:image/") || imageInput.startsWith("data:application/")) { + const validation = validateAndSanitizeImage(imageInput, 15 * 1024 * 1024); + if (validation.valid) { + const base64Data = validation.sanitizedBase64!.split(",")[1]; + return persistJpeg(Buffer.from(base64Data, "base64")); } else { console.warn(`[Storage Security Alert] Refused to persist invalid image: ${validation.error}`); return { imageUrl: imageInput, filename: "" }; @@ -258,20 +267,8 @@ export async function persistImageFile( 15 * 1024 * 1024 ); - if (validation.valid && validation.extension) { - const filename = `${safeId}.${validation.extension}`; - const filePath = path.join(IMAGES_DIR, filename); - fs.writeFileSync(filePath, buffer); - - // Automatically apply school branding: logo (top-left), WeChat QR (bottom-right), and footer text - try { - const { applyWatermarkToFile } = await import("@/lib/watermark"); - await applyWatermarkToFile(filePath); - } catch (wmErr) { - console.warn("[Storage] Watermark application skipped:", wmErr); - } - - return { imageUrl: `/api/images/${filename}`, filename }; + if (validation.valid) { + return persistJpeg(buffer); } else { console.warn(`[Storage Security Alert] Remote file was not a valid image: ${validation.error}`); } diff --git a/lib/watermark.py b/lib/watermark.py index 5dfbd2f..84e26dd 100644 --- a/lib/watermark.py +++ b/lib/watermark.py @@ -17,7 +17,7 @@ def watermark_portrait(image_path_or_buffer): bw, bh = base.size # 1. School Logo (Top-Left) - logo_path = os.path.join(process_cwd(), "public", "assets", "brand", "shanghai_karate_logo.png") + logo_path = os.path.join(process_cwd(), "public", "assets", "brand", "shanghai_karate_logo.webp") if os.path.exists(logo_path): logo = Image.open(logo_path).convert("RGBA") target_logo_w = int(bw * 0.28) diff --git a/lib/watermark.ts b/lib/watermark.ts index a462bee..6ecc12d 100644 --- a/lib/watermark.ts +++ b/lib/watermark.ts @@ -16,7 +16,7 @@ export async function applyWatermarkToFile(filePath: string): Promise { try { const baseDir = process.cwd(); - const logoPath = path.join(baseDir, "public", "assets", "brand", "shanghai_karate_logo.png"); + const logoPath = path.join(baseDir, "public", "assets", "brand", "shanghai_karate_logo.webp"); const qrPath = path.join(baseDir, "public", "assets", "brand", "dojo_qr.png"); const baseImage = sharp(filePath); diff --git a/public/assets/brand/shanghai_karate_logo.png b/public/assets/brand/shanghai_karate_logo.png deleted file mode 100644 index ed68eae..0000000 Binary files a/public/assets/brand/shanghai_karate_logo.png and /dev/null differ diff --git a/public/assets/brand/shanghai_karate_logo.webp b/public/assets/brand/shanghai_karate_logo.webp new file mode 100644 index 0000000..f769485 Binary files /dev/null and b/public/assets/brand/shanghai_karate_logo.webp differ diff --git a/public/assets/styles/bund_vampire_master.jpg b/public/assets/styles/bund_vampire_master.jpg deleted file mode 100644 index 1b4ae28..0000000 Binary files a/public/assets/styles/bund_vampire_master.jpg and /dev/null differ diff --git a/public/assets/styles/bund_vampire_master.webp b/public/assets/styles/bund_vampire_master.webp new file mode 100644 index 0000000..e18975c Binary files /dev/null and b/public/assets/styles/bund_vampire_master.webp differ diff --git a/public/assets/styles/ghostly_kiai_dojo.jpg b/public/assets/styles/ghostly_kiai_dojo.jpg deleted file mode 100644 index a8c14af..0000000 Binary files a/public/assets/styles/ghostly_kiai_dojo.jpg and /dev/null differ diff --git a/public/assets/styles/ghostly_kiai_dojo.webp b/public/assets/styles/ghostly_kiai_dojo.webp new file mode 100644 index 0000000..5c98da9 Binary files /dev/null and b/public/assets/styles/ghostly_kiai_dojo.webp differ diff --git a/public/assets/styles/ink_wash_cyber_shinobi.jpg b/public/assets/styles/ink_wash_cyber_shinobi.jpg deleted file mode 100644 index 50af302..0000000 Binary files a/public/assets/styles/ink_wash_cyber_shinobi.jpg and /dev/null differ diff --git a/public/assets/styles/ink_wash_cyber_shinobi.webp b/public/assets/styles/ink_wash_cyber_shinobi.webp new file mode 100644 index 0000000..667cad6 Binary files /dev/null and b/public/assets/styles/ink_wash_cyber_shinobi.webp differ diff --git a/public/assets/styles/jack_o_lantern_sensei.jpg b/public/assets/styles/jack_o_lantern_sensei.jpg deleted file mode 100644 index 339598f..0000000 Binary files a/public/assets/styles/jack_o_lantern_sensei.jpg and /dev/null differ diff --git a/public/assets/styles/jack_o_lantern_sensei.webp b/public/assets/styles/jack_o_lantern_sensei.webp new file mode 100644 index 0000000..e9ad836 Binary files /dev/null and b/public/assets/styles/jack_o_lantern_sensei.webp differ diff --git a/public/assets/theme/mascot_bow.png b/public/assets/theme/mascot_bow.png deleted file mode 100644 index 79eae45..0000000 Binary files a/public/assets/theme/mascot_bow.png and /dev/null differ diff --git a/public/assets/theme/mascot_bow.webp b/public/assets/theme/mascot_bow.webp new file mode 100644 index 0000000..9e7a1bb Binary files /dev/null and b/public/assets/theme/mascot_bow.webp differ diff --git a/public/assets/theme/mascot_kick.png b/public/assets/theme/mascot_kick.png deleted file mode 100644 index 28f32cc..0000000 Binary files a/public/assets/theme/mascot_kick.png and /dev/null differ diff --git a/public/assets/theme/mascot_kick.webp b/public/assets/theme/mascot_kick.webp new file mode 100644 index 0000000..c30409b Binary files /dev/null and b/public/assets/theme/mascot_kick.webp differ diff --git a/public/assets/theme/mascot_punch.png b/public/assets/theme/mascot_punch.png deleted file mode 100644 index 8d7e03d..0000000 Binary files a/public/assets/theme/mascot_punch.png and /dev/null differ diff --git a/public/assets/theme/mascot_punch.webp b/public/assets/theme/mascot_punch.webp new file mode 100644 index 0000000..1268e1f Binary files /dev/null and b/public/assets/theme/mascot_punch.webp differ diff --git a/public/assets/theme/mascot_zen.png b/public/assets/theme/mascot_zen.png deleted file mode 100644 index d74668e..0000000 Binary files a/public/assets/theme/mascot_zen.png and /dev/null differ diff --git a/public/assets/theme/mascot_zen.webp b/public/assets/theme/mascot_zen.webp new file mode 100644 index 0000000..28c3cd4 Binary files /dev/null and b/public/assets/theme/mascot_zen.webp differ diff --git a/public/assets/theme/shanghai_dojo_night_bg.jpg b/public/assets/theme/shanghai_dojo_night_bg.jpg deleted file mode 100644 index c3d41e6..0000000 Binary files a/public/assets/theme/shanghai_dojo_night_bg.jpg and /dev/null differ diff --git a/public/assets/theme/shanghai_dojo_night_bg.webp b/public/assets/theme/shanghai_dojo_night_bg.webp new file mode 100644 index 0000000..89499eb Binary files /dev/null and b/public/assets/theme/shanghai_dojo_night_bg.webp differ diff --git a/types/booth.ts b/types/booth.ts index f4aa8ed..e904bb9 100644 --- a/types/booth.ts +++ b/types/booth.ts @@ -113,4 +113,5 @@ export type ScreenState = | "style" | "processing" | "result" + | "finished" | "rate_limited"; diff --git a/watermark_processor.py b/watermark_processor.py index 53952ce..43f689a 100644 --- a/watermark_processor.py +++ b/watermark_processor.py @@ -8,7 +8,7 @@ def apply_watermark(input_img_path, output_img_path): bw, bh = base.size base_dir = os.path.dirname(os.path.abspath(__file__)) - logo_path = os.path.join(base_dir, "public", "assets", "brand", "shanghai_karate_logo.png") + logo_path = os.path.join(base_dir, "public", "assets", "brand", "shanghai_karate_logo.webp") qr_path = os.path.join(base_dir, "public", "assets", "brand", "dojo_qr.png") margin = int(bw * 0.035)