perf(booth): optimize assets and result flow

WebP reduces UI payload; JPEG keeps portrait saves compatible.\nFinish ends the visit without prompting another portrait.
This commit is contained in:
Ajasra committed 2026-10-30 00:00:16 +08:00
1 parent 56da9c86d2
commit 6772e0cf37
41 files changed
+225 -97

No files matched your search

+1 -1
View File
@@ -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,
+7 -7
View File
@@ -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",
+23 -26
View File
@@ -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}`);
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -16,7 +16,7 @@ export async function applyWatermarkToFile(filePath: string): Promise<void> {
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);