Files
roboco/docs/backend/api/video-engine-endpoints.md
T

250 lines
8.2 KiB
Markdown
Raw Normal View History

# Video Engine API: Project-Scoped Endpoints
## Overview
The RoboCo video engine API is CEO-only and manages three concerns:
1. **On-demand video requests**: `POST /api/video/request` opens a video-authoring task scoped to a specific project
2. **Re-render (CEO retry)**: `POST /api/video/pipeline/{task_id}/rerender` clears render idempotency keys to re-trigger rendering
3. **Live preview proxy**: `GET /api/video/preview/{task_id}/{file_path:path}` serves authoring task composition HTML + assets with path-traversal confinement
All endpoints are CEO-only and require the global video engine flag enabled (`ROBOCO_VIDEO_ENGINE_ENABLED`).
---
## Endpoint: POST /api/video/request
### Purpose
Open a UX/UI video-authoring task for the CEO's on-demand brief, scoped to a specific project.
### Authentication
CEO-only (401 if not CEO).
### Request Body (VideoRequestBody)
```json
{
"occasion": "string", // Unique identifier; required, min 1 char
"brief": "string", // Video brief description; required, min 1 char
"platforms": ["string"], // Target platforms: ["x", "tiktok"]; required, min 1
"project_id": "UUID" // Project to author against; required (NEW in v2)
}
```
**Breaking Change**: `project_id` is now **required**. This field scopes the video authoring task and its render pass to the specified project, replacing the hardcoded `self_heal_project_slug` behavior.
### Response (VideoRequestResponse)
```json
{
"status": "opened|disabled|not_opened",
"task_id": "UUID|null",
"detail": "string"
}
```
### Response Codes
| Code | Status | Meaning |
|------|--------|---------|
| 200 | opened | Task created and dispatched to UX/UI developer |
| 200 | disabled | Video engine is off (`ROBOCO_VIDEO_ENGINE_ENABLED=false`) |
| 200 | not_opened | Duplicate occasion or open-post cap reached |
| 404 | — | `project_id` unresolvable OR project not opted in (`video_engine_enabled=false`) |
| 401 | — | Not authenticated as CEO |
### Behavior
1. **Project validation**: Looks up `project_id` and checks `video_engine_enabled=true`. Returns 404 if unresolvable or not opted in.
2. **Task creation**: Opens a normal ASSIGNED delivery task (`source=video`) dispatched to an available UX/UI developer (balanced by open-task count).
3. **Duplicate check**: Returns `not_opened` if a task for this `occasion` is already open.
4. **Open-post cap**: Returns `not_opened` if open-task count ≥ `ROBOCO_VIDEO_MAX_OPEN_POSTS`.
### Example
```bash
curl -X POST http://localhost:3000/api/video/request \
-H 'X-Agent-Token: <ceo-token>' \
-H 'Content-Type: application/json' \
-d '{
"occasion": "v2.0 Launch",
"brief": "30-second teaser for new dashboard",
"platforms": ["x", "tiktok"],
"project_id": "550e8400-e29b-41d4-a716-446655440000"
}'
```
---
## Endpoint: POST /api/video/pipeline/{task_id}/rerender
### Purpose
Clear render idempotency keys (`render_status`, `render_attempts`, `render_error`) on a completed video-authoring task, triggering the render loop to re-pick it up on the next cycle.
**Use case**: CEO fixes a composition error or wants to retry past a `failed` terminal state.
### Authentication
CEO-only (401 if not CEO).
### Path Parameters
| Name | Type | Description |
|---|---|---|
| `task_id` | UUID | Video-authoring task ID |
### Response (VideoPipelineItemResponse)
```json
{
"task_id": "UUID",
"title": "string",
"occasion": "string",
"status": "string",
"pr_number": "int|null",
"composition_id": "string|null",
"render_status": "string|null",
"render_attempts": "int",
"max_attempts": "int",
"render_error": "string|null"
}
```
After clearing, `render_status`, `render_attempts`, and `render_error` are `null` or zero.
### Response Codes
| Code | Meaning |
|------|---------|
| 200 | Keys cleared; next render cycle re-picks this task |
| 404 | Task not found, not video task, not completed, or no `composition_id` (nothing to render) |
| 401 | Not authenticated as CEO |
### Behavior
1. **Validation**: Checks task exists, is a video-authoring task (`source=VIDEO_SOURCE`), is COMPLETED, and has a `composition_id`.
2. **Clear keys**: Removes `render_status`, `render_attempts`, `render_error` from `video_draft` marker; preserves other fields.
3. **Render loop pickup**: On next orchestrator cycle, render loop scans for tasks with `render_status` unset and re-renders.
### Example
```bash
curl -X POST http://localhost:3000/api/video/pipeline/550e8400-e29b-41d4-a716-446655440000/rerender \
-H 'X-Agent-Token: <ceo-token>'
```
---
## Endpoint: GET /api/video/preview/{task_id}/{file_path:path}
### Purpose
Serve a video-authoring task's composition HTML and sibling assets (kit/, public/, etc.) from the project's merged read-clone. Used by the panel's live preview iframe.
### Authentication
CEO-only (401 if not CEO).
### Path Parameters
| Name | Type | Description |
|---|---|---|
| `task_id` | UUID | Video-authoring task ID |
| `file_path` | string | Path relative to workspace root; e.g., `motion/compositions/<id>/vertical.html` |
### Response
- **Content-Type**: Auto-detected from file extension
- **Headers**:
- `X-Frame-Options: SAMEORIGIN` — Allows same-origin iframe embedding
- `Content-Security-Policy: frame-ancestors 'self'` — Restricts frame embedding to same origin
- **Body**: File contents (HTML, CSS, JS, images, etc.)
### Response Codes
| Code | Meaning |
|------|---------|
| 200 | File served successfully |
| 404 | Task/project not found, file doesn't exist, or file path escapes workspace root |
| 401 | Not authenticated as CEO |
### Behavior
1. **Task lookup**: Fetches task; validates it's a video task (`source=VIDEO_SOURCE`) with `project_id`.
2. **Project resolution**: Looks up project by `project_id`.
3. **Workspace fetch**: Ensures project's read-clone is available (clones if needed).
4. **Path resolution**:
- Strips leading `/` from `file_path`
- Resolves against workspace root
- Validates resolved path is under root and is a regular file
- Returns 404 on traversal attempt or non-file path
5. **Serve**: Returns file with iframe-permitting headers.
### Path Traversal Confinement
The `_resolve_preview_path` helper prevents directory-traversal attacks:
```python
candidate = (root / file_path.lstrip("/")).resolve()
if not candidate.is_relative_to(root) or not candidate.is_file():
return None
```
Guarantees:
- `../` sequences are resolved before confinement check
- Absolute paths don't escape (resolved relative to root)
- Symlinks are resolved and still held under confinement
- Only regular files served; directories return 404
### Example
```bash
# Serve composition HTML
curl -H 'X-Agent-Token: <ceo-token>' \
'http://localhost:3000/api/video/preview/550e8400-e29b-41d4-a716-446655440000/motion/compositions/my-id/vertical.html'
# Serve referenced asset
curl -H 'X-Agent-Token: <ceo-token>' \
'http://localhost:3000/api/video/preview/550e8400-e29b-41d4-a716-446655440000/kit/public/logo.png'
```
---
## Project-Scoping Architecture
### What Changed
Previously, the video engine hardcoded `settings.self_heal_project_slug` everywhere. Now:
1. **On-demand requests** (`POST /video/request`): `project_id` required in request body
2. **Authoring tasks**: Each task stores its own `project_id`
3. **Render loop** (`orchestrator._render_both_cuts`): Uses task's `project_id` to resolve motion/ workspace, not hardcoded setting
### Rationale
Each opted-in project can now:
- Author and render videos against its own `motion/` directory
- Participate in release and spotlight videos from its own codebase
- Support on-demand briefs scoped to specific projects
### Resolution Method
New `VideoEngine.resolve_authoring_project(project_id, occasion)`:
- **If `project_id` provided** (on-demand, per-task): Looks up project by ID
- **If `project_id` is None** (release/spotlight hooks): Falls back to fixed RoboCo project (`self_heal_project_slug`)
- **Both paths**: Check `video_engine_enabled` and log skip reasons identically
### Migration Impact
**For on-demand endpoint clients**:
- Must now supply `project_id` in request body
- Requests without `project_id` fail validation (422)
- Panel's video-request form needs project picker (frontend task, out of scope)
**For release/spotlight hooks**:
- No change; they continue defaulting to fixed RoboCo project when no `project_id` provided