-
New blacklist store (dressup-store.js) — explicitly distinct from T32's automated face-blacklist model and T26's garment moderation, per M.11's own warning. isBlacklisted(targetType, targetId) is checked at every submission entry point (/api/dance/pick, both paid upload endpoints — rejected before any spend, matching M.11's "不进流水线、不扣积分") and filters GET /api/dance/wardrobe and pickNextDanceClip's random-fill pool (extending T36's expired-character filter to also drop blacklisted garments/characters/generated_videos) — records are never deleted, just excluded from future selection, same "记录保留" spirit as T36. Admin CRUD: GET/POST /api/admin/blacklist, DELETE /api/admin/blacklist/:id — creation attributed via adminActorUserId (T30), same null-over-the-legacy-ADMIN_PASSWORD-channel behavior as credit/adjust. Playback management (需求文档 6.5 节): POST /api/admin/dance/schedule/pin inserts an existing clip into the very next slot (factored T34's priorityInbox-insert logic into a shared insertClipNext() so both call sites move in lockstep); POST .../schedule/skip advances past whatever's currently playing; POST /api/admin/dance/pause freezes only the random-fill loop (paid_boost content still drains immediately even while paused — 需求文档 6.5 节 frames "暂停自动填充" as filler-specific, not a full stop). dance-admin-ops.test.js covers blacklist CRUD/attribution, entry-point rejection for both a blacklisted garment (pick) and a blacklisted user (upload), pin/skip changing the schedule, and pause/resume against a deliberately drained-to-stable-size schedule (comparing exact lengths — comparing clip ids/count directly was flaky since a small mock pool can legitimately re-pick the same clip after a skip). This completes dev-plan-5.md (T31-T39) — 需求文档 9 节's fifth and final planned phase. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
需求文档 7.4 节's stated floor: an expired user_paid character (T36) or its videos must stay individually retrievable by their owner — "下线=不再出现在公开直播间和其他人的衣柜/随机池里",不是"下线=消失". GET /api/dance/my-works (login required, no emailVerified gate — this reads your own past purchases, doesn't spend anything) returns the requesting user's own characters/garments (ownerUserId match) each joined with their generated videos from library.json. New standalone page my-works.html (matches live.css's visual language) renders two sections with inline video players (same preload="none" pattern as admin.html), a status badge (播放中/已下线 for characters, 衣柜中/已下架 for garments), and a login gate for logged-out visitors — reusing GET /api/dance/my-works's 401 rather than duplicating dance.html's auth modal in a second page. Linked from dance.html's header once logged in. my-works.test.js: 401 without a session; a real user who uploaded one garment (T27) and one 1-segment character (T35, deliberately expired by test time) sees both with their videos still attached; a second, unrelated user sees neither. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
consumeCharacterSegment(clip), called alongside markPlayed() at both dance-specific scheduling call sites (T34's priorityInbox insert and the normal fill loop) — deliberately not folded into the shared markPlayed() itself, since live.html's text-channel clips never have a user_paid character behind them and architecture.md 2.1 keeps the two channels' scheduling logic independent. Decrements characters.remainingSegments each time a user_paid character's clip gets scheduled (its first play via T34's priority insert, and every later replay through the normal random-fill pool); hits 0 -> status:"expired". pickNextDanceClip's random-fill pool now excludes clips belonging to an expired character (需求 3.3 第 7 点: excluded from future selection, but the character record and its generated videos are never deleted — GET /api/admin/characters and GET /api/library still return them). dance-mode3.test.js: buying exactly 1 segment expires the character right after its single (priority-boosted) play; totalSegments stays 1 as history while remainingSegments goes to 0; the character and its clip remain queryable via the admin/library APIs afterward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
T34: priorityInbox (M.8) — paid_boost clips get inserted immediately after whatever's currently playing (danceSchedule[0]), pushing every later entry's timing back, rather than waiting for the fill-loop's normal "top up to 30s ahead" trigger. That distinction mattered: draining priorityInbox only from inside the fill loop (first attempt) technically satisfied "never loses to a vote-ranked pick" but could leave a ready paid_boost clip waiting a long time if the schedule already had ~30s queued — dance-mode3.test.js's race test caught this (timed out) before the eager-insert fix. ensureDanceSchedule's existing "generating -> done" reconciliation loop diverts a job into priorityInbox instead of danceQueue's "ready" state when job.priority === "paid_boost", spliced out of danceQueue after the loop (not mid-iteration). T35: POST /api/dance/upload-character (需登录+emailVerified, same rule as T27). Face photo -> T2 upload+EXIF strip -> T32 moderateFaceBlacklist -> T25-style describeCharacterImage (new pluggable fn, shares VISION_API_KEY) -> characters.create(source:"user_paid", isDefault: false, totalSegments/remainingSegments computed from pricing_config's new spend_character_per_segment×segmentsPerUnit) -> creditLedger.spend (relatedAssetId=character.id, rolled back on failure) -> submitGeneration(..., priority:"paid_boost") -> lands in priorityInbox (cache hit) or danceQueue pending T34's reconciliation (fresh generation). Garment is optional (need求文档 3.3 第 4 点): given garmentId is validated active-or-400, otherwise a random active garment is picked — a fully empty wardrobe is a 400 before any spend. dance-mode3.test.js covers both: auth/balance/no-active-garments gates, the happy path (cost scales with requested segments, character record shape, clip carries priority/requestedByUserId), and T34's actual guarantee — a paid_boost character is scheduled at or ahead of an already-ready free pick for the same garment, distinguished by characterId since both target the same garment on purpose. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
T32: moderateFaceBlacklist(imagePath, assetId) — same pluggable seam shape as T26, for mode 3's uploaded face photos (需求文档 5 节: filtering politically-sensitive likenesses). Default-permissive (auto_pass) when unconfigured, per explicit user confirmation this session — deliberately not assumed, since a wrong-default here (a real face silently passed through) has a different risk profile than T26's garment photos. T33: forwardToFalAssets (uploads.js) + fal's queue API (falSubmit/ falPollStatus/falFetchResult) wired into submitGeneration/ generateDanceVideo, which now dispatches on job.backend between the existing H3 Console path (renamed generateDanceVideoViaH3, unchanged behavior) and the new generateDanceVideoViaFal. A paid request (userId set) routes to fal once FAL_API_KEY is configured; everything else — free mode-一, or paid but no key yet — stays on H3 Console exactly as before (verified: full suite green, no FAL_API_KEY set in any existing test's env). Endpoints/params (storage/upload/initiate, queue.fal.run/{model}/ requests/{id}/status vs /requests/{id} as two separate calls, Authorization: Key <FAL_API_KEY>) are taken from fal's published docs and the fal-js client's own source, not guessed — fal-generation.test.js proves this codebase's side of the integration against a mock fal server. What remains genuinely unverified (no FAL_API_KEY exists) is whether the H3-Console-tuned prompt is interpreted the same way by fal's minimax/h3/reference-to-video model, and fal's exact terminal-failure status string(s) — flagged in code comments rather than guessed at. Also fixes a real gap T27 shipped with: submitGeneration never received userId, so every mode-2 clip's requestedByUserId (M.7) was silently null. T27's call site now passes userId (priority stays "normal" — only mode 3 skips voting), which also makes mode 2 fal-eligible. dance-mode2.test.js's happy-path test now asserts this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo -
T28 replaces the "开发中敬请期待" placeholder on dance.html's 传衣服 tab with a real upload panel: file input, the live spend_garment price pulled from the new public GET /api/pricing (not hardcoded, per dev-plan-4.md's acceptance criterion), and a submit flow that POSTs to T27's /api/dance/upload-garment. Not logged in opens the existing T22 login modal instead of failing silently; not email-verified shows an inline message; a 402 (insufficient balance) points at the new topup flow; success refreshes the balance and immediately polls the queue, same as the free wardrobe pick's own pollState() call. T29 adds a 充值 pill (visible once logged in) opening a tier picker built from GET /api/pricing's topup_tier entries, POSTing to T24's /api/credits/checkout and navigating to whatever URL comes back (the mock completion page today, Stripe's hosted Checkout once a real key lands tomorrow — both redirect back to dance.html?topup=success/ cancelled). A one-shot banner reads that query param on load, then scrubs it via history.replaceState. The account button's own label now also shows creditBalance, riding on the field GET /api/auth/me already started returning in T24. Manually verified end to end against the live server (localhost:8934): register -> verify -> mock checkout -> balance updates in the header -> topup banner -> upload panel shows the live price and opens the login modal when logged out. The upload itself surfaced that production's one seed character had never been marked isDefault (see the preceding commit) — not a bug in this code, but blocked a live walk of the full generate path until fixed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
Requires login + emailVerified (M.10). Order matters and each step undoes the previous one on failure: moderate (T26) before anything else exists, so a rejected image leaves nothing behind; create the garments row (source: user_paid) before spending, so the ledger entry can carry relatedAssetId — a failed spend deletes it back out; generation submission failure refunds and deletes it too. Reuses T8/ T9's submitGeneration whole, same M.7 dedupe cache, then rides the same danceQueue/ensureDanceSchedule machinery /api/dance/pick (mode 1) already uses — a paid entry only differs by carrying userId/ amountPaid, which mode 1's free entries don't set. Async generation failure (job fails after this handler already responded) is refunded from ensureDanceSchedule's existing "generating -> failed" tick, gated on a findByRelatedAssetAndReason lookup so a crash between that write and saveDanceState() persisting it can't double-refund — a duplicate here is real money, not a cosmetic bug. dance-mode2.test.js covers the full chain against a mock H3 Console: login/verification/balance gates, a non-image upload rejected before anything is spent, the happy path (spend -> auto-tag -> generate -> lands in dance schedule, garments row has user_paid/ownerUserId/ appearanceDescriptor), and — via a second server pointed at a mock H3 that always fails submission — the async refund path, redelivered twice to prove it's exactly-once. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
createCheckoutSession() is the seam (dev-plan-4.md §1.2 item 4): with no STRIPE_SECRET_KEY set — no real test-mode key exists yet, the user said "先做一个 mock,我明天再给你真实测试的 key" — POST /api/credits/checkout returns a same-origin /api/credits/mock-checkout URL that completes the topup itself, standing in for "redirected to Stripe, paid, redirected back" so the whole checkout -> credited loop (and T29's UI on top of it) can be built and tested today. With a key set, it creates a real Stripe Checkout Session via plain REST (fetch, no stripe SDK — this project has no dependencies at all) — untested against a live account until the key lands. POST /api/credits/webhook hand-rolls Stripe's documented HMAC-SHA256 signature scheme and refuses anything unsigned or unverifiable, including every request while STRIPE_WEBHOOK_SECRET is unset (not just badly-signed ones). Both the mock and real paths route through one completeTopup() -> creditLedger.topup() (T23) so Stripe's guaranteed at-least-once webhook delivery can't double-credit — verified in credits.test.js by redelivering the same event id twice. Also added creditBalance to GET /api/auth/me's response (T29's need) instead of a dedicated balance endpoint — it's the same "logged-in user's own state" this route already serves, and live.html/dance.html already poll it after login. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
/api/admin/* accepted only the T6 ADMIN_PASSWORD cookie until now — MVCP M.1/M.10's stated rule is users.role='admin' + a real session, and M.2's credit_ledger needs a real admin identity to attribute manual balance corrections to. isAdminAuthed() now accepts either path (the ADMIN_PASSWORD channel stays as a fallback so nobody's locked out before any role='admin' account exists by hand — dev-plan-4.md T30's explicit requirement). New POST /api/admin/credit/adjust wraps the new creditLedger.adjust() (db.js), which — unlike spend/refund — takes either sign of delta and records actorUserId (null over the legacy ADMIN_PASSWORD channel, which has no per-admin identity to attribute to). credit_ledger's actorUserId column needed an idempotent ALTER TABLE guard (no migration framework exists anywhere in this project) since the table's CREATE TABLE IF NOT EXISTS already shipped in T23 without it. Also switched db.js to WAL journal mode — surfaced by two processes legitimately touching the same data.sqlite at once in this session (the long-running server + a test run) hitting "database is locked" on the default rollback-journal mode; worth having before this bites in production too, not just to unblock local test runs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
Both are pluggable seams (dev-plan-4.md §1.2 items 1-2), same shape as T20's sendVerificationEmail: a fixed env var gates a real vendor call that isn't implemented yet (none has been picked), and the no-vendor path is what actually runs today. describeGarmentImage() — mode 2 has no admin around to hand-write garments.appearanceDescriptor the way admin_seed garments do, so it needs *something* non-empty to fill that field; today that's a fixed placeholder string, swapped for a real vision-model call once a vendor's picked. moderateGarmentImage() — the requirements doc flags image-content moderation as vendor-TBD but never actually lists it as its own task; this fills that gap. Every call writes a moderation_log row (new dressup-store.js export, M.9 schema) regardless of which branch runs, so the audit trail exists before a real vendor does. Default-permissive when unconfigured — T2's upload endpoint already enforces the real trust-boundary checks (magic-byte type check, 8MB cap, EXIF-stripped re-encode); this only covers the *content* check, which nothing here can perform without a vendor. Neither is unit-tested standalone (server.js doesn't export internal functions, matching sendVerificationEmail's existing precedent) — T27 integration-tests both through the actual upload endpoint. Also adds pricing.js to the tmpdir file-copy list in every test that spawns server.js in isolation — server.js now requires it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
-
T20: register now also creates a kind='email_verify' session (24h TTL) and calls sendVerificationEmail() — the one pluggable seam for a real provider once one's chosen (dev-plan-3.md §1 flagged no vendor exists yet). Outside production the register response carries a devVerifyUrl so the whole loop is testable without a real inbox. GET /api/auth/verify?token= flips emailVerified and revokes the token so it can't be replayed. T21: register reads any existing viewer_uid/viewer_nickname cookie (live.html/dance.html's plain client-set identity, unrelated to this session system) and carries it onto the new account as legacyUid/ nickname. Login pushes that legacyUid back onto the browser via Set-Cookie — even from a clean browser with no viewer_uid cookie at all — so logging into an account doesn't let someone dodge per-viewer_uid vote limits by starting a fresh anonymous identity each time. No history is rewritten, per dev-plan-3.md's explicit scope cut. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
POST /api/auth/register, POST /api/auth/login, POST /api/auth/logout, and a debug GET /api/auth/me (T19's own acceptance criteria calls for it explicitly). Passwords hashed with node:crypto's scryptSync (salt stored alongside the hash, verified with timingSafeEqual) — still zero new dependencies. Sessions live in a separate user_session cookie from admin_session (T6) — the two are unrelated systems, confirmed by a test that a user session can't unlock /api/admin/*. Login and "email not registered" return the identical 401 body so the endpoint can't be used to enumerate registered emails. Every test file that spawns server.js in an isolated tmpdir now also copies db.js alongside it (dance-channel/dressup-generate/dressup-crud/ admin-pages-flow/uploads-route .test.js) — server.js requires db.js unconditionally now, so those child processes were crashing silently without it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
h3ConsoleFetch blindly JSON.parses the response body regardless of status code; a momentary non-JSON 500 from H3 Console during polling (GET /v1/videos/:id) rejected the promise, which the poll loop let propagate straight to the outer catch — permanently marking the job "failed" even though the job was still fine on H3's side. Hit this for real today: a 20-job batch's concurrent polling briefly 500'd H3 Console's HTTP layer, and all 20 jobs got marked failed locally while every one of them was still queued/running on H3 (confirmed via direct GET /v1/videos/:id against the real console). Now a failed poll attempt is swallowed and retried on the next tick, bounded by the same wall-clock deadline as everything else (12aff60). Only a genuine terminal status (done/error/failed/cancelled) or deleted_at ends the job. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
H3's 4 GPUs run one sharded model (H3-CONSOLE.md /v1/status), not 4 independent workers, so a batch of N jobs is served ~serially. The poll loop's old fixed 200-iteration budget (~1000s at the 5s default interval) didn't account for queue depth: submitting a 9-job batch hit this live and 9/11 jobs got marked "generation timed out" locally while still sitting healthy in H3's queue (queue_position confirmed via GET /v1/videos/:id). Also treats H3's "cancelled" status as terminal, matching "error"/"failed". Recovered the wrongly-failed jobs by resetting them to "generating" in generation-jobs.json (their h3JobId was already set, so resumeDressupGenerations() picked them back up without resubmitting to H3) rather than resubmitting and burning the queue further. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
ingest.js's syncLibrary() only carries a library entry forward as-is when edited is true; otherwise its periodic auto-sync (every 5 min) rebuilds the record from the raw H3 job and silently drops channel/characterId/garmentId/refDanceVideoId. That broke the M.7 dedupe cache and would have let dance clips leak into the regular live-channel pool once channel got wiped. Caught while running a real batch generation: the second sync cycle stripped a clip's traceability fields within 5 minutes of it landing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
-
Backend (server.js): - T11: dance.html gets its own schedule/queue state machine, persisted to dance-state.json (gitignored, same as live-state.json). Per MVCP M.8, the schedule stores videoId references rather than inlined clip snapshots like live's does, joining back to library.json at read time. Generalized pickBestIndexByStatus/extracted pickLeastPlayedClip so both channels' schedulers share the same selection logic instead of forking it; live's random-fill pool now excludes channel:"dance" clips (and vice versa) so the two channels' filler picks stay independent, which wasn't true before this (T8's dance clips could have leaked into live.html's filler). - T12: GET /api/dance/wardrobe — public, unauthenticated list of active garments. - T15: POST /api/dance/pick {garmentId, nickname?} — free path, always uses the configured default character, reuses submitGeneration (T8/T9) whole including its M.7 dedupe cache, then hands the result to danceQueue. In-flight generations are reconciled against generationJobs on each schedule tick rather than via a promise chain, so a pick request survives a server restart the same way T8's own jobs do. - T17: separate danceViewers map and POST /api/dance/queue/vote/:id, so viewer counts and votes don't mix with live.html's. - SCHEDULE_TICK_MS env var makes both channels' tick interval configurable so tests don't wait out a real 2s. Frontend: - dance.html (T13/T14/T16): reuses live.html's visual language via live.css, adds dance.css for the 9:16 boxed player and wardrobe grid. Left column is a 衣柜/传衣服/传人 tab set (only 衣柜 is wired up — the other two need accounts/credits, dev-plan-2.md 第三/四期); right column shows the queue with vote buttons instead of live.html's chat. Age-confirmation gate (5.1节) blocks the page on first visit via a cookie, non-dismissable by clicking the backdrop. Tests: dance-channel.test.js spins up a mock H3 Console (same approach as dressup-generate.test.js) to exercise wardrobe listing, pick rejection/happy-path/dedupe, vote, and independent viewer counts end to end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> -
POST /api/admin/batch-gen takes {characterId, garmentIds[], refDanceVideoIds?[]}, expands to one job per garment (cross-producted with refDanceVideoIds when given, else one random active dance-ref per garment like the single-generate default), and submits each through the same submitGeneration() helper the single POST /api/admin/generate route now uses — so the M.7 dedupe cache (existing characterId+ garmentId combo) is honored per combo, not just per batch. GET /api/admin/batch-gen/status?batchId= returns that batch's generationJobs records for progress polling. admin-batch-gen.html: pick one character, multiple garments, and optionally pin specific reference dances; submits the batch, polls status every 3s into a progress table, and lets failed rows be retried individually via POST /api/admin/generate. Linked from the other admin pages' nav. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> -
generateDanceVideo() takes a queued generationJobs record through prompt build -> forward character/garment/dance-ref uploads to H3 Console's /v1/assets -> POST /v1/videos -> poll -> push a traced clip into library.json (characterId/garmentId/refDanceVideoId). Mirrors generateForPrompt's submit/poll/persist shape but keyed off the new generationJobs store so a restart resumes any in-flight job by its h3JobId, same as resumeGenerations() does for the live queue. POST /api/admin/generate accepts characterId/garmentId (+ optional refDanceVideoId, else a random active dance-ref is picked) and skips straight to the cached clip if that character x garment combo was already generated (M.7 dedupe). GET /api/admin/generate/:id polls job status. Tests spin up a mock H3 Console over HTTP so the full submit/poll/land path runs offline. Also fixes two existing test fixtures (dressup-crud, admin-pages-flow) that spawn server.js without copying prompt-template.js — worked until T8 added the first require() of it into server.js, at which point the spawned server silently failed to boot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
POST /api/admin/upload-video: mp4 magic-byte sniff (checks the ftyp box, not offset-0 like images), 50MB cap, no re-encode needed (unlike images, no EXIF/GPS concern for video). Reuses the multipart parser and uploads/ layout from T2's image path via a shared placeInUploads helper.
-
mountCrud() is a small generic GET-list/POST-create/PATCH/DELETE router shared across the three entities (same shape, see MVCP M.3/M.4/M.5) instead of copy-pasting the boilerplate three times. Each entity fills in its own admin_seed-appropriate defaults on create. characters additionally enforces the single-default-character invariant (2 节) across create/update. HTML admin pages for these come next.
-
Single shared password (.env ADMIN_PASSWORD) + in-memory HttpOnly session cookie, no accounts/roles — MVCP M.1/M.10's real account system supersedes this in period 3. Covers the existing /api/admin/library/* routes (previously unauthenticated, see architecture.md 1.2's tech-debt note) plus the new upload endpoint. admin.html gets a fetch() interceptor that redirects to the new admin-login.html on any 401 from /api/admin/*, rather than patching every call site.
-
POST /api/admin/upload: hand-rolled multipart parser (no deps installed in this project), real magic-byte sniffing (rejects extension-spoofed files), 8MB cap, and EXIF/GPS stripping via a sips bmp round-trip (macOS-native, no dependency — sips's direct jpeg->jpeg re-encode alone does NOT strip EXIF, only the bmp intermediate does). GET /uploads/* serves the result with a path-traversal guard. forwardToH3Assets() is the T7/T8 hook for pushing a saved upload to H3 Console's own POST /v1/assets (found in the S1-S4 spike) — not covered by the automated suite since it needs a live network call, same treatment as the spike scripts.
-
H3_GEN_PARAMS.frames 120 -> 240 (at 24fps, 5s -> 10s), still within H3's 4-15s range (H2 in docs/h3skill.md). Updated the prompt-optimization system prompt's duration reference and widened its word budget (40-70 -> 60-100) to match — still one continuous shot, just more room to let the camera move and the action unfold. Verified against the real GLM endpoint. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144MiJ2cLjMd9jWaeEpVfUJ
-
The previous system prompt asked GLM for a free-form "shot-by-shot" expansion, which reliably produced 4-5 fake cuts with timestamps running out to 8-10s — impossible for a clip fixed at ~5s (H3_GEN_PARAMS.frames / FPS). Rewrote it against docs/h3skill.md (MiniMax's own H3 prompt-writing methodology, added alongside this commit): every chat prompt has no reference images, so it's always T2VA mode, and at ~5s it's one continuous shot, never a montage. The new prompt asks for H3's actual three-field T2VA structure (integrated_multimodal_description / overall_soundscape / non_diegetic_music, fixed order and labels), a single dominant camera movement from the official vocabulary (type + amplitude + speed), concrete lighting (no hollow buzzwords like "8K"/"cinematic" alone), and — previously missing entirely — explicit sound design. Verified against the real GLM endpoint on several sample prompts: single continuous shot throughout, correct field structure, no more impossible multi-shot timestamps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144MiJ2cLjMd9jWaeEpVfUJ
-
Adds a prompt-optimization step ahead of H3 generation: a viewer's short chat line ("一只猫跳舞") gets expanded into a detailed shot-by-shot English prompt (camera work, lighting, action, setting) via one GLM Coding Plan call (glm-5.3-flash, /api/coding/ endpoint — the coding-plan-specific path, not the pay-per-token one) before being handed to H3. The expanded prompt becomes the clip's fullPrompt; the original short text stays as the display prompt. Persisted on the queue entry so a restart mid-generation doesn't re-call GLM. Best-effort throughout — no API key, a timeout, or any GLM error just falls back to using the original text unchanged, never blocking generation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144MiJ2cLjMd9jWaeEpVfUJ -
Swaps the H3_GEN_PARAMS default so future chat-triggered generations come out mobile-friendly (9:16) instead of 832x480 landscape. Existing library clips keep whatever aspect ratio they were generated at — the player already uses object-fit: contain, so mixed aspect ratios render fine either way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144MiJ2cLjMd9jWaeEpVfUJ
-
Adds playCount to each library clip, incremented every time it airs (markPlayed). The idle-filler pick in pickNextClip now randomizes only among the least-played clips in the eligible pool — everyone at 0 plays first, then everyone tied at 1, and so on — instead of picking uniformly at random across the whole library, which let some clips rack up far more airtime than others by chance alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144MiJ2cLjMd9jWaeEpVfUJ
-
Reworks live.css/live.html to match infiniteslop.ai's visual language: Tahoma bold type, pure-black canvas, hot-pink accent, and hand-drawn white cards with a thick black border + hard offset shadow for every button/bubble/prompt card. Left queue now groups into 接下来播放/生成中/排队中 sections, the center gets a white "now playing" card (time + requester + prompt) plus a pulsing LIVE pill, chat bubbles are colored per-nickname via a pastel hash, and the top bar gained a chat-close toggle alongside mute and the watching count. Adds real upvoting to go with the new queue UI: POST /api/queue/vote/:id toggles a per-viewer (cookie uid) vote on a queued prompt, and pickNextClip now schedules the highest-voted matched/ready entry first instead of strict FIFO — lets chat bump a favorite prompt to the front of the line. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0144MiJ2cLjMd9jWaeEpVfUJ
-
- Cookie-based viewer identity: first chat attempt prompts for a nickname (inline modal, not a blocking native prompt()), generates a client-side uid, both persisted in cookies for 1 year - Right panel is now a live chat feed (username + message + status) with an input row below it, replacing the old bare prompt textarea - New POST /api/chat: runs a moderation check (banned-word blocklist — ponytail: starter list, not a real content policy, swap in the real one) before a message is allowed into the existing prompt-matching/generation pipeline; rejected messages still show in the chat log, just flagged - New GET /api/chat: recent chat history (capped at 300), polled every 3s - Left queue simplified to prompt + submitter name only — dropped the resolution/duration/date metadata that was there before - schedule/promptQueue now carry `requestedBy`/`nickname` through to /api/live/state so the left queue and "now playing" bar can show who asked for what - /api/prompt kept working (now a thin wrapper over the same submitPromptToQueue() the chat endpoint uses) for direct API testing Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
-
Static video-gen testing tool (MiniMax native API, then fal.ai) that grew into an AI drama live channel backed by an internal H3 Console GPU cluster: shared virtual-playhead live playback, prompt submit/match/generate queue, admin CRUD for the video library, and bilingual (zh/en) semantic clip names. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>