Files
roboco/.claude/rules/video-engine.md
T
2026-07-29 16:38:49 +02:00

5.5 KiB

paths
paths
roboco/services/video*.py
roboco/services/x_video_client.py
roboco/services/tiktok_client.py
motion/**
docker/video-renderer.Dockerfile

RoboCo video engine

RoboCo video engine (default-off). Bespoke motion-graphics videos (release announcements, feature spotlights, on-demand CEO briefs) authored by a UX/UI dev and distributed to X/TikTok — nothing renders or posts without the flags on, and nothing posts without an explicit CEO approval. Gated by ROBOCO_VIDEO_ENGINE_ENABLED (+ sub-switches ROBOCO_VIDEO_ON_RELEASE / ROBOCO_VIDEO_ON_SPOTLIGHT, and _MAX_OPEN_POSTS / _RENDER_INTERVAL_SECONDS / _RENDER_TIMEOUT_SECONDS / _REQUEST_TIMEOUT_SECONDS / _OUTPUT_DIR); a CEO on-demand brief rides POST /video/request regardless of the release/spotlight sub-switches. A project opts in via projects.video_engine_enabled (migration 063, mirroring ci_watch_enabled): the global flag arms the subsystem, the per-project flag opts a repo into authoring against its motion/VideoEngine._opted_in_project no-ops open_video_task until the operator flips it on the project settings page's budget/ops card (Wave C — was the edit-project dialog). Two task kinds mirror the XEngine/ReleaseManagerEngine "originate a CEO-scoped artifact" shape but split across the real delivery lifecycle: VideoEngine.open_video_task (roboco/services/video_engine.py) opens a normal, ASSIGNED authoring task (source=video, confirmed_by_human=True, team=UX/UI, balanced across ux-dev-1/ux-dev-2 by open-task count) that dispatches like any other pre-assigned code task — NOT held, NOT in any dispatcher's skip bucket. The assigned dev builds a HyperFrames HTML composition under motion/compositions/<id>/ and calls the UX/UI-team-gated propose_video do-tool (metadata-only: composition id, input props, per-platform captions — every developer role carries the tool on their manifest, but the runtime _caller_team check rejects a be-dev/fe-dev) to stamp the task's video_draft marker, then commits + open_pr through the normal PR-review gate. Once that authoring task reaches completed, the orchestrator's _video_render_loop (bounded retry, _MAX_VIDEO_RENDER_ATTEMPTS) tars the merged motion/ dir from the project's read-clone and POSTs it to the credential-free video-renderer sidecar (VideoRenderer in roboco/services/video_renderer_client.py, ROBOCO_VIDEO_RENDERER_BASE_URL) to render both the 9:16 and 1:1 cuts to MP4 (video_output_dir); on success VideoEngine._originate_video_post materializes a held video-post draft (source=video_post, confirmed_by_human=False, Secretary-owned, skipped by every dispatcher) carrying mp4_paths ({vertical, square} absolute paths) + the per-platform captions. The CEO reviews it in the panel's video queue (video-post-queue.tsx; GET /video/posts lists drafts including mp4_paths so the panel knows which cuts exist, GET /video/posts/{id}/media?cut=vertical|square streams the MP4 bytes for the preview player, CEO-gated throughout) and edits captions / approves / rejects. VideoPostService.approve (roboco/services/video_post_service.py) is the ONLY caller of the X-v2 (XVideoPoster in x_video_client.py) and TikTok inbox-upload (TikTokPoster in tiktok_client.py) posters; because a video upload + transcode/poll can run well past a minute, the critical section runs under a heartbeat-renewed Redis mutex (heartbeat_mutex.py, mirroring ReleaseProposalService's release-execute lock shape) rather than a flat lock, commits each platform's posted-id durably before attempting the next (a partial failure never re-posts an already-succeeded platform on retry), and is idempotent (an already-COMPLETED draft returns the stored ids without calling a poster again). TikTok's four OAuth2 secrets live Fernet-encrypted in a singleton tiktok_credentials row (mirroring the git-token / x_credentials pattern; the API only ever returns has_credentials) — set via the panel's TikTok credentials card. NullVideoRenderer / NullXVideoPoster / NullTikTokPoster make every unconfigured leg a graceful no-op rather than a crash. Artifact verification (2026-07-16): authoring is gated on the RENDERED clip, not its source — the request_render do-verb (developer/QA, mirroring request_sandbox's shape) renders the caller's actual composition via the sidecar's new frames mode (POST /render with frames=N → ffprobe'd duration + N midpoint-sampled PNGs, tar.gz back) and extracts them to the container-shared {workspaces_root}/{project}/.previews/{task8}/{orientation}/; the dev renders their own working tree (worktree-aware, head_sha/dirty provenance), QA a read-only git archive export of the assembled branch (WorkspaceService.export_branch_motion — the read clone's pinned HEAD undisturbed). Success stamps the render_preview marker; i_am_done on a source=video task refuses without it (Requirement.RENDER_VERIFIED in the tracing gate, mirrored in the possibilities-matrix fast path — the canonical source string lives in foundation as markers.VIDEO_TASK_SOURCE), the dev spawn prompt orders Read-every-frame verification, and QA's claim_review evidence carries a video_context block so the reviewer checks output, not source. A CEO reject of a held video-post draft with a non-empty reason now also re-enters the flow: VideoPostService.rejectVideoEngine.reauthor_from_rejection opens a fresh authoring task carrying the verbatim feedback + a revise-in-place pointer at the existing composition (best-effort, never fails the reject).