724b0cbefeb19ae0cfafd5a61b396cf62ca0a5dd
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
400cf3f6ea |
feat(validation): try the comp's other delivered plates before failing timecode
When the timing plate disagrees with the render, the worker now checks the manifest's other plates[] that sit under a plate search root (or beside the timing plate's footage folder). A match is a PASS naming that element; no match still FAILs against the timing plate, listing the alternates tried. Pre-renders are excluded because they agree with the render by construction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a6ee1917e7 |
fix(connector): cancel the export poll when the panel closes or reloads
Deploy / deploy (push) Successful in 6m46s
app.scheduleTask outlives the panel instance that armed it. The repeating task id was held in the pipelineState closure, so closing or reloading the panel took the id with it and left nothing able to cancel the task: it kept firing every 15 s for the rest of the AE session, and every tick landing while a modal dialog was open raised "Cannot run a script while a modal dialog is waiting for response." The id now lives in global scope beside the tick function that scheduleTask already evaluates there, so the next panel instance can still reach it. getPollTaskId and setPollTaskId wrap the access and treat anything that is not a positive number as no task. Panel startup cancels whatever a previous instance left armed before pointing the tick at itself, rather than letting the old task fire into a fresh, empty state whose shot code went with the closure that armed it, and panel.onClose stops the poll on the way out. The export is unaffected either way: it runs on the farm, and the header picks its status back up from the server on the next Refresh. Section 12.1 of RenderPipeline2.md records that. PANEL_VERSION goes to 2.6.1 per the documented rule, so Pipeline → Machines does not show two workstations on one version behaving differently. This change was authored in the working tree, not by Claude; it is committed here alongside the status and client-portal work at the author's request, and the message was written from reading the diff. It has not been exercised in a live After Effects session as part of this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6678721fb1 |
fix(pipeline): create the comp task an export needs when a shot has none
Deploy / deploy (push) Successful in 6m48s
A shot with no COMP task rendered and previewed fine and then showed
nothing in the app. createExport set Export.taskId from the shot's first
COMP task or null, and finalizePreview registered the review Version with
that same null taskId — so the MP4 was uploaded and stored, but the
Version hung off no task and none of the review flows had anywhere to
draw it. From the artist's side the export completed and produced
nothing.
E1 now resolves the shot's comp task inside the version-allocation
transaction and creates one when there isn't any, titled after the
preview file the export will produce: {shotCode}_cmp_TT_{version}, the
same convention the manual batch upload's create-task path already uses.
renderOutputBase() is exported and buildPreviewManifest takes its `base`
from it, so the task title and the MOV/MP4 name come from one function
rather than two copies of the same template. The title therefore already
carries the version, which makes finalizePreview's rename-to-the-
delivered-version a no-op on that first export rather than an immediate
rename of a task created moments earlier.
The lookup and the create sit inside the transaction so the P2002
version-collision retry rolls a losing attempt's task back with
everything else, instead of leaving a duplicate behind.
recalcShotStatus runs only when a task was actually created: a shot with
no tasks derives WAITING and now has one. Approval-driven statuses still
take precedence, so an already-approved shot stays COMPLETE.
Both the queue-time ExportEvent note and the E1 response record what
happened, so a task appearing under a shot on its own is traceable to the
export that made it.
Exports already in flight with a null taskId keep their orphaned
versions; backfilling those is a separate migration.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7f066c1372 |
feat(pipeline): MOV review delivery packages from selected shots
Deploy / deploy (push) Successful in 7m28s
A formal review is shared as a flat, dated folder of the preview MOVs —
{deliveryRoot}/PRODUCER_REVIEWS/20260828_Producer_review, then _02, _03
for further reviews the same day. It reuses the delivery package's
machinery end to end: DeliveryPackage/DeliveryItem, the DELIVERY_BUILD
job, the same claim path and the same hardlink-or-copy builder, with
DeliveryPackage.kind telling the two apart.
Three things separate a review from a delivery:
A review never advances an Export. Reviews happen throughout a shot's
life, long before QC passes it, and the same version is routinely shown
twice — so nothing is marked PACKAGED, and markDelivered() refuses a
review package outright (the button is hidden too).
Selection is by shot, not by export. Shot Status → Bulk Actions → Build
MOV Review Package; the server resolves each shot to its newest export
that actually has a deliveryMovPath, which is not always its newest
export — a shot whose latest version is still rendering belongs in the
review as the version that can be watched. Shots with no MOV at all are
listed in the confirm dialog with the reason rather than silently
shrinking the folder.
The folder is exactly the MOVs. No CSV, no manifest.json written beside
them; the manifest still returns to the server, so the record of what was
shared is kept without cluttering the share. Names follow the same
client-naming rule as a delivery, so a shot reads identically in a review
folder and in the delivery that follows it.
Verification (DeliveryStage.BuildReviewItem) is proportional as it is for
EXRs, adapted to a single file with no validation-time digest: a hard
link is the source and needs nothing, a copy is checked against its
source by length always and by full hash under "always-hash". A missing
MOV fails the item here — in an EXR delivery the MOV is a viewing copy
beside the real deliverable, but in a review it is the deliverable, and a
review folder quietly one shot short is worse than one that says so.
Worker bumped to 1.6.0 for the new manifest field (DeliveryManifest.Kind).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
caaa333f02 |
feat(pipeline): render-derived shot thumbnails, and bulk shot delete
Deploy / deploy (push) Failing after 19m11s
Two independent features landing together: Generated shot thumbnails — a shot can now carry a thumbnail promoted from a render export (Shot.generatedThumbnailUrl/generatedThumbnailFrom) alongside the existing brief-supplied thumbnailUrl, which always takes display precedence (lib/shot-thumbnail.ts) so a promoted frame can only ever fill a gap, never overwrite the brief. Shows with pipelineConfig.autoShotThumbnail promote automatically on every finished export (lib/render-pipeline/preview.ts); promoteExportThumbnail/ clearGeneratedThumbnail (actions/shots.ts) are the manual equivalent for everyone else. Worker-side seek logic for picking the promoted frame is covered by RenderWorker/VFXReviewWorker.Tests/ThumbnailSeekTests.cs. Bulk shot delete — the shot-status page's selection bar gets a "Bulk Actions" dropdown (alongside the existing Change Due Date) with Delete Shots. bulkDeleteShots() (actions/shots.ts) deletes one at a time rather than in a single statement: exports_shotId_fkey is ON DELETE RESTRICT by design, so a shot with renders or deliveries on record is protected and gets skipped and reported by name rather than aborting the whole selection or being silently destroyed along with its history. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
53fc8dc65b |
fix(pipeline): re-exports keep a shot's approval, and carry no client-facing text
Deploy / deploy (push) Successful in 6m1s
A shot is routinely re-exported for delivery after it has already been client-approved — most often an unscaled/un-punched-in pass. Every pipeline-created Version defaulted to approvalStatus PENDING_REVIEW, which is exactly what the dashboard counts, the shot page badge and the playlist read as "awaiting review" — so a re-export made an approved shot look like new unreviewed work to the whole team, with nothing actually different about its approval. approvalStatus is now inherited from the task's previous latest version rather than left at the create default. A task with no prior version (its first export) still gets PENDING_REVIEW, which is correct. Separately, every pipeline version was stamped with notes: "Pipeline render vXXX (internal only)" — and notes is the artist's message to reviewers, rendered verbatim in the client portal and the review player. Nothing enforces that a pipeline version stays unshared once created (see the gap noted in RenderPipeline2.md), so that internal-sounding string could reach a client if the shot were re-shared after a re-export. notes is left unset; what a version is and where it came from belongs on the Export and its timeline, which is where a human would actually go looking. RenderPipeline2.md §9 documents both, plus the share-flow gap that let notes reach a client in the first place: shareWithClient promotes whichever Version is isLatest with no awareness that a pipeline version is meant to stay internal permanently. Not fixed here — closing it needs a way to identify a pipeline-origin version at the share call sites, and Export.versionId is currently a one-way scalar with no inverse relation on Version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
483db65eb3 |
fix(connector): build shots at the shot's frame rate, not a hardcoded 24
Deploy / deploy (push) Successful in 6m23s
Shot.fps existed, the lookup API returned it, and one button honoured it — Set Start Timecode — while the shot builder ignored it entirely. Comps were created at 24 and every plate conformed to 24. DH2 is a 25 fps show. Its plates carry framesPerSecond 25/1, so comps built at 24 conform every plate to the wrong speed and compute every frame-from-timecode sum against the wrong rate. The builder now takes the rate from the shot record, which is the show's delivery rate and therefore the authority; a plate's own rate is whatever happens to be on disk. Import Renders follows the comp it imports into, and Import EXR follows the shot. Validation gains plate_fps, comparing the render against the timing plate. timecode_fps only compares the render to its manifest and both come from the comp, so a shot built at the wrong rate agrees with itself and passes; the plate is the independent witness. It is a WARN, never a FAIL: sampling DH2 found one plate at 50 fps against a 25 fps show, a deliberate over-crank, so a whole-number multiple is reported as exactly that and anything else as a probable rate mistake. Note this does not fix the data: all 175 DH2 shots still carry fps 24 from the create-time default and need correcting before their comps are rebuilt. Panel 2.4.0, worker 1.4.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c9b0f25bb2 |
fix(pipeline): let a finished render's preview outrank pending renders
Deploy / deploy (push) Successful in 6m11s
Urgent already propagated — a preview inherits its render's priority, so an urgent export stays ahead all the way to QC. The complaint was the other half: at equal priority previews always ran last. The queue orders by priority then age, and a preview job is only created when its render finishes, so it is always newer than the renders queued beside it. Ten shots ran as ten renders and then ten previews, and nothing was reviewable until the end of the batch. render.previewPriorityBoost (default 1) subtracts from the render's priority when the preview is created, so a shot finishes end to end before the next render starts. The boost never promotes a job across the urgent threshold: a normal preview must not inherit permission to run outside the render window that its render never had. Set it to 0 for the old behaviour. Worth knowing separately: a worker will not claim preview jobs at all while an artist has AE open on that machine, which during the day is the larger reason previews appear to wait. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4642ecb11f |
feat(pipeline): skip the preview build and inherit the previous one
Deploy / deploy (push) Successful in 7m8s
Some exports are deliberately framed differently from their review — unscaled or un-punched-in EXRs — and a preview rebuilt from those frames would be the wrong picture. The previous export's MOV is the correct one. skipPreview renders the EXRs and goes straight to READY_FOR_QC. Available at Queue Export (a "No preview" checkbox in the panel, E1 field) and as an option when overriding a validation failure. The inheritance is as important as the skip: deliveryMovPath, the review MP4, the thumbnail and the Version are carried over from the newest earlier export of that shot which has them. Without it the delivery builder — which takes its MOV from deliveryMovPath — would package EXRs with no MOV at all. Where there is nothing to inherit, the export still reaches QC and the timeline says it has no review media. QC is untouched by any of this: a skipped preview still needs a human pass before the export can be delivered. Panel 2.3.0. Adds migration 20260814000000_export_skip_preview. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e66e4868ea |
feat(validation): manual override for a failed run
Deploy / deploy (push) Successful in 6m13s
Checks fail on renders that are genuinely fine — a plate the engine cannot reach, an expectation that does not apply to this shot — and a VALIDATION_FAILED export was a dead end, blocking delivery on a shot a human can see is correct. Override validation on the Export detail page resumes the pipeline where a pass would have handed off: preview, then READY_FOR_QC. It never skips QC and never delivers directly, so the frames are still looked at — the override waives the automated check, not the human one. The failed ValidationResult rows are left untouched rather than rewritten, so the QC queue keeps listing them as warnings and whoever QCs the shot sees what was waved through. The reason is required and recorded with the checks it covered on the export's timeline, and the validation panel keeps an "overridden" badge long after the event scrolls away. Restricted to admin/producer/supervisor: waving a failed check through is a supervisor's call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
991abe072c |
fix(validation): anchor the timecode check on a delivered plate
Deploy / deploy (push) Successful in 6m12s
UNG_110_030_010 v002 failed with a 461614-frame offset on a render whose timecode was correct. Two causes, both in how the plate reference was recorded. The comp held three same-length elements and the timing plate was picked by duration alone, so a PRERENDER won on layer order. Using a pre-render as the timecode reference is circular anyway: it was rendered from this comp, so it can only ever agree with it — including when both are wrong. Only footage under the Footage Root is now eligible; among those, longest still wins. The offset itself came from the plate's frameStart. AE writes renders as "..._PRERENDER_01001.exr" while delivered plates use "....01001.exr", and the parser only handled the dot form, so the number fell back to the comp's frame range — 462615 against a render numbered from 1001. Both separators now parse, and an unparseable name omits the field rather than filling it with a number from the wrong coordinate system. Verified against the real manifest: the BG01 plate is now chosen over both pre-renders, and all three filenames parse to 1001. Panel 2.2.1, worker 1.2.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
37f73a2bd7 |
fix(validation): separate output file numbering from comp frame numbers
Deploy / deploy (push) Successful in 6m13s
A render carries frame numbers in two unrelated systems. The comp is anchored to its plate's timecode, so its timeline runs 1247792-1247896; the output module names files from 1001. Validation compared the file numbers against the comp range and failed every correctly-rendered shot by ~1.2 million frames — first_frame, last_frame and missing_frames all reporting a defect that was not there. The manifest now carries outputFrameStart (the output module's starting number, house convention 1001) alongside the comp range. Count and contiguity are measured on the output numbering; first/last compare against outputFrameStart and are SKIPPED on older manifests, where the count and gap checks still hold on their own. The same conflation broke the API timecode fallback: it anchored on the comp's frameStart while the render's first frame is 1001, producing a +1246791 frame "offset". Shot.timecodeStart is the edit in-point and the plate carries handleFrames of head handle, so the reference is the API value less handles, anchored on the render's own first frame. Verified on the shot that reported it: UNG_107_051_010 v004 now passes count, contiguity and a plate-anchored timecode match. Regression tests cover both coordinate systems, including that a genuine +8 handle offset still fails. Panel 2.1.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
cc89415a29 |
feat(pipeline): render queue, worker service and automated preview generation
One "Queue Export" click now renders the EXR sequence, then rebuilds the shot headlessly with the studio slate/overlay template to produce the delivery MOV and review MP4. Implements RenderPipeline2 phases 1-2 plus the preview stage. Server: - New models Export, RenderJob, ExportEvent, Machine, WorkerHeartbeat, plus Project.deliveryConfig and per-submission slate fields (Export.vfxScope, Export.submissionNote, inherited from the shot's previous export). Both migrations are purely additive; no existing column is touched. - lib/render-pipeline: server-enforced state machine, transactional version increment with supersede, atomic FOR UPDATE SKIP LOCKED claim gated by machine availability windows, and a lease reaper run from instrumentation.ts. - /api/ext/* endpoints for the panel and workers; session-auth mirrors under /api/render and /api/machines for the web UI. - Pipeline pages: render queue, export detail, machine monitoring, plus an Exports tab on shot detail. RenderWorker (.NET 8 Windows service, new): - Registration, heartbeat as cancel channel, claim loop, aerender runner with progress parsing and stall watchdog, crash recovery and disk-spooled reporting that survives server downtime. - Preview stage: headless AE assembles the preview comp into a throwaway AEP with both output modules queued, then a single aerender pass renders them. Preview jobs are not claimed while an interactive AE session is open, so an artist's project is never taken over. AE panel: Queue Export with live status polling, urgent flag, retry, and the VFX Scope / Submission Note fields. Every existing panel action is unchanged. Preview chaining ships disabled behind SystemConfig preview.enabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |