• 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
    yibo authored
     
    Browse File »
  • 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
    yibo authored
     
    Browse File »
  • 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
    yibo authored
     
    Browse File »
  • 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
    yibo authored
     
    Browse File »
  • 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>
    yibo authored
     
    Browse File »
  • 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>
    yibo authored
     
    Browse File »
  • 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>
    yibo authored
     
    Browse File »
  • 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.
    yibo authored
     
    Browse File »
  • 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.
    yibo authored
     
    Browse File »
  • 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.
    yibo authored
     
    Browse File »
  • 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.
    yibo authored
     
    Browse File »

  • 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
    yibo authored
     
    Browse File »
  • 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
    yibo authored
     
    Browse File »
  • 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
    yibo authored
     
    Browse File »
  • 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
    yibo authored
     
    Browse File »
  • 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
    yibo authored
     
    Browse File »
  • 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
    yibo authored
     
    Browse File »
  • - 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>
    yibo authored
     
    Browse File »
  • 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>
    yibo authored
     
    Browse File »