feb1c7b3e9
Completes RenderPipeline2 phases 3-5. Additive throughout: with validation.enabled=false the pipeline behaves exactly as before. Validation (§8) runs on the worker straight after a render, while it still holds the job and the files are local. Everything is read from the EXR header — one 64 KB read per sampled frame, no pixel decode — so the engine needs no oiiotool. The render job deliberately stays open through the stage, so a worker that dies there is reaped and retried instead of stranding the export in VALIDATING. Timecode is the primary check and is anchored on the plate, never the API: measured on the live show, Shot.timecodeStart is the editorial in-point and sits one handle-length (8f) ahead of the plate, so checking against it would pass the defective exports and fail the correct ones. Reference precedence is manifest timing plate -> plate found on disk -> API value, and the API rank is WARN-only. The panel now resolves plate provenance by walking the comp's own footage layers, so the worker never guesses which element carries the comp's timing. Verified against real studio renders via the new --validate mode: it catches both defect classes seen in shipped work — the +8 handle offset and frame-derived timecode (00:00:41:17 = frame 1001 at 24fps). QC (§10) is one service behind two front doors, API-key for the panel and session for the web. PASS means ready to upload and nothing else; FAIL sets the comp task to CHANGES. The panel gains a QC section that imports the real EXR sequence at full res rather than the H.264. Delivery (§11) rides the existing queue as a DELIVERY_BUILD job: hard links with a copy fallback, verification by re-hashing delivered bytes against the validation-time digest, manifest.json and the CSV report. fsutil hardlink list is unavailable over SMB, so link identity is never queried — reading the bytes back covers links and copies alike. Also fixes ApiClient.ProgressAsync throwing on transport failure, which would have aborted work in flight during a brief server outage; progress reporting is best-effort by contract. Removes the stale RenderWorker/ rule from the root .gitignore. The worker's source is tracked like the rest of the repo and its own .gitignore already covers bin/, obj/ and publish/; the rule silently excluded new files while the existing 20 stayed tracked. Not yet applied: migration 20260813000000 was hand-written because the dev database was unavailable — run prisma migrate deploy and verify before relying on it. None of this has run against a live server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
546 lines
15 KiB
Markdown
546 lines
15 KiB
Markdown
# VFXReview External API Reference
|
|
|
|
Base URL: `https://review.twotalesvfx.com`
|
|
|
|
All requests require an `Authorization` header. Two formats are accepted:
|
|
|
|
```
|
|
Authorization: Bearer <API_SECRET_KEY>
|
|
X-Api-Key: <API_SECRET_KEY>
|
|
```
|
|
|
|
---
|
|
|
|
## Endpoints Overview
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| `GET` | `/api/ext/projects` | List all projects with showIds |
|
|
| `GET` | `/api/ext/shots/lookup` | Look up a shot by code — primary pipeline endpoint |
|
|
| `GET` | `/api/ext/projects/{projectCode}/episodes` | List episodes for a project |
|
|
| `GET` | `/api/ext/projects/{projectCode}/shots` | List shots for a project by code |
|
|
| `GET` | `/api/ext/shots` | List shots by database project ID |
|
|
| `GET` | `/api/ext/shots/{shotId}` | Get a single shot by DB id or shot code |
|
|
| `POST` | `/api/ext/shots` | Create a new shot |
|
|
| `PATCH` | `/api/ext/shots/{shotId}` | Update shot fields (e.g. increment version) |
|
|
|
|
### Render pipeline (RenderPipeline2)
|
|
|
|
| # | Method | Path | Caller | Description |
|
|
|---|--------|------|--------|-------------|
|
|
| E1 | `POST` | `/api/ext/exports` | panel | Queue Export — creates Export + RenderJob |
|
|
| E2 | `GET` | `/api/ext/exports/latest` | panel | Latest export + live status for the panel header |
|
|
| E3 | `GET` | `/api/ext/exports/{exportId}` | panel/worker | Export detail incl. validations and QC |
|
|
| E4 | `POST` | `/api/ext/exports/{exportId}/qc` | panel | QC Pass / QC Fail |
|
|
| E5 | `GET` | `/api/ext/qc/queue` | panel | Exports in READY_FOR_QC |
|
|
| E6 | `POST` | `/api/ext/workers/register` | worker | Register machine, receive tuning |
|
|
| E7 | `POST` | `/api/ext/workers/{machineId}/heartbeat` | worker | Heartbeat + cancel channel |
|
|
| E8 | `POST` | `/api/ext/render/jobs/claim` | worker | Atomically claim the next job |
|
|
| E9 | `PATCH` | `/api/ext/render/jobs/{jobId}/progress` | worker | Progress/ETA; renews the lease |
|
|
| E10 | `POST` | `/api/ext/render/jobs/{jobId}/fail` | worker | Report failure |
|
|
| E11 | `POST` | `/api/ext/render/jobs/{jobId}/complete-render` | worker | aerender finished → VALIDATING |
|
|
| E12 | `POST` | `/api/ext/render/jobs/{jobId}/validation` | worker | Report all validation results |
|
|
| E13 | `POST` | `/api/ext/render/jobs/{jobId}/finalize` | worker | Preview artifacts done → READY_FOR_QC |
|
|
| E14 | `POST` | `/api/ext/exports/{exportId}/retry` | panel | Retry a failed export |
|
|
| E20 | `POST` | `/api/ext/render/jobs/{jobId}/artifact-presign` | worker | Presigned upload URL |
|
|
| — | `POST` | `/api/ext/render/jobs/{jobId}/delivery` | worker | Report a DELIVERY_BUILD result |
|
|
|
|
Session-authenticated twins for the web UI live under `/api/render/*` and
|
|
`/api/deliveries/*` — same service layer, no API key.
|
|
|
|
---
|
|
|
|
## Shot Status Values
|
|
|
|
| Value | Description |
|
|
|---|---|
|
|
| `WAITING` | Not yet started |
|
|
| `IN_PROGRESS` | Currently being worked on |
|
|
| `IN_REVIEW` | Submitted for review |
|
|
| `REVISIONS` | Changes requested |
|
|
| `COMPLETE` | Approved and complete |
|
|
|
|
---
|
|
|
|
## Shot Priority Values
|
|
|
|
| Value | Description |
|
|
|---|---|
|
|
| `LOW` | Low priority |
|
|
| `NORMAL` | Default |
|
|
| `HIGH` | High priority |
|
|
| `URGENT` | Urgent |
|
|
|
|
---
|
|
|
|
## Shot Code Format
|
|
|
|
```
|
|
{showId}_{episode}_{scene}_{shotNumber}
|
|
|
|
e.g. UNG_106_010_020
|
|
^^^ ^^^ ^^^ ^^^
|
|
| | | shot number (zero-padded)
|
|
| | scene
|
|
| episode
|
|
show ID
|
|
```
|
|
|
|
---
|
|
|
|
## 1. List Projects
|
|
|
|
```
|
|
GET /api/ext/projects
|
|
```
|
|
|
|
Returns all projects with their `showId`, `code`, and metadata. Useful for populating project selectors in pipeline tools or resolving a showId → projectCode mapping.
|
|
|
|
### Query Parameters
|
|
|
|
| Parameter | Required | Default | Description |
|
|
|---|---|---|---|
|
|
| `status` | No | `ACTIVE` | `ACTIVE`, `ARCHIVED`, or `ALL` |
|
|
|
|
### Response `200 OK`
|
|
|
|
```json
|
|
{
|
|
"projects": [
|
|
{
|
|
"id": "cmp6l5mzq0001ua0gz07bk72f",
|
|
"name": "Ungo Season 1",
|
|
"code": "UNG_S1",
|
|
"showId": "UNG",
|
|
"projectType": "EPISODIC",
|
|
"status": "ACTIVE",
|
|
"startDate": "2026-01-01T00:00:00.000Z",
|
|
"dueDate": "2026-12-01T00:00:00.000Z",
|
|
"_count": { "shots": 312 }
|
|
}
|
|
],
|
|
"total": 1
|
|
}
|
|
```
|
|
|
|
### Project Type Values
|
|
|
|
| Value | Description |
|
|
|---|---|
|
|
| `STANDARD` | Non-episodic project |
|
|
| `EPISODIC` | Series with episode numbers |
|
|
|
|
### Project Status Values
|
|
|
|
| Value | Description |
|
|
|---|---|
|
|
| `ACTIVE` | Currently in production |
|
|
| `ARCHIVED` | Completed or archived |
|
|
|
|
---
|
|
|
|
## 2. Look Up Shot by Code
|
|
|
|
**Primary endpoint used by pipeline tools (After Effects, Nuke, etc.)**
|
|
|
|
```
|
|
GET /api/ext/shots/lookup?shotCode={shotCode}&projectCode={projectCode}
|
|
```
|
|
|
|
### Query Parameters
|
|
|
|
| Parameter | Required | Description |
|
|
|---|---|---|
|
|
| `shotCode` | Yes | Human-readable shot code, e.g. `UNG_106_010_020` |
|
|
| `projectCode` | Recommended | Project code, e.g. `UNG_S1` — disambiguates if same shot code appears in multiple projects |
|
|
|
|
### Response `200 OK`
|
|
|
|
```json
|
|
{
|
|
"shot": {
|
|
"id": "clxxxxxxxxxxxxxx",
|
|
"shotCode": "UNG_106_010_020",
|
|
"scene": "010",
|
|
"episode": "106",
|
|
"sequence": "010",
|
|
"shotNumber": 20,
|
|
"description": "Hero establishing wide",
|
|
"notes": "Keep sky practical",
|
|
"status": "IN_PROGRESS",
|
|
"priority": "NORMAL",
|
|
"frameStart": 1001,
|
|
"frameEnd": 1120,
|
|
"fps": 24,
|
|
"dueDate": "2026-08-01T00:00:00.000Z",
|
|
"thumbnailUrl": "/api/files/thumb_abc123",
|
|
"createdAt": "2026-06-01T10:00:00.000Z",
|
|
"updatedAt": "2026-07-15T14:30:00.000Z",
|
|
|
|
"shotVersion": "v003",
|
|
"exrOutput": "UNG_106_010_020_cmp_TT_v003",
|
|
|
|
"sourceClip": "A106_C020_0601AB",
|
|
"timecodeStart": "01:00:10:00",
|
|
"timecodeEnd": "01:00:15:00",
|
|
"clipDuration": "00:00:05:00",
|
|
"seqTimecodeStart": "00:59:49:12",
|
|
"seqTimecodeEnd": "00:59:55:00",
|
|
|
|
"project": {
|
|
"id": "cmp6l5mzq0001ua0gz07bk72f",
|
|
"name": "Ungo Season 1",
|
|
"code": "UNG_S1",
|
|
"showId": "UNG"
|
|
},
|
|
"artist": {
|
|
"id": "clxxxxxxxxxxxxxx",
|
|
"name": "Jane Smith",
|
|
"email": "jane@studio.com"
|
|
},
|
|
"tasks": [
|
|
{
|
|
"id": "clxxxxxxxxxxxxxx",
|
|
"title": "Comp",
|
|
"type": "COMP",
|
|
"status": "IN_PROGRESS"
|
|
}
|
|
],
|
|
"versions": [
|
|
{
|
|
"id": "clxxxxxxxxxxxxxx",
|
|
"versionNumber": 3,
|
|
"approvalStatus": "PENDING",
|
|
"reviewStatus": "PENDING",
|
|
"fileUrl": "/api/files/vid_abc123",
|
|
"thumbnailUrl": "/api/files/thumb_abc123",
|
|
"createdAt": "2026-07-15T14:30:00.000Z"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pipeline-Specific Fields
|
|
|
|
| Field | Type | Description |
|
|
|---|---|---|
|
|
| `shotVersion` | `string` | Current version string, e.g. `v003`. Used for output naming. |
|
|
| `exrOutput` | `string` | Base filename for EXR renders, e.g. `UNG_106_010_020_cmp_TT_v003` |
|
|
| `seqTimecodeStart` | `string` | Shot in-point in the locked picture sequence (HH:MM:SS:FF) |
|
|
| `seqTimecodeEnd` | `string` | Shot out-point in the locked picture sequence (HH:MM:SS:FF) |
|
|
| `sourceClip` | `string` | Original camera roll/clip name from the EDL |
|
|
| `timecodeStart` | `string` | Source clip timecode in (HH:MM:SS:FF) |
|
|
| `timecodeEnd` | `string` | Source clip timecode out (HH:MM:SS:FF) |
|
|
| `clipDuration` | `string` | Clip duration (HH:MM:SS:FF) |
|
|
|
|
---
|
|
|
|
## 3. List Episodes
|
|
|
|
```
|
|
GET /api/ext/projects/{projectCode}/episodes
|
|
```
|
|
|
|
### Path Parameters
|
|
|
|
| Parameter | Description |
|
|
|---|---|
|
|
| `projectCode` | Human-readable project code, e.g. `UNG_S1` |
|
|
|
|
### Query Parameters
|
|
|
|
| Parameter | Required | Description |
|
|
|---|---|---|
|
|
| `shotNames` | No | Pass `1` to include full per-episode shot list (default off) |
|
|
|
|
### Response `200 OK`
|
|
|
|
```json
|
|
{
|
|
"project": {
|
|
"id": "cmp6l5mzq0001ua0gz07bk72f",
|
|
"name": "Ungo Season 1",
|
|
"code": "UNG_S1",
|
|
"showId": "UNG"
|
|
},
|
|
"episodes": [
|
|
{
|
|
"episode": "106",
|
|
"shotCount": 42,
|
|
"sequences": ["010", "020", "030"]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
When `shotNames=1`, each episode entry also includes a `shots` array with:
|
|
`id`, `shotCode`, `episode`, `sequence`, `status`, `exrOutput`, `seqTimecodeStart`, `seqTimecodeEnd`, `sourceClip`, `timecodeStart`, `timecodeEnd`, `clipDuration`.
|
|
|
|
---
|
|
|
|
## 4. List Shots by Project Code
|
|
|
|
```
|
|
GET /api/ext/projects/{projectCode}/shots
|
|
```
|
|
|
|
### Path Parameters
|
|
|
|
| Parameter | Description |
|
|
|---|---|
|
|
| `projectCode` | Human-readable project code, e.g. `UNG_S1` |
|
|
|
|
### Query Parameters
|
|
|
|
| Parameter | Required | Default | Description |
|
|
|---|---|---|---|
|
|
| `episode` | No | — | Filter by episode, e.g. `106` |
|
|
| `sequence` | No | — | Filter by sequence, e.g. `010` |
|
|
| `status` | No | — | Filter by shot status |
|
|
| `page` | No | `1` | Page number |
|
|
| `limit` | No | `200` | Results per page (max `500`) |
|
|
|
|
### Response `200 OK`
|
|
|
|
```json
|
|
{
|
|
"project": { "id": "...", "name": "...", "code": "UNG_S1", "showId": "UNG" },
|
|
"pagination": { "page": 1, "limit": 200, "total": 42, "pages": 1 },
|
|
"shots": [
|
|
{
|
|
"id": "clxxxxxxxxxxxxxx",
|
|
"shotCode": "UNG_106_010_020",
|
|
"scene": "010",
|
|
"episode": "106",
|
|
"sequence": "010",
|
|
"shotNumber": 20,
|
|
"description": "Hero establishing wide",
|
|
"status": "IN_PROGRESS",
|
|
"priority": "NORMAL",
|
|
"frameStart": 1001,
|
|
"frameEnd": 1120,
|
|
"fps": 24,
|
|
"dueDate": "2026-08-01T00:00:00.000Z",
|
|
"thumbnailUrl": null,
|
|
"exrOutput": "UNG_106_010_020_cmp_TT_v003",
|
|
"seqTimecodeStart": "00:59:49:12",
|
|
"seqTimecodeEnd": "00:59:55:00",
|
|
"sourceClip": "A106_C020_0601AB",
|
|
"timecodeStart": "01:00:10:00",
|
|
"timecodeEnd": "01:00:15:00",
|
|
"clipDuration": "00:00:05:00",
|
|
"artist": { "id": "...", "name": "Jane Smith", "email": "jane@studio.com" },
|
|
"_count": { "versions": 3, "tasks": 2 }
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
> **Note:** `shotVersion` is not included in this response. Use the lookup endpoint to get it.
|
|
|
|
---
|
|
|
|
## 5. List Shots by Project ID
|
|
|
|
```
|
|
GET /api/ext/shots?projectId={projectId}
|
|
```
|
|
|
|
Legacy endpoint. Prefer `/api/ext/projects/{projectCode}/shots` for new integrations.
|
|
|
|
### Query Parameters
|
|
|
|
| Parameter | Required | Description |
|
|
|---|---|---|
|
|
| `projectId` | Yes | Database CUID of the project |
|
|
| `episode` | No | Filter by episode |
|
|
| `status` | No | Filter by shot status |
|
|
| `shotCode` | No | Return only the shot matching this exact code |
|
|
|
|
### Response `200 OK`
|
|
|
|
```json
|
|
{
|
|
"shots": [ /* array of shot objects */ ],
|
|
"total": 42
|
|
}
|
|
```
|
|
|
|
Shot objects include: `id`, `shotCode`, `scene`, `episode`, `shotNumber`, `description`, `status`, `priority`, `frameStart`, `frameEnd`, `fps`, `dueDate`, `createdAt`, `updatedAt`, `artist`, `_count`.
|
|
|
|
> **Note:** Pipeline-specific fields (`exrOutput`, `shotVersion`, `seqTimecodes`, etc.) are **not** included. Use the lookup endpoint.
|
|
|
|
---
|
|
|
|
## 6. Get Single Shot
|
|
|
|
```
|
|
GET /api/ext/shots/{shotId}
|
|
```
|
|
|
|
### By Database ID
|
|
|
|
```http
|
|
GET /api/ext/shots/clxxxxxxxxxxxxxx
|
|
```
|
|
|
|
### By Shot Code
|
|
|
|
```http
|
|
GET /api/ext/shots/UNG_106_010_020?byCode=1&projectId=cmp6l5mzq0001ua0gz07bk72f
|
|
```
|
|
|
|
| Query Param | Description |
|
|
|---|---|
|
|
| `byCode` | Set to `1` to treat the path segment as a shot code |
|
|
| `projectId` | Required when `byCode=1` to scope the lookup |
|
|
|
|
### Response `200 OK`
|
|
|
|
Returns the same full shot object as the lookup endpoint, including tasks and latest version. Pipeline-specific fields (`exrOutput`, `shotVersion`, `seqTimecodeStart`, `seqTimecodeEnd`, `sourceClip`, etc.) are included.
|
|
|
|
---
|
|
|
|
## 7. Create Shot
|
|
|
|
```
|
|
POST /api/ext/shots
|
|
Content-Type: application/json
|
|
```
|
|
|
|
Accepts either `application/json` or `multipart/form-data` (when uploading a thumbnail file).
|
|
|
|
### Request Body
|
|
|
|
| Field | Type | Required | Description |
|
|
|---|---|---|---|
|
|
| `projectId` | string (CUID) | Yes | Database project ID |
|
|
| `scene` | string | Yes | Scene identifier, alphanumeric + underscore, e.g. `010` |
|
|
| `episode` | string | No | Episode number. Required for `EPISODIC` projects |
|
|
| `description` | string | No | Shot description |
|
|
| `artistId` | string (CUID) | No | Assign to an artist by their DB id |
|
|
| `priority` | string | No | `LOW`, `NORMAL`, `HIGH`, or `URGENT` (default `NORMAL`) |
|
|
| `fps` | number | No | Frame rate (default `24`) |
|
|
| `frameStart` | integer | No | First frame number |
|
|
| `frameEnd` | integer | No | Last frame number |
|
|
| `dueDate` | string (ISO 8601) | No | Due date |
|
|
| `thumbnailUrl` | string (URL) | No | Pre-hosted thumbnail URL |
|
|
| `thumbnail` | File | No | Thumbnail image upload (multipart only, max 50 MB) |
|
|
| `shotGroupName` | string | No | Shot group name — created automatically if it doesn't exist |
|
|
| `shotCode` | string | No | Explicit shot code — bypasses auto-generation, must be unique in the project |
|
|
|
|
### Response `201 Created`
|
|
|
|
```json
|
|
{
|
|
"shot": {
|
|
"id": "clxxxxxxxxxxxxxx",
|
|
"shotCode": "UNG_106_010_0010",
|
|
"scene": "010",
|
|
"episode": "106",
|
|
"shotNumber": 10,
|
|
"description": null,
|
|
"status": "WAITING",
|
|
"priority": "NORMAL",
|
|
"fps": 24,
|
|
"frameStart": null,
|
|
"frameEnd": null,
|
|
"dueDate": null,
|
|
"thumbnailUrl": null,
|
|
"projectId": "cmp6l5mzq0001ua0gz07bk72f",
|
|
"artistId": null,
|
|
"shotGroupId": null,
|
|
"createdAt": "2026-07-21T09:00:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error `409 Conflict`
|
|
|
|
Returned when an explicit `shotCode` already exists in the project:
|
|
|
|
```json
|
|
{ "error": "A shot with that code already exists in this project", "shotCode": "UNG_106_010_020" }
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Update Shot
|
|
|
|
```
|
|
PATCH /api/ext/shots/{shotId}
|
|
Content-Type: application/json
|
|
```
|
|
|
|
Used by pipeline tools to write back metadata after a render/upload.
|
|
|
|
### Path Parameters
|
|
|
|
| Parameter | Description |
|
|
|---|---|
|
|
| `shotId` | Database ID of the shot (from the lookup response `shot.id`) |
|
|
|
|
### Request Body
|
|
|
|
| Field | Type | Required | Validation | Description |
|
|
|---|---|---|---|---|
|
|
| `shotVersion` | string | Yes* | Must match `v###`, e.g. `v004` | Sets the current version string |
|
|
|
|
*At least one field must be provided; currently only `shotVersion` is supported.
|
|
|
|
### Example Request
|
|
|
|
```http
|
|
PATCH /api/ext/shots/clxxxxxxxxxxxxxx
|
|
Authorization: Bearer your-api-key
|
|
Content-Type: application/json
|
|
|
|
{ "shotVersion": "v004" }
|
|
```
|
|
|
|
### Response `200 OK`
|
|
|
|
```json
|
|
{ "success": true, "shot": { "id": "clxxxxxxxxxxxxxx", "shotVersion": "v004" } }
|
|
```
|
|
|
|
---
|
|
|
|
## Error Responses
|
|
|
|
| Status | Meaning |
|
|
|---|---|
|
|
| `400` | Missing required parameter |
|
|
| `401` | Missing or invalid API key |
|
|
| `403` | Forbidden (session-auth routes only) |
|
|
| `404` | Resource not found |
|
|
| `409` | Conflict (duplicate shot code) |
|
|
| `413` | Payload too large (thumbnail > 50 MB) |
|
|
| `422` | Validation error |
|
|
| `500` | Internal server error |
|
|
|
|
```json
|
|
{ "error": "shotCode query param is required" }
|
|
{ "error": "Validation error", "details": [ ... ] }
|
|
```
|
|
|
|
---
|
|
|
|
## After Effects Connector — Endpoints Used
|
|
|
|
For reference, the endpoints called by `VFXReviewConnector.jsx`:
|
|
|
|
| Action | Endpoint |
|
|
|---|---|
|
|
| Populate Episode dropdown | `GET /api/ext/projects/{PROJECT_CODE}/episodes` |
|
|
| Populate Shot dropdown | `GET /api/ext/projects/{PROJECT_CODE}/shots?episode={value}` |
|
|
| All shot operations (overlay, queue, preview, delivery) | `GET /api/ext/shots/lookup?shotCode={code}&projectCode={PROJECT_CODE}` |
|
|
| Increment Shot Version | `PATCH /api/ext/shots/{shot.id}` with `{ "shotVersion": "v00X" }` |
|
|
| Queue Export | `POST /api/ext/exports` (E1) |
|
|
| Refresh Status / poll | `GET /api/ext/exports/latest` (E2) |
|
|
| Retry Export | `POST /api/ext/exports/{id}/retry` (E14) |
|
|
| Regenerate Previews | `POST /api/ext/exports/{id}/regenerate-preview` |
|
|
| Load QC Queue | `GET /api/ext/qc/queue?projectCode={PROJECT_CODE}` (E5) |
|
|
| QC Pass / QC Fail | `POST /api/ext/exports/{id}/qc` (E4) |
|