feat(web): InfiPlot low-fi homepage with AI-generated cards + gender-reactive hero + audio toggle fix
Rebuilds the landing page from the prototype: 1900px scale-to-fit hero with hand-drawn SVG-jitter frames, typewriter input + start button, 5 horizontal collapsible category selectors (with style-picker modal), 7 scattered hero cards over a 16-card masonry gallery, and project intro panel. Each card is filled with a Runware FLUX.2 image, pre-generated and stored as WebP (~2 MB total for 30 cards). Hero card content + image switches by 性向 (男性向 / 女性向); gallery stays shared. Hover overlay on every card shows title + outline in a bottom-up dark gradient, matching the prior homepage's interaction style. Bug fixes uncovered by tracing the form-state → engine pipeline: - 「语音配音:关闭」was previously stuffed into styleGuide (consumed only by FLUX, ignored by TTS). Now serialized as audioEnabled boolean in the sessionStorage payload; play page's fetchBeatAudio early-returns when false, so no /api/beat-audio request fires. - 「绘画风格:自动」used to pass the literal Chinese phrase "由模型根据 prompt 自动判断画风" to FLUX, which painted it as text. Now maps to the 二次元/galgame default prompt. Adds reusable scripts under apps/web/scripts/: - generate-home-images.mjs — Runware FLUX.2 idempotent batch generator - optimize-home-images.mjs — sharp WebP downscale + recompress Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -262,6 +262,10 @@ function PlayInner() {
|
||||
// changes so stale in-flight requests can't poison the new scene's map
|
||||
// (beat ids like "b1" are scene-local and would collide across scenes).
|
||||
const beatAudioAbortRef = useRef<Map<string, AbortController>>(new Map());
|
||||
// User-toggled "语音配音" from the homepage. Defaults to true for back-compat
|
||||
// when older sessionStorage payloads omit the field. Mutated once in
|
||||
// bootstrap and read by fetchBeatAudio to early-return without any /api call.
|
||||
const audioEnabledRef = useRef<boolean>(true);
|
||||
|
||||
// Mirrors for use inside async handlers (closure-stable)
|
||||
const sessionRef = useRef<Session | null>(null);
|
||||
@@ -317,6 +321,7 @@ function PlayInner() {
|
||||
sess: Session,
|
||||
beat: { id: string; speaker?: string; line?: string; lineDelivery?: string },
|
||||
): Promise<void> => {
|
||||
if (!audioEnabledRef.current) return; // user toggled 语音配音 → 关闭
|
||||
if (!beat.speaker || !beat.line) return;
|
||||
const speaker = sess.characters.find((c) => c.name === beat.speaker);
|
||||
if (!speaker?.voice) return; // not yet provisioned — server can't synth anyway
|
||||
@@ -450,7 +455,14 @@ function PlayInner() {
|
||||
const stored = sessionStorage.getItem("yume:custom");
|
||||
if (stored) {
|
||||
try {
|
||||
payload = JSON.parse(stored);
|
||||
const parsed = JSON.parse(stored) as {
|
||||
worldSetting: string;
|
||||
styleGuide: string;
|
||||
audioEnabled?: boolean;
|
||||
};
|
||||
payload = { worldSetting: parsed.worldSetting, styleGuide: parsed.styleGuide };
|
||||
// default true for older payloads that omit the flag
|
||||
audioEnabledRef.current = parsed.audioEnabled !== false;
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user