Skip to content

Anuva Python Server System Description

Purpose

This document describes the Anuva Python Server as a system and defines the current expected worker-facing Anuva Web App API contract that the Python Server should call.

It is intended to be used as implementation context for Codex and engineers working on the communication boundary between:

  1. The Anuva Web App / Payload backend
  2. The Anuva Python Server worker
  3. The Unity Video Generator running as a Windows EXE or in Unity Editor Play Mode

This document is based on:

  1. docs/Anuva_Python_Server_Requirements.md
  2. The current worker code under src/anuva_python_server/webapp/
  3. The current simulator contract under src/anuva_python_server/simulator/

Where the final Payload route layout is not yet fixed, this document proposes a worker-facing API contract that matches the current Python worker model and can be implemented behind any Payload route structure.

1. System Overview

The Anuva Python Server is a single-job orchestration worker that runs beside Unity on a Windows machine or Windows GPU VM.

Its responsibilities are:

  1. Poll the Web App for render jobs or expose available jobs for manual claiming
  2. Atomically claim exactly one job at a time
  3. Download the render package and referenced assets
  4. Rewrite the package into a deterministic local workspace
  5. Serve local asset/config URLs to Unity over localhost HTTP/HTTPS
  6. Launch or connect to Unity
  7. Send render.start to Unity over ZeroMQ
  8. Translate Unity progress/failure/completion back into Web App job updates
  9. Validate the rendered MP4
  10. Upload the rendered MP4 and generated thumbnail metadata
  11. Mark the job completed, failed, cancelled, or upload-failed
  12. Emit health and diagnostic information for operators

The Python Server is not responsible for:

  1. Generating presentation content
  2. Compiling presentation semantics
  3. Making camera or scene decisions
  4. Polling or mutating arbitrary Web App domain objects beyond worker-facing render operations

2. Runtime Boundaries

2.1 Web App Responsibilities

The Web App owns:

  1. Presentations, compilations, and source media
  2. Render job creation and queueing
  3. Atomic claim semantics
  4. Signed upload issuance and upload confirmation
  5. Final media record creation
  6. Operator-visible render status as the source of truth

2.2 Python Server Responsibilities

The Python Server owns:

  1. Worker lifecycle and configuration
  2. Job acquisition mode: auto, manual, or hybrid
  3. Local workspace preparation
  4. Unity supervision and ZeroMQ integration
  5. Output validation and upload execution
  6. Rich failure diagnostics

2.3 Unity Responsibilities

Unity owns:

  1. Consuming the local presentation config URL
  2. Downloading referenced local assets over localhost HTTP/HTTPS
  3. Executing the actual render
  4. Emitting unity.ready, render.accepted, render.progress, render.completed, and render.failed

3. Core System Invariants

The Web App API should preserve these invariants because the current worker code assumes them:

  1. V1 workers process one active job at a time.
  2. A job is not safe to process until the claim endpoint succeeds.
  3. Manual claim and automatic claim must use the same backend claim rules.
  4. Worker-visible statuses include:
  5. queued
  6. claimed
  7. inProgress
  8. uploading
  9. uploadFailed
  10. completed
  11. failed
  12. cancelled
  13. Job progress is modeled as a float in the range 0.0 to 1.0.
  14. Asset filenames in the package must be safe basenames, not paths.
  15. Asset IDs must be unique within one job package.
  16. The Web App is the source of truth for cancellation and terminal job state.

4. Current Python Worker Interface

The worker currently expects a Web App client with this shape:

get_next_queued_job(worker_id: str) -> RenderJob | None
list_available_jobs(worker_id: str, filters: RenderJobListFilters | None = None) -> list[RenderJobSummary]
get_job_summary(job_id: str) -> RenderJobSummary
claim_job(job_id: str, worker_id: str) -> RenderJob
manual_claim_job(job_id: str, worker_id: str) -> RenderJob
get_job_package(job_id: str) -> RenderJobPackage
update_job_status(job_id: str, status: str, progress: float | None = None, message: str | None = None) -> None
send_worker_heartbeat(worker_id: str, payload: WorkerHeartbeat) -> None
upload_render_result(job_id: str, file_path: Path, metadata: dict[str, Any]) -> UploadedMedia
create_signed_upload(job_id: str, filename: str, content_type: str, file_size_bytes: int, metadata: dict[str, Any]) -> dict[str, Any]
confirm_signed_upload(job_id: str, upload: dict[str, Any], metadata: dict[str, Any]) -> UploadedMedia
complete_job(job_id: str, result_media_id: str) -> None
fail_job(job_id: str, error_code: str, error_message: str, diagnostics: dict[str, Any] | None = None) -> None
cancel_job(job_id: str, reason: str) -> None

