-
Extends the 跳舞直播间 (dance room) system to one independent live room per character, with an operator playback-management/blacklist layer (T39), a "我的作品" page for paid users (T38), a standalone 人衣图生成 admin tool, and a new admin-dance-library.html page for curating the per-character dance clip pool with AI QC. On top of that, dance clips now have an explicit published (上架/下架) status: passing AI QC (or a manual admin override to "correct") auto- publishes a clip, failing it auto-unpublishes it, and only published clips are eligible for a room's random-fill pool — unreviewed/rejected clips stay in the library but no longer autoplay for viewers. Existing already-QC'd "correct" clips are backfilled to published on first boot so the room doesn't go quiet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MvkJsRcQ2qEuaFea31rBY8
-
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