Files
vfxreview/EXT_API_REFERENCE.md
T
twotalesanimation feb1c7b3e9 feat(render-pipeline): validation, QC and delivery builder
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>
2026-08-13 13:49:51 +02:00

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) |