This is the most important contract to preserve.

The current code defaults to /api/worker/v1 when webapp.mode is real, and to /sim/... when webapp.mode is simulated. Those paths remain configurable through webapp.endpoints if the contract evolves again.

Recommended production namespace:

/api/worker/v1

Suggested mapping:

Operation Simulator default client path Real Web App default client path
Get next queued job GET /sim/jobs/available?workerId=... GET /api/worker/v1/jobs/next?workerId=...
List available jobs GET /sim/jobs/available GET /api/worker/v1/jobs/available
Get job summary GET /sim/jobs/{jobId} GET /api/worker/v1/jobs/{jobId}
Claim job POST /sim/jobs/{jobId}/claim POST /api/worker/v1/jobs/{jobId}/claim
Get package GET /sim/jobs/{jobId}/package GET /api/worker/v1/jobs/{jobId}/package
Update status PATCH /sim/jobs/{jobId}/status PATCH /api/worker/v1/jobs/{jobId}/status
Worker heartbeat POST /sim/workers/{workerId}/heartbeat POST /api/worker/v1/workers/{workerId}/heartbeat
Direct upload POST /sim/jobs/{jobId}/upload POST /api/worker/v1/jobs/{jobId}/upload
Create signed upload POST /sim/jobs/{jobId}/uploads/signed POST /api/worker/v1/jobs/{jobId}/uploads/signed
Confirm signed upload POST /sim/jobs/{jobId}/uploads/confirm POST /api/worker/v1/jobs/{jobId}/uploads/confirm
Complete job POST /sim/jobs/{jobId}/complete POST /api/worker/v1/jobs/{jobId}/complete
Fail job POST /sim/jobs/{jobId}/fail POST /api/worker/v1/jobs/{jobId}/fail
Cancel job POST /sim/jobs/{jobId}/cancel POST /api/worker/v1/jobs/{jobId}/cancel

6. Authentication and Headers

The current worker client sends:

Accept: application/json
Authorization: Bearer <worker-api-token>

The Web App API should therefore support:

  1. A worker-scoped bearer token from ANUVA_WORKER_API_TOKEN
  2. JSON request and response bodies for all metadata endpoints
  3. Multipart file upload only for the direct Payload upload fallback endpoint

The worker will not persist secrets in YAML config; it expects the token to come from environment variables.

7. Data Structures

All examples below use the current worker field names and JSON aliases.

7.1 RenderJob

This is the full worker-facing job object returned by claim operations and sometimes by "next job" operations.

{
  "jobId": "job_001",
  "workspaceId": "workspace_001",
  "presentationId": "presentation_001",
  "compilationId": "compilation_001",
  "jobType": "finalRender",
  "priority": "standard",
  "status": "claimed",
  "progress": 0.0,
  "message": "claimed by worker",
  "workerId": "unity-worker-01",
  "claimedAt": "2026-05-21T10:00:00Z",
  "updatedAt": "2026-05-21T10:00:00Z",
  "errorCode": null,
  "errorMessage": null,
  "resultMediaId": null
}

Notes:

  1. jobType is currently preview or finalRender.
  2. priority is currently standard, priority, or fastest.
  3. progress should be 0.0..1.0.
  4. updatedAt should always be present.

7.2 RenderJobSummary

This is the safe operator-facing job listing shape used by the dashboard/manual claim flow.

{
  "jobId": "job_001",
  "workspaceId": "workspace_001",
  "presentationId": "presentation_001",
  "compilationId": "compilation_001",
  "jobType": "finalRender",
  "priority": "standard",
  "status": "queued",
  "title": "Q2 Product Launch",
  "createdAt": "2026-05-21T09:55:00Z",
  "updatedAt": "2026-05-21T09:55:00Z",
  "estimatedDurationSeconds": 75,
  "assetCount": 14
}

This is intentionally smaller than the full package and should avoid leaking:

  1. Signed asset URLs
  2. Raw compiled presentation config
  3. Unnecessary private user data

7.3 RenderJobListFilters

Current worker-supported filters:

{
  "jobType": "finalRender",
  "priority": "priority",
  "workspaceId": "workspace_001",
  "limit": 25
}

The Web App does not need to support every filter immediately, but it should safely ignore unsupported optional filters or document its supported subset.

7.4 RenderJobPackage

This is the most important payload. It is what the worker downloads after a successful claim.

{
  "jobId": "job_001",
  "workspaceId": "workspace_001",
  "presentationId": "presentation_001",
  "compilationId": "compilation_001",
  "jobType": "finalRender",
  "priority": "standard",
  "config": {
    "presentationId": "presentation_001",
    "scenes": [],
    "renderHints": {}
  },
  "assets": [
    {
      "assetId": "asset_img_001",
      "kind": "image",
      "url": "https://signed-or-public-url.example.com/file.png",
      "filename": "slide-01.png",
      "mimeType": "image/png",
      "checksum": "sha256:abc123",
      "required": true
    }
  ],
  "render": {
    "resolution": "1080p",
    "frameRate": 24,
    "aspectRatio": "16:9",
    "format": "mp4",
    "watermark": false
  }
}

Contract details:

  1. config is opaque to the worker except for local rewriting and delivery to Unity.
  2. assets[].url may be signed or otherwise protected.
  3. assets[].filename must be a safe basename.
  4. assets[].assetId must be unique within the package.
  5. render.format is currently expected to be mp4.

7.5 WorkerHeartbeat

Current worker heartbeat shape:

{
  "status": "idle",
  "activeJobId": null,
  "details": {
    "workerId": "unity-worker-01",
    "mode": "exe",
    "unityReady": true
  }
}

details is intentionally flexible. The Web App should treat it as an extensible object and store it without requiring a rigid schema.

7.6 UploadedMedia

This is the normalized media record returned to the worker after upload finalization.

{
  "mediaId": "media_001",
  "url": "https://cdn.example.com/renders/presentation_001_final.mp4",
  "filename": "presentation_001_final.mp4",
  "mimeType": "video/mp4",
  "fileSizeBytes": 184000000,
  "metadata": {
    "jobId": "job_001",
    "workerId": "unity-worker-01"
  }
}

The worker uses mediaId when calling complete_job.

8. Endpoint Contract Details

8.1 Get Next Queued Job

GET /api/worker/v1/jobs/next?workerId=unity-worker-01

Response:

  1. 200 OK with a RenderJob
  2. 200 OK with null
  3. 204 No Content

The current Python client also tolerates a list response and picks the first item, but a single object or null is cleaner for production.

8.2 List Available Jobs

GET /api/worker/v1/jobs/available?workerId=unity-worker-01&jobType=finalRender&limit=25

Response: RenderJobSummary[]

This endpoint supports the local dashboard and manual claim flows.

8.3 Get Job Summary

GET /api/worker/v1/jobs/{jobId}

Response: RenderJobSummary

The worker currently uses this endpoint mainly to detect cancellation of the active job.

8.4 Claim Job

POST /api/worker/v1/jobs/{jobId}/claim
Content-Type: application/json

{
  "workerId": "unity-worker-01"
}

Response: RenderJob

Rules:

  1. Claim must be atomic.
  2. Only queued jobs are claimable.
  3. If another worker already claimed the job, return 409 Conflict.

Recommended conflict response:

{
  "error": "job_already_claimed",
  "message": "Job was already claimed by another worker"
}

8.5 Get Job Package

GET /api/worker/v1/jobs/{jobId}/package

Response: RenderJobPackage

This must be available after claim. It should represent the exact package to render and should not depend on additional hidden state in the worker.

8.6 Update Job Status

PATCH /api/worker/v1/jobs/{jobId}/status
Content-Type: application/json

{
  "status": "inProgress",
  "progress": 0.42,
  "message": "Unity render in progress"
}

Rules:

  1. progress is optional.
  2. message is optional.
  3. The Web App should validate transitions or at least reject obviously invalid terminal rewrites.

Recommended accepted statuses:

  1. claimed
  2. inProgress
  3. uploading
  4. uploadFailed
  5. completed
  6. failed
  7. cancelled

8.7 Worker Heartbeat

POST /api/worker/v1/workers/{workerId}/heartbeat
Content-Type: application/json

{
  "status": "rendering",
  "activeJobId": "job_001",
  "details": {
    "unityConnected": true,
    "unityMode": "editor",
    "dashboardReachable": true
  }
}

Response:

{
  "status": "ok"
}

This endpoint should be lightweight and safe to call frequently.

8.8 Direct Payload Upload Fallback

POST /api/worker/v1/jobs/{jobId}/upload
Content-Type: multipart/form-data

Parts:

  1. file: the MP4 bytes
  2. metadata: JSON string

Example metadata:

{
  "jobId": "job_001",
  "workerId": "unity-worker-01",
  "renderStartedAt": "2026-05-21T10:00:00Z",
  "renderCompletedAt": "2026-05-21T10:05:00Z",
  "durationSec": 72.4,
  "frameCount": 1737,
  "fileSizeBytes": 184000000,
  "thumbnailPath": "C:\\Anuva\\Worker\\jobs\\job_001\\output\\thumbnail.png"
}

Response: UploadedMedia

8.9 Preferred Signed S3 Upload Flow

This is the current desired production flow.

Step 1: Create Signed Upload

POST /api/worker/v1/jobs/{jobId}/uploads/signed
Content-Type: application/json

{
  "filename": "presentation_001_final.mp4",
  "contentType": "video/mp4",
  "fileSizeBytes": 184000000,
  "metadata": {
    "jobId": "job_001",
    "workerId": "unity-worker-01"
  }
}

Recommended response:

{
  "uploadUrl": "https://s3-presigned-url.example.com/...",
  "headers": {
    "Content-Type": "video/mp4"
  },
  "storageKey": "renders/job_001/presentation_001_final.mp4",
  "uploadId": "upload_001"
}

The Python worker already expects at least uploadUrl and optional headers.

Step 2: Worker uploads bytes directly to S3

The worker does a raw PUT to uploadUrl.

Step 3: Confirm Signed Upload

POST /api/worker/v1/jobs/{jobId}/uploads/confirm
Content-Type: application/json

{
  "upload": {
    "uploadUrl": "https://s3-presigned-url.example.com/...",
    "headers": {
      "Content-Type": "video/mp4"
    },
    "storageKey": "renders/job_001/presentation_001_final.mp4",
    "uploadId": "upload_001"
  },
  "metadata": {
    "jobId": "job_001",
    "workerId": "unity-worker-01"
  }
}

Response: UploadedMedia

The Web App should create or resolve the final media record during this confirmation step.

8.10 Complete Job

POST /api/worker/v1/jobs/{jobId}/complete
Content-Type: application/json

{
  "resultMediaId": "media_001"
}

Behavior:

  1. Transition the job to completed
  2. Associate the uploaded media with the render job
  3. Update presentation-level pointers such as latest preview/final media where applicable

8.11 Fail Job

POST /api/worker/v1/jobs/{jobId}/fail
Content-Type: application/json

{
  "errorCode": "UNITY_RENDER_TIMEOUT",
  "errorMessage": "Unity render exceeded 1800s timeout",
  "diagnostics": {
    "workerId": "unity-worker-01",
    "jobId": "job_001",
    "state": "rendering",
    "retrySafe": true,
    "lastUnityEvent": {
      "type": "render.progress"
    },
    "logPaths": [
      "C:\\Anuva\\Worker\\jobs\\job_001\\logs\\python_job.log",
      "C:\\Anuva\\Worker\\jobs\\job_001\\logs\\unity_job.log",
      "C:\\Anuva\\Worker\\jobs\\job_001\\logs\\zmq_messages.jsonl"
    ],
    "diagnosticsPath": "C:\\Anuva\\Worker\\jobs\\job_001\\diagnostics"
  }
}

Important notes:

  1. The worker already classifies some failures as retry-safe.
  2. The diagnostics object is intentionally flexible and should be stored, not heavily normalized at the API boundary.
  3. Signed URLs and secrets should never be echoed back in diagnostics.

8.12 Cancel Job

POST /api/worker/v1/jobs/{jobId}/cancel
Content-Type: application/json

{
  "reason": "cancelled_by_webapp"
}

The worker may also discover cancellation by polling GET /jobs/{jobId} and seeing status == cancelled.

9. Status and Transition Semantics

Recommended valid transitions:

queued -> claimed
queued -> cancelled
claimed -> inProgress
claimed -> failed
claimed -> cancelled
inProgress -> uploading
inProgress -> uploadFailed
inProgress -> failed
inProgress -> cancelled
uploading -> completed
uploading -> uploadFailed
uploading -> failed
uploading -> cancelled
uploadFailed -> uploading
uploadFailed -> failed
uploadFailed -> cancelled

Terminal statuses:

  1. completed
  2. failed
  3. cancelled

The worker already expects uploadFailed to exist as a first-class state.

10. End-to-End Call Sequences

10.1 Automatic Polling Flow

  1. Worker calls GET /jobs/next
  2. Worker calls POST /jobs/{jobId}/claim
  3. Worker calls GET /jobs/{jobId}/package
  4. Worker downloads package assets from asset URLs
  5. Worker calls PATCH /jobs/{jobId}/status with inProgress
  6. Worker renders through Unity and sends more PATCH /status calls
  7. Worker creates signed upload or uses direct upload
  8. Worker uploads MP4
  9. Worker calls POST /jobs/{jobId}/complete

10.2 Manual Claim Flow

  1. Dashboard asks worker to list jobs
  2. Worker calls GET /jobs/available
  3. Operator selects a job
  4. Worker calls POST /jobs/{jobId}/claim
  5. Remaining lifecycle is identical to automatic mode

10.3 Cancellation Flow

  1. Web App marks job cancelled or exposes cancellation through summary lookup
  2. Worker notices cancellation while idle or rendering
  3. Worker sends render.cancel to Unity if needed
  4. Worker calls POST /jobs/{jobId}/cancel if it is the actor finalizing local cancellation

11. Asset Delivery Expectations

The current system direction is:

  1. The Web App gives the worker a package containing asset URLs
  2. The worker downloads those assets locally
  3. The worker rewrites the config so Unity consumes localhost HTTP/HTTPS URLs
  4. Unity should not depend on the original remote URLs during render

This means the Web App package should be stable enough for the worker to fully materialize the job offline except for the final upload.

12. Error Handling Expectations

Recommended API behavior:

  1. 401 or 403 for invalid worker auth
  2. 404 for unknown jobs or packages
  3. 409 for claim conflicts or invalid state transitions
  4. 422 for malformed request payloads
  5. 503 for temporary worker-facing backend outages

The worker already treats these categories as operationally meaningful, especially:

  1. Web App unavailable
  2. Claim conflict
  3. Invalid job package
  4. Upload failure
  5. Cancellation

13. Payload / Implementation Notes for the Web App Team

The production Web App does not have to match the Python simulator's route names, but it should preserve these behaviors:

  1. A thin worker-facing API should exist even if internal Payload collections are more complex.
  2. Claim logic should be server-side and atomic.
  3. The job package response should be fully renderable by the worker without extra implicit fetches.
  4. The upload confirmation endpoint should return a normalized media object with mediaId.
  5. The summary endpoint should be cheap enough to poll for cancellation.
  6. The API should tolerate extensible details and diagnostics objects.

If the Payload backend wants different route names, the Python worker can adapt through webapp.endpoints config as long as the operation semantics stay the same.

If the Web App team wants the smallest useful first implementation, this is the minimal production-safe set:

  1. GET /api/worker/v1/jobs/next
  2. GET /api/worker/v1/jobs/available
  3. GET /api/worker/v1/jobs/{jobId}
  4. POST /api/worker/v1/jobs/{jobId}/claim
  5. GET /api/worker/v1/jobs/{jobId}/package
  6. PATCH /api/worker/v1/jobs/{jobId}/status
  7. POST /api/worker/v1/workers/{workerId}/heartbeat
  8. POST /api/worker/v1/jobs/{jobId}/uploads/signed
  9. POST /api/worker/v1/jobs/{jobId}/uploads/confirm
  10. POST /api/worker/v1/jobs/{jobId}/complete
  11. POST /api/worker/v1/jobs/{jobId}/fail
  12. POST /api/worker/v1/jobs/{jobId}/cancel

This set matches the current Python worker architecture cleanly and supports:

  1. Automatic polling
  2. Manual dashboard claiming
  3. Unity progress reporting
  4. Direct-to-S3 upload
  5. Failure diagnostics
  6. Cancellation handling

15. Codex-Oriented Summary

If Codex is generating code for either side of this boundary, the key assumptions should be:

  1. The Python worker is a deterministic single-job orchestration service.
  2. The worker communicates with the Web App through a dedicated client abstraction.
  3. The Web App should expose a worker-specific render-job API, not generic CMS objects directly.
  4. The most important objects are RenderJob, RenderJobSummary, RenderJobPackage, WorkerHeartbeat, and UploadedMedia.
  5. Claim must be atomic, package retrieval must be explicit, and upload finalization must return a mediaId.
  6. The preferred upload flow is signed S3, with direct multipart upload retained only as a fallback.
  7. uploadFailed is a first-class job state and should be implemented.