feat(web): gender-differentiated 4:5 covers + per-card styleGuide prebake

- Regenerate 60 covers (30 male + 30 female) via FLUX with story-specific
  prompts, replacing the prior gender-shared set
- Crop covers to 4:5 (960×1200) via sharp attention cover; matches new
  homepage card aspectRatio
- Persist all 60 prompts to public/home/prompts.json so the prebake step
  can reuse the cover's exact visual anchor (per-card styleGuide) and the
  first-act scene visually carries over from the poster the player clicked
- Restore /play?card= prebaked instant-play path on homepage card click
- Add OpenAI-compatible image route in ai-client for non-Runware endpoints
- Hide Next.js dev indicators globally; tweak F-key fullscreen label

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
DESKTOP-I1T6TF3\Q
2026-06-03 02:20:20 +08:00
parent 820a5f7e87
commit bed4dc5a8f
135 changed files with 826 additions and 476 deletions
+16 -7
View File
@@ -2,7 +2,10 @@
/**
* Compresses the freshly generated public/home/*.png into much
* smaller .webp files alongside them, then deletes the originals.
* Output webps target ~1200px on the long edge and quality 78.
* Crops each image to a 4:5 vertical aspect ratio (matching the homepage
* StoryCard layout in app/page.tsx) using sharp's smart-attention cover
* strategy, so the most salient subject stays in frame. Output webps
* target 960×1200 at quality 78.
*/
import { readdirSync, statSync, unlinkSync } from "node:fs";
@@ -13,7 +16,9 @@ import sharp from "sharp";
const __dirname = dirname(fileURLToPath(import.meta.url));
const DIR = resolve(__dirname, "..", "public", "home");
const MAX_EDGE = 1200;
// 4:5 final, 1200 long edge → 960×1200
const TARGET_W = 960;
const TARGET_H = 1200;
const QUALITY = 78;
const files = readdirSync(DIR).filter((f) => f.toLowerCase().endsWith(".png"));
@@ -26,11 +31,15 @@ for (const f of files) {
const inSize = statSync(inPath).size;
totalIn += inSize;
const img = sharp(inPath);
const meta = await img.metadata();
const longEdge = Math.max(meta.width ?? 0, meta.height ?? 0);
const resized = longEdge > MAX_EDGE ? img.resize({ width: meta.width >= meta.height ? MAX_EDGE : undefined, height: meta.height > meta.width ? MAX_EDGE : undefined }) : img;
await resized.webp({ quality: QUALITY, effort: 5 }).toFile(outPath);
await sharp(inPath)
.resize({
width: TARGET_W,
height: TARGET_H,
fit: "cover",
position: sharp.strategy.attention,
})
.webp({ quality: QUALITY, effort: 5 })
.toFile(outPath);
const outSize = statSync(outPath).size;
totalOut += outSize;
console.log(`${f.padEnd(16)} ${(inSize / 1024).toFixed(0).padStart(5)} KB → ${(outSize / 1024).toFixed(0).padStart(4)} KB`);