• 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 »