Unprioritized → Backlog → Sprints → To Do — and this board runs the work
858
flagged
Today's Focus Priorities(Top 3 • Drag left↔right to reorder)
Live Task Maps
Unassigned focus priority slot
Unassigned focus priority slot
Lanes
TODO Farm
0
Hit Farm todos to mine proposals from the todo history.
Unprioritized
33
#3158Cookbook recipe rot: 12 manifests that fail validation, name missing modules, or share step ordersneeds detailChoreclaude
·3 months ago
tests/test_cookbook_recipes_load.py (added alongside #3115) walks every recipe manifest in the cookbook and validates it. It found 12 pre-existing defects, frozen into three allowlists in that file so the guard could ship green. This ticket is to burn that list down; it only ever shrinks, and a NEW recipe cannot join it without editing the file.
WHY THESE MATTER. Nothing validates a recipe until the app asks for it, so each of these is live and silent. The reason the guard exists at all is that a bad step_type ("create", not in StepType) took down the whole dsn publish recipe and 500'd the version view - no test noticed, because no test loaded a recipe.
== 1. CANNOT HYDRATE AT ALL (1) ==
alchemy/shelves/lra/lra.json - `steps` is a dict, not a list. This recipe fails Recipe.model_validate outright, so the lra shelf 500s exactly as the dsn publish did. Highest priority: it is broken right now, not merely rotten.
== 2. STEPS NAMING MODULES THAT DO NOT EXIST (7) ==
Each will ImportError the moment somebody presses Run All. Mostly renames or never-written steps left behind in the manifest.
alchemy/ingest/tex/tex.json GetManifestVersionData, CopyFiles, CreateReviewMedia, CreateSgVersion, CreateMsd
blender/tasks/snd/snd.json task
maya/publish/rlo/rlo.json RemoveUnneededCameras, RenameShotCamera, SetStartFrame, RemoveUnneededCharacters, RenderShot, OrganizeScene, ExportUsd, PublishLayout, PublishShotCamera (all nine - this recipe looks entirely unimplemented)
maya/review/anm/anm.json ChooseAnimationType, CheckForRenders, CreateJpgs, CrearteQuicktime (note the typo in the last one - likely why it was never found)
maya/shelves/rig/rig.json Toola, Controls ("Toola" also looks like a typo)
maya/tasks/dsn/dsn.json task
unreal/render/lgt/lgt.json SeuqencerSelected, SetRenderSettings, SetOutputPath, AddToRenderQueue (module path says unreal.pre_render.lgt but the recipe lives at unreal/render/lgt - the folder was renamed and the modules were not)
For each: either write the step, correct the module path, or delete the entry. Deleting is a fine answer - a manifest entry for work nobody did is worse than no entry.
== 3. STEPS SHARING AN ORDER (4) ==
Which one runs first is whatever the sort happens to do. Harmless until it isn't - dsn publish depends on its order being exact (the sheet must compose AFTER the images are copied and the register written).
alchemy/build/rig/rig.json two steps at order 1
alchemy/build/scr/scr.json two steps at order 1
alchemy/context-menus/thumbnail/thumbnail.json two steps at order 3
alchemy/publish/lra/lra.json two steps at order 1
== DONE WHEN ==
The three allowlists in tests/test_cookbook_recipes_load.py are empty and the xfail branches are removed. Each entry cleared individually is a real fix; there is no need to do them in one pass.
#3839we hvae great hover info on the creative tasks on the timeline - on the code/features side of the timeline i'd like to see information like: link to original ticket, ticket created time, PR Merged time and the time between them just like we have the time between phases on the creative timeline items.needs detailNew featureTomVoice Over
·App · code_editor·Alchemy·2 months ago
#4208if i click on a scene that has a script published i should be able to open it in the script editor, we may have to pull scene data from the published script but this should be possible.needs detailNew featureOpen Published ScriptTomScript
·App · code_editor·Alchemy·2 months ago
#4229local voices don't show up in "my voices".needs detailBug fixTomVoice Over
·App · voices·Alchemy·2 months ago
#4231Pre-existing test failure: test_the_first_section_has_no_boundary_control finds no section nudge controlsreadyBug fixclaudeScript
·App · transcript·Alchemy·2 months ago
tests/test_script_template_ui.py::test_the_first_section_has_no_boundary_control fails on sprint/pipeline-videos-2026-08-24 independent of working-tree changes (verified with the tree stashed). The test sets template pas_v and expects nudge_section_start controls for sections after the first, but the rendered rows HTML contains none (the regex finds an empty list). Either the nudge control markup moved/renamed in the rows template or section-boundary nudges regressed. Start at tests/test_script_template_ui.py:379 and app/tools/transcript/templates/partials/transcript_rows.html.
#4241Board timeline URL tests are stale: template now appends &variant= but the two coordinate-URL tests don't expect itneeds detailBug fixclaude:otio-premiere
·App · transcript·Alchemy·2 months ago
Update test_the_export_url_carries_the_boards_coordinate and test_the_save_url_carries_the_boards_coordinate in tests/test_board_timeline_export.py to expect the &variant= query param that transcript_main.html's _cq/_save_cq now emit (added around commit 302868e / the variant work). Both tests currently fail on main.
#4243Premiere panel: Import Timeline button pulls the saved board cut into the projectreadyNew featurePremiere Conformclaude:otio-premiere
A button on the panel's edit menu that finds the newest saved *_fcp.xml in the open project's version render tree and imports it as a new sequence via premiere.create.edt.import_timeline (built in #4240). For when the timeline was saved before Premiere was opened, so the auto-push had nothing to push into. Does not auto-save the project - the editor is in the room.
#4262Port Rig Caddy to Unreal: edit rigs + apply motion-library clips in UE previsreadyNew featureUE Previs PerformanceclaudePrevis
·Plugin · unreal·Unreal Engine·2 months ago
Bring the Rig Caddy over to UE so a previs artist can select a character rig in the level, pose/edit it, and apply clips from the motion library (browse previews, drop a clip on a rig, retarget to the UE skeleton) - the missing performance-editing layer of the UE previs workflow. The UE build recipe already assembles the scene (sequence levels, env, design meshes, MagiCam, cameras from the timeline, HDRI); ImportConnectedRigs is currently disabled in unreal/build/prv/prv.json and should come back alongside this. With this landed, previs moves to UE end-to-end and renders through MRQ at UE speed instead of Blender/Eevee.
#4264Script publish fans out the table read + per-scene storyboard buildsreadyNew featureScript Publish Fan-OutclaudeScript
·Cookbook · alchemy · publish · scr·Alchemy·2 months ago
SCENE-LEVEL script workflow (clarified twice): (1) when working a scene's script, that scene's build = table read + storyboards, implemented by what svz's steps do today; (2) CONCRETE DUPLICATION found in the Iron Giant A rehearsal 2026-09-01: there are TWO storyboard paths - the scene page's batch 'Render Storyboards' (legacy svz: writes takes to svz blocks/NNNN/storyboards, cinematic-film-still prompt) and the script tool's per-row visual generation (current: writes row visuals[].media, honors the scene Style mode incl. the hand-drawn storyboard look). Batch-rendered boards never appear in the script editor because the two stores never sync. The fold: ONE storyboard path - the batch surface should drive the script tool's row-visual generation (or backfill row media), and svz's block store dissolves into the script workflow. Scope-invariant project/scene script publish then fans out table read + boards per covered scene.
#4266Character expression opt-out: flag a character as having no facial expression setreadyNew featureFacial Expression GenerationclaudeDesign
·Ontology · models·Alchemy·2 months ago
Add an explicit per-character opt-out for the facial expression set - e.g. expression_set: 'full'|'none' on the CharacterAsset, stored with the other production metadata (voice_id, rig type, design_notes) in the character's standalone json. Set from the design view's expression row (a toggle: 'this character has no facial expression set'). Readers: (1) cookbook EnsureFacialExpressions skip-passes for opted-out characters (same shape as its non-chr skip); (2) the design view renders the expression row as 'opted out by design' instead of nine amber slots; (3) readiness / script_required_tags stops counting the character's expressions as owed, and the beat resolver's fallback to the neutral design becomes correct behavior rather than a flagged gap - a robot who acts with his body still gets direction lines like 'looks surprised'.
#4269Scene scripts board like videos: lift the is_video gate on the visual-beats columnreadyAdjust existingScene Boardingclaude
·App · transcript·Alchemy·2 months ago
The transcript editor's picture-slot pills (Storyboard / Style frame / Video) and the per-beat visual cards were gated to entity_type == 'videos', so a SCENE's script (shots/NNN/SEQ/scr) rendered only Speaker/Text/Screen-time and no boarding widgets. Scenes board the same way videos do (a video IS a self-homed scene), so both template gates widen to ('videos', 'shots'): transcript_rows.html _video flag and board_columns.html pills guard. Python-side is_video (save routing) deliberately untouched. ALSO in this pass (same surface, Tom's layout note): the visual tile's location/cast area restructured from crowded horizontal flex rows into ONE vertical column - location chip, then each character/prop chip one per row (long names stay single-line, truncate), with the add inputs as the tail and the location override ALWAYS last (visual_tile.html).
#4275Review room: clicking playlist items does not load them into the playerreadyBug fixReview Playlistclaude
·App · media·2 months ago
On the media review page (e.g. /c/cgl/project/hag/media/review/<id>), clicking an item in the left playlist rail should load that item into the player (video/media swaps to the clicked item, selection highlight moves, URL updates to the item's review id keeping the ?playlist= query). Currently clicking between playlist items does not load them into the player.
#4276Style-frame payload: character ref should resolve through the expression chainreadyBug fixExpression-Aware Style Framesclaude
·App · transcript·2 months ago
The STYLE FRAME PAYLOAD modal's character reference (Image 2) binds the character's MAIN published front. It should resolve through the same chain the video refs builder uses in alc/services/ai/shot_finish.py (named expression -> composed expression sheet -> published front), so Image 2 binds the composite/sheet by default and the payload labels which was sent ('Character design' vs 'Character design (surprised)'). Reuse the shot_finish resolver rather than duplicating it.
#4277Beat board: choose which previs version the view readsreadyNew featurePrevis Version Pickerclaude
·App · transcript·2 months ago
The beat board / visual payload resolves previs media from an implicit version, with no way to choose. When previs renders exist under a different version than the one the view is pinned to, the payload falls back to 'No previs render for this beat yet' even though renders exist. Add a version picker (user/version scope) to the view so the board and its payloads resolve previs from the chosen version; the payload banner should also SAY which version it looked in, so an empty result is diagnosable.
#4297Harmony: import media into a scene + handbook pagereadyNew featureclaude
·Toon Boom Harmony·4 days ago
Add harmony.create.common.import_* to bring image sequences/PSD into a Harmony scene (drawing/element creation API; model on resources/scripts/TB_ImportPSDLayered.js), and a handbook page covering the batch runner, scene ops and the cookbook shelf. Also test whether external CPython can import ToonBoom.harmony.
#4298Concept-to-Layout Environment Pipeline (umbrella)readyNew featureConcept to Environmentclaude
·Plugin · smart_task·Alchemy·4 days ago
One concept/design image in, a correctly scaled, laid-out 3D environment out, via art-department stages: concept, asset breakdown, asset build, set dressing. Every stage checkable and debuggable.
#4342Analytics panel: roll up AI spend (cost_usd) by task/shot/asset/project/model/dayneeds detailNew featureclaude:ai-spend
·App · analytics·3 days ago
Run events (render phase, Image/Video Generation) now carry details.usage, cost_usd, rate_version, cost_basis, account (#4340). Aggregate cost_usd in the analytics panel by task, shot, asset, project, model and day; show failed-run attempts separately. Re-price via alc.services.ai.spend.price_usage.
#4380Model registry service: track, query and download models across Ollama, ComfyUI and the other AI servicesreadyNew featureModel Registryclaude
·Service · ai·3 days ago
One service that knows every model we have access to, per service and per machine, so large models are never downloaded twice. TRACK: a registry (JSON, data-in-json) of {service, name, kind (vlm/llm/checkpoint/lora/vae/segmenter/...), size, path or tag, version/hash, where it lives (this machine / shared drive / cloud), last verified}, built by scanning each backend: Ollama (/api/tags — image_to_text client already has list_models), the ComfyUI models dir (comfyui_bridge.client.models_dir, plus its list_local_models), SAM checkpoint (ALC_SAM_CHECKPOINT), MiniMax H3 weights, and any other local weights (today each is found by its own env var / setup.py, nothing is central). QUERY: has(service, name), find(kind=..., name~...) answering 'is this already on disk anywhere we can reach, and via which service?' — e.g. the same checkpoint sitting in both Ollama and ComfyUI trees, or a copy already on the shared drive. DOWNLOAD: ensure(name) checks the registry first and only fetches if absent (ollama pull / comfyui download_model / civitai / huggingface), recording the result; surface in-flight and failed downloads. Also expose to agents (MCP resource) and a simple Models page. Dedupe report: models present more than once, with sizes.
#4389Cookbook UI saved the dsn publish recipe with every step duplicated (doubled order numbers)needs detailBug fixclaude
·Cookbook · alchemy · publish · dsn·Alchemy·3 days ago
alchemy/publish/dsn/dsn.json in the live cookbook working tree (staged, not committed) lists every step twice - 32 entries for 16 steps, orders 1,1,3,3,5,5... - and puts Publish (31) after everything. Find which cookbook UI action (adding the EnsureDesignCamera stub / reordering) appends instead of replacing, fix it, and repair the file.
#4406Kanban: show which tickets have merged to main, and make them findablereadyNew featureclaude
·App · kanban·3 days ago
A ticket whose PR has merged to main is visibly marked (Merged badge + merge date/PR link on the card), and the board has a filter/column for Merged-recently so a batch of tickets worked back-to-back can be checked off without scanning the whole Done list. Source of truth: provenance PR/commit reachable from origin/main (same check ticket_cli audit uses).
#4478Cookbook: replace sidebar and tab rows with typeable dropdowns and phase chipsreadyNew featureCookbook Navigationclaude
·App · cookbook·a day ago
Cookbook navigation is one row of labelled dropdowns (Pipeline, Software, Group, Type/Recipe) that open the full list on focus and filter as you type (arrows/Enter/Esc), replacing the pipeline sidebar, software icon column, Group/Type pill rows and instance tabs. The Phases group shows the artist-facing phase chips (Start, Build, Art, Render, Review, Refresh, Publish) with an underline on the selected one. Opens on the Default pipeline; the row wraps to extra lines on narrow windows. Steps column and detail panel unchanged.
#4553Route test — capture a feature from the header pillneeds detailNew featureuserMotion Capture
·App · transcript·Alchemy·16 hours ago
#3474filling out the request doesn't copy the alc_do_ticket command to the clipboard in the premiere toolreadyBug fixTomScript
·Plugin · premiere · edt·Adobe Premiere Pro·2 months ago
#3573Delivery receipt: remember the last date filter between visits, so a stakeholder reopening the page sees the window they were reviewing rather than all timeneeds detailNew featureAnalytics PageTom
·App · cookbook·Alchemy·3 months ago
#3733Full-suite pytest wrote the live PUBLISHES registry and work board (test_cookbook_work_items)needs detailBug fixclaude
·App · kanban·3 months ago
tests/test_cookbook_work_items.py must never touch live PUBLISHES data: its cleanup fixture loaded the live features registry and called reg.commit(), re-stamping the live registry.json with the current build's schema on every full-suite run (this is how capability_tickets landed on the live file mid-sprint) and racing concurrent writes from the live server; board items were also written live. Fix: autouse wired fixture monkeypatching alc.services.features.features_root and WorkBoard.path to tmp_path, and silencing log_architect. Already implemented on branch claude/epic-carson-9562b8 (commit 34895f7) — this ticket is the board record for that PR.
#3815Analytics tests pinned to live ppe data drift and failreadyBug fixclaude
·App · analytics·Alchemy·3 months ago
Two tests assert exact counts against the LIVE ppe events tree, so they fail as soon as real work lands: tests/test_analytics_task_effort.py::test_real_ppe_effort_table_matches_the_verified_numbers and tests/test_analytics_autosave_actuals.py::test_real_ppe_scr_reports_five_point_three_hours_from_auto_saves. As of 2026-08-09 the scr task reports sitting_count 12 vs the pinned 11 (the 93 Premiere auto-saves still match). Freeze a fixture snapshot of the ppe events subset these tests read, or assert invariants/ranges instead of exact counts, so a real regression is distinguishable from ordinary new work.
#3859Design publish emits a DCC-neutral 3D interchange (USD) alongside the GLBreadyNew featureDesign InterchangeclaudeDesign
·Cookbook · alchemy · publish · dsn·Alchemy·3 months ago
The design (dsn) publish currently emits the design mesh as a .glb only, which makes every downstream DCC responsible for reading glTF. Blender can; 3ds Max cannot without a third-party plugin, and the same question returns for every DCC added after it. Fix it at the publish boundary instead of per-consumer: have the dsn publish ALSO write a DCC-neutral interchange of the same design mesh — USD is the natural choice — next to the .glb in the version's render dir, registered on the version MSD so consumers resolve it by address rather than by guessing a filename. The .glb stays (it is what the generative/design side produces and what Blender already consumes); this is additive, not a replacement. Extend the model MSD payload so a consumer can ask for the design mesh and get the format it can actually open. Done when: a dsn publish writes both formats, both are on the MSD, and a model build in a DCC with no glTF support can resolve and import the design mesh through the normal path.
#4125Previs menu hides Export Timeline / Open Versions on a board that declares no prep assetsreadyBug fixclaudeScript
·App · transcript·Alchemy·2 months ago
The header's Previs control in app/tools/transcript/templates/transcript_main.html is wrapped in "{% if create_mode and prep_total_count %}" (line ~73). A board that has tagged no location / character / prop has prep_total_count == 0, so the ENTIRE control is absent - including "Open Previs Versions" and "Export Timeline", both of which only READ what already exists on disk.
That contradicts the block's own comment three lines above it: "That gate is on the BUILD items only: opening versions and exporting a timeline read what exists and can't be made wrong by an unpublished rig, so gating them behind the chain would only hide finished work." The intent is already written down; the implementation does not match it.
Repro: a video board that has a previs version on disk but nothing tagged on its beats. There is no way to reach its previs versions or the previs timeline export from the editor at all.
Second, smaller instance of the same conflation: line ~88 disables the toggle button unless (_previs_ready or _previs_started), so the menu cannot be opened in that state. Impact is limited today because both items inside would be disabled anyway - but it is the same "can build" / "can open" mixup and will bite the moment a read-only item is added to the menu.
Suggested fix: render the control whenever create_mode is true, and keep the readiness gate on the two BUILD items (Blender / Unreal) only - which is what the comment already says the design is.
Found while working #4124 (Save Timeline). That ticket added a separate, scr-local quick save and deliberately did NOT touch this control, so this gate is still in place.
#4183when i click "build" to process a script - it should refresh the version window when the build completesneeds detailBug fixTomScript
·App · script·Alchemy·2 months ago
#4196Crop the Headshot from the Main / T-pose instead of generating itreadyNew featureHeadshot Cropclaude
·Service · ai·2 months ago
The Headshot deliverable on the character design row is currently a fresh nano-banana generation (design.generate_view -> prompt_compose.design_view_prompt('headshot')), so every click re-renders the face and risks identity drift. It also appends the project STYLE SPINE to the prompt ('illustration / painterly' etc), which can restyle a face we already approved.
We already have images we like. Derive the headshot by CROPPING instead of generating - the same shape as the existing tposeSideB path, which is a deterministic PIL flip with no model call (design._mirror_view).
What it should do:
- Add a crop path for the 'headshot' view in alc/services/ai/design.py: pick a source from the images already on disk - the Anchor / Main (primary_designs['main'] / reference_design) or the front T-pose (tag 'tpose') - locate the head, crop head-and-shoulders, write the next …_headshot_vNNN.png candidate. No generate_image call.
- Head localisation: mediapipe and cv2 are both already in the venv. Try mediapipe FaceDetection (full-range then short-range); these are photo-trained so a stylised character may not detect. Fall back to a subject-bbox heuristic - the design views are on a plain seamless grey backdrop, so the figure bbox is a background-difference away and the head is the top band of it.
- Source choice: prefer whichever candidate gives the LARGEST face in pixels (a T-pose head is small, a portrait-ish Main is big) so the crop is the highest-fidelity one available. LOG which source won and the native crop size, so the console says what it did.
- Framing: expand the face box to head-and-shoulders (top of head and both shoulders in frame, not cropped at the crown or the chin) and emit a square image matching the other design deliverables. Upsample only when the native crop is smaller than the target.
- No silent fallback: if nothing can be localised in any source, raise with a clear message and let the route surface a hint (same treatment as the 'no anchor set' and 'generate Side A first' hints in app/routes/assets.py generate_design_view). Do NOT quietly fall back to generating.
- Update the slot tooltip: _DELIVERABLE_HELP['headshot'] in app/routes/assets.py still says 'Portrait framing, generated from the Anchor.'
Acceptable smaller alternative if the crop proves unworkable for stylised characters: keep the generation but strip the style spine out of design_view_prompt for the headshot view and harden the wording so the model cannot restyle the face - identity/costume/render-style all inherited from the reference, only the framing changes.
Verify on http://127.0.0.1:8080/c/cgl/project/hmg/versions/at/assets/chr/hogarth/dsn - click Headshot and confirm the result is the approved face at portrait framing, pixel-identical in look to the source.
#273when i click a file in source files or render files - the PATH: should update with that full path it doesn't currently [debug-path-display-container]needs detailBug fixTom
·Cookbook · alchemy · context-menus · files·Alchemy·3 months ago
#227i don't know why the versions are showing up red when currentlyneeds detailBug fixTom
·Alchemy·3 months ago
#3946[cookbook] [alchemy] [edt] Implement the edt shelf/menureadyNew featurestub
·Cookbook · alchemy · menus · edt·Alchemy·2 months ago
Backlog
39
P14
#1802Style-frame prompts hard-code 'painterly' / 'illustrated', so the project style spine cannot change the mediumreadyBug fixclaudePrevis
·Alchemy·3 months ago
The style spine was introduced so the look comes from the Style manifest instead of being hard-coded, but only the LAST sentence of each prompt is actually spine-driven (_spine_sentence). The medium is still asserted several times before it, in words the spine cannot reach: _build_composite_prompt line 592 opens '{playblast} is the finished PAINTERLY scene'; _build_style_prompt line 564 'match its color palette, brushwork, and PAINTERLY treatment' and line 573 'Re-render {playblast} as a finished ILLUSTRATED animation frame'; the single-character template at line 657/663 says 'a finished painterly frame' and 'Illustrated animation style, painterly, no photorealism'. So a project whose spine says stop motion still sends a prompt that calls the plate a painterly scene three times and the spine once, and comes back as a painted illustration. WANTED: every medium word in these templates derives from the spine (or is neutral - 'the finished frame', 'match its colour palette and treatment'), so the Style rail is the single control over medium. Worth a pass over _DEFAULT_STYLE_SPINE too: defaulting to 'painterly illustrated animation style' means a project that never set a spine silently gets one house style.
#1857Previs should render a matte for every named asset in frame, not just charactersreadyNew featureclaudePrevis
·Blender·3 months ago
The previs render writes mattes/{char_id}/{char_id}.{frame}.png for CHARACTERS only. Everything else in the shot - props, set dressing, environment pieces - has no mask, which means the style pass cannot place or protect it. Concretely on loj/theaxe: a coffeeCup held by Jack falls INSIDE jack's matte, so nano repaints it as part of Jack and invents the design (#1748 now sends the cup's published design as an identity reference, which fixes what it looks like but not where it is or that it is a separate object). WANTED: anything that is a NAMED ASSET in the frame gets a matte on the same convention - mattes/{slug}/{slug}.{frame}.png - so the style pass can address it the way it addresses a character: matte fixes silhouette/location/scale, published dsn design fixes surface. That makes the existing composite prompt work unchanged for props (see _char_block / _prop_block in alc/services/ai/nano_banana/create.py: the prop block exists only because there is no mask to lock against). Tom: 'anything that's a named asset in the frame should get a mask to be honest.' Likely also wants the shot manifest to declare props alongside characters, so the styler knows what to expect rather than being told by the caller (style_frame_for_shot takes props= from the beat today).
#4265GenerateModel is dead code: stray early return skips env/prop mesh generationneeds detailBug fixGenerate ModelclaudeDesign
·Cookbook · alchemy · build · dsn·Alchemy·2 months ago
cookbook alchemy/build/dsn/GenerateModel.py line ~86 has a debug 'print(...); return' before the Meshy logic, so every env/prop design build passes in ~1s producing NO glb, and the design publish then fails EnsureDesignModel ('no .glb found'). Characters are unaffected (GenerateModelFromTpose is a separate step). Remove the early return so the image->png->Meshy image_to_mesh path runs again.
#236i need to be able to add/remove studio tools from the right panel on the Production Tasks pageneeds detailNew featureTom
·Cookbook · alchemy · tools·Alchemy·3 months ago
P214
#2132PROJECT_REGISTRY.current in DCC-side code resolves the config's default show, not the open scene'sreadyBug fixclaude
·Plugin · alchemy·3 months ago
PROJECT_REGISTRY.current resolves the project from CONFIG, not from the scene that is open. Inside a DCC or a render process that is routinely the WRONG SHOW: #1596 was diagnosed live with the registry hydrating 'cgl:jlt' while the open scene was E:/Alchemy/cgl/loj/... - so the lookup queried a different show's script entirely and returned None. #1596 fixed exactly one caller (_resolve_env_front in alc/plugins/blender/create.py, now grounded on the scene's PathObject via store_for(po) plus alc.context.scope_for(po)). The ticket suspected the defect reached past that one button. It does. TWO CALLERS REMAIN, both the same shape - a step-side call that has a PathObject in hand and asks config instead: (1) alc/plugins/alchemy/tasks/svz/__init__.py:38 - 'scene = PROJECT_REGISTRY.current.script.get_scene(scene_id)' where scene_id came from self.path_object.parent. It has the PathObject RIGHT THERE on the line above and still asks the registry's current project. Raises ValueError('svz.refresh: scene not found') on the wrong show, so at least it fails loudly. (2) alc/plugins/alchemy/tasks/dsn/query.py:128 - 'return lookup_asset(PROJECT_REGISTRY.current, parent, child)'. This one is WORSE than (1) because it is wrapped in 'except Exception: ... disk-only' and degrades silently to a disk-only path, so a wrong-show lookup is indistinguishable from a registry miss. That violates the no-silent-fallback convention twice over: wrong source, and the wrongness is swallowed. WANTED: both callers take the project from the PathObject they already have - store_for(po), with alc.context.scope_for(po) around the lookup where the callee resolves its publish base from the active context (which is what _resolve_env_front needed; check whether lookup_asset does too). Then make dsn/query.py name what failed instead of collapsing every cause into 'disk-only'. WORTH DOING AT THE SAME TIME: decide whether PROJECT_REGISTRY.current should be reachable from step/DCC-side code at all, or whether it should assert an explicit scope and blow up when there isn't one - a wrong answer from an authoritative-sounding source is the whole failure mode here, and a lint/grep gate would stop the next one landing.
#2160Sweep the publish-only task-start guard across the 10 remaining callsitesreadyChoreclaude
·App · routes·Alchemy·3 months ago
Every remaining 'start the task if it hasn't been started' guard asks 'if not po.task_msd.user_versions' — whether the version register is empty AT ALL — instead of whether THIS user has a rail. A task carrying only the 'publish' pseudo-user reads non-empty, so the start never runs and the artist lands on a surface with a published snapshot and nothing to select. Migrate each to PathObject.ensure_started(), added for #2118.
#3199Request pill mis-routes asset-grid bugs to the cookbook off the single word 'asset'readyBug fixclaude
·App · kanban·Alchemy·3 months ago
Two fixes. (1) route_facets() should return an area_id for the asset-grid routes (/c/{co}/project/{p}/assets/{Type}) -> app-webui, so WHERE it was filed wins as request.py:187-190 already intends. (2) The text autotagger should not be able to flip a ticket into the COOKBOOK repo on one generic English word; require a stronger signal (multi-word phrase, or a software facet that agrees) before a cb- area is accepted.
#3476Premiere panel phase buttons run a hardcoded recipe, not the one the card displaysreadyBug fixclaudeScript
·Plugin · premiere·Adobe Premiere Pro·3 months ago
A phase button's Run should execute the same recipe the card displays - the one Recipe.resolve returns for premiere/{phase}/{po.task} - instead of a fixed import list. Today cookbook/premiere/menus/alchemy/Render.py imports cookbook.premiere.render.edt.{SaveSession,EnsureAlchemyPath,SetRenderPath,RenderTimeline} unconditionally, and Review.py imports cookbook.premiere.review.edt.* unconditionally, so both run the edt recipe no matter which production task the open project belongs to. Publish.py has an empty body and returns True, so pressing Publish does nothing at all.
#4003Startup guard: warn when the loaded AlchemyUE module does not match the dll on diskreadyNew featureUnreal Alchemy Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
At editor startup (setup.startup.start), compare the AlchemyUE C++ module the editor actually LOADED against the dll currently on disk in the plugin's Binaries/Win64, and log a loud, actionable line when they differ — naming both (loaded build time / size vs on-disk) and the fix ('restart the editor to pick up the new binary'). Cheap check: the module is loaded from a known path, so compare the on-disk file's mtime/size against what was there when the module loaded (capture at startup), or probe for a known-newest symbol via hasattr(unreal.AlchemyUILibrary, '<newest fn>') and compare to what the on-disk dll exports. Surface it in the panel too — a dim 'plugin binary is newer than the loaded module' line in the header (next to the context) is enough; it must never block startup.
#4019Generated board visuals get an extension that does not match their bytesneeds detailBug fixclaudeScript
·App · transcript·Alchemy·2 months ago
Generated board visuals are written with an extension that does not always match their bytes, so a downstream consumer that picks its decoder by extension fails on them.
Found 2026-08-13 building theaxe's previs in Unreal. The board's plate for block 0020 is 'gen_v0002.jpg' - and it is a valid PNG:
gen_v0002.jpg magic 89 50 4E 47 0D 0A 1A 0A ('PNG'), tail AE 42 60 82 (IEND crc, intact)
copy_v0002.jpg magic FF D8 FF E0 ... JFIF (a real JPEG), tail FF D9 (EOI, intact)
So it is not corruption and not truncation - the file simply got a .jpg name over PNG bytes. The real JPEG next to it is fine.
Consequence: Unreal's Interchange selects a translator by FILE EXTENSION, hands the PNG bytes to InterchangeJPGTranslator, and gets 'Failed to decode JPEG'. The texture then does not exist and the shot cannot be built. One mislabelled file cost a board its camera. The same class of failure will hit anything else that trusts the extension - ffmpeg calls, PIL without a format hint, browser MIME sniffing, DCC importers generally.
WHERE TO LOOK: whatever writes /VERSIONS/render/videos/{video}/SEQ/scr/{...}/images/. The name pattern (gen_v0002) says this one came from the generator path rather than a user upload, so the extension is being chosen from a template or a request parameter instead of from what the encoder actually produced.
WHAT IT SHOULD DO: name the file after the bytes. Whatever encodes/downloads the image knows its real format - use it for the suffix, rather than assuming, defaulting, or carrying an extension over from a request. If a format must be forced, transcode to it rather than renaming.
Also worth auditing the other plates on this board: they come from mixed origins (gen_v0001.png, copy_v0003.png, sipping.png, Gemini_Generated_Image_koc70jkoc70jkoc7.png, bright_light.png), so user-supplied images likely travel the same path and can arrive mislabelled too.
NOT a blocker for the Unreal previs any more: prv.create._true_image_path now sniffs the magic bytes and imports a correctly-suffixed copy from Intermediate/AlchemyPlates, logging a warning that names the offending file. That makes previs robust to mislabelled plates from any source - it does not fix the file on disk, and it does not help the consumers that have not been taught the same trick. This ticket is the actual bug.
#4224Variant switcher on the version view and asset/shot gridsreadyNew featureVariant Navigationclaude
·App · routes·2 months ago
There is currently no way to switch variants anywhere in the UI: the version view (a task's CI/CD page) and the asset/shot grids always show the default variant, and a coordinate's other variants are invisible unless you type the ?variant= query by hand. Add: (1) on the version view, a variant selector beside the user/version rail — listing the variant directories that actually exist on disk for that task (derived, like everything else; hidden or inert when only 'default' exists); (2) on asset/shot grid cards, a small variant count/badge when a task carries more than one variant, clicking through to the version view homed on that variant. Context that promoted this from blind spot to need: pipeline videos (#4213/#4214) use variant as the audience axis (tut/mkt), so scr tasks with multiple real variants now exist routinely in the ckb project. Related but separate: #4223 fixes the editor OPS dropping the variant token; this ticket is the missing NAVIGATION for variants.
Every custom template in the studio overlay (feature_highlight, pain_to_proof, vertical_series, plus a customized pipeline_primitive) carried a section whose max_secs was BELOW its authored secs — typically section_1 with the 30s default secs and a small user-set max. Pattern suggests the editor lets the user set min/max while the authored secs silently keeps its default, producing a ceiling the section can never reach; layout then redistributes that section's time and every section band files wrong. Fix in the editor: when a user sets a max below the current secs, either move secs with it or refuse inline with a clear message — never save an incoherent clamp. The new registry validator (TemplateSection._check_bounds) refuses these at load as the backstop; the editor should make it unreachable.
#4292Storyboard Pro plugin: feasibility spike + initial frameworkreadyNew featureStoryboard Pro Pluginclaude
·Plugin · premiere·4 days ago
Phase 1 (spike): determine what Toon Boom Storyboard Pro exposes - Python/JS scripting API (SB Pro uses QtScript/JS + a Python-capable Harmony-family CLI?), headless/batch CLI, import/export formats (XML/OTIO/EDL/PSD/PDF/FCPXML), render/export of the current scene, and whether an internal MCP/bridge (as with blender_bridge, premiere bridge) is feasible. Document findings in dev/ and the handbook. Phase 2 (framework): scaffold alc/plugins/storyboard_pro following the Blender/Premiere plugin patterns (bridge service + panel + tasks, contract-first DCC layer). Map SB Pro scenes/shots/panels onto Alchemy ontology (scene, shot, storyboard layers, ShotManifest); import panels/layers/timing from SB Pro into Alchemy and export Alchemy storyboards back; keep all editorial data (timing, sequence order, transitions) in our OTIO structure as source of truth; register basic ops via the etymology {dcc}.{verb}.{task}.{op}: open scene, list scenes/shots, import, export, render current scene/panel to image or mp4 proxy. Done = findings doc + a working round trip of one sequence (SB Pro -> OTIO/Alchemy -> SB Pro) and a render-current-scene op.
#4343Spike: orthographic views for generated concept assets (fill-in as source, or mesh-then-render)readyNew featureclaude
·Alchemy·3 days ago
Given one clean isolated image of an asset (from the #4299 fill-in), produce six consistent orthographic views. Compare (a) per-view generation with the fill-in as reference and a no-perspective prompt plus an automatic check that each view differs from the others, and (b) image-to-3D then render six orthos with the existing ortho-to-Blender harness. Output contact sheet and a verdict.
#4436Design (dsn) publish doesn't scale the published .glb to the height set in the design phasereadyBug fixclaudeDesign
·Blender·2 days ago
The dsn publish should write the design .glb already scaled to the height/scale declared in the design phase, so anything importing it (e.g. blender build/lay ImportPublishedAssets design-glb fallback, ImportDesignModels) lands at the correct production size with scale 1.0 and no per-consumer correction.
#4505Tests must never write into the real cookbook (tutorials / walkthroughs folder)readyChoreWorkflowsclaude
·Alchemy·a day ago
Many tests save tutorial/walkthrough files into the REAL cookbook folder (Walkthrough._dir() points at the live cookbook) and some leak: batch G found 147 untracked 'Blender Model Pipeline' scaffold files in the live cookbook, 99 of them created in a single day, and test runs (including parallel session runs) keep adding more. Make every test that touches tutorials use a throwaway cookbook dir: one shared fixture that redirects the tutorial store (and any other cookbook write path) to tmp_path, applied automatically (autouse) to the tutorial/walkthrough test files, plus a guard that FAILS a test run if the real cookbook tutorials folder changed (count files before/after in a session-scoped check). Fix the leak sources, don't just clean up after them.
#4578Ingest: send the import (copy) to Deadline instead of copying inside the web requestreadyNew featureLinked Folder Ingestclaude
·App · ingest·7 hours ago
Today 'Ingest tagged' (linked folders) and 'Ingest Files' (batches) run IngestManifest.commit() inside the HTTP request: create_empty_version then shutil.copy2 per file, so the page hangs until every copy finishes (a 90-frame PSD sequence or a multi-GB folder takes minutes). Move the copying to Deadline. On confirm: (1) in the app, quickly create_empty_version for each destination version group (commit_groups(pending_only=True)) so the version/MSDs exist; (2) submit ONE Deadline job per destination version, with one task per file (Frames=0..N-1, ChunkSize=1, ConcurrentTasks capped, the validated fan-out pattern in DeadlineService.submit_python_job) so a version's files copy in parallel; (3) return immediately with 'Submitted N job(s)'. Workers must not all write ingest.json at once: each task writes its own result file (cache/commit/<batch id>/<task>.json: dest, ok, error), and the app (progress poll) or a dependent completion job merges those into the tag manifest, marking files 'copied' (failed ones stay 'tagged' so a later commit retries them). The link workspace shows live progress (copied N of M, per-version job state, failures with a Retry), tiles flip to the imported badge as files land, and a repeat click never double-submits files already queued/copied. Source stays read-only; works the same for sequences (each frame is a file) and multi-tag files (each tag its own import).
#30Convert the Review Room transport to the shared AlcPlayback widgetneeds detailChoreGroom
·Cookbook · alchemy · review·Alchemy·3 months ago
P321
#8Screen-Recording Shot List & Asset Thumbnail Generatorneeds detailVideo
·Alchemy·3 months ago
A lightweight pipeline tool that (1) reads the current project's asset list from the production tracker and auto-generates a shot list of required screen-recording moments (env/char/prop with thumbnails, spreadsheet view, animatic), and (2) exports a contact-sheet of asset thumbnails ready to drop into the presentation. Eliminates ad-hoc hunting for the right assets at capture time.
#302Startup identity-sync writes global active company (set_current_company+commit) — a second #290 triggerreadyBug fixclaude
·3 months ago
Related to #290 (active company/project is global mutable state). Separate from the browser switch routes: alc/services/identity.py sync_companies() (L351), run at app startup via app/main.py lifespan _identity_sync, checks CFG.company_id against accessible_company_ids() and, if the persisted default isn't accessible, calls CFG.set_current_company(fallback) + CFG.commit() (L383-393). This mutates the SHARED alchemy.json active company at boot and picks a studio-wide fallback, which then clobbers headless consumers / other sessions keyed off CFG (observed live: a TestClient boot flipped active company cgl->bse). Once #290 makes active company/project per-session/per-browser, this boot-time global switch+commit should also be reconsidered: grounding an INACCESSIBLE default is legitimate, but it should not rewrite a global studio-wide default that other tabs/tasks read. ACCEPTANCE: booting the app under an identity whose default company is inaccessible does not silently rewrite the global active company for every other session; the accessible-company grounding is request/session-scoped.
#2101Cross-DCC parity tests are red on main (5 failures in rig/prv/tex)needs detailBug fixclaude
·Plugin · smart_task·Alchemy·3 months ago
Five tests fail on a clean main, unrelated to any in-flight work:
test_parity.py::test_dcc_matches_contract[blender:rig/edit.py:RigEdit]
test_parity.py::test_dcc_matches_contract[unreal:rig/edit.py:RigEdit]
test_parity.py::test_dcc_matches_contract[unreal:rig/query.py:RigQuery]
test_parity.py::test_dcc_matches_contract[maya:prv/query.py:PrevisQuery]
test_tex_ldv_dispatch.py::test_canonical_channel_collapses_synonyms
Run: venv/Scripts/python.exe -m pytest alc/plugins/blender/tasks/tests/ -q
Result: 5 failed, 100 passed.
Either the concrete verb modules have drifted from their contract ABCs and should
be brought back in line, or the contracts moved on and the tests are stale. Decide
which, then fix so the suite is green.
#3668Capability naming schema: Feature title + App/Widget tags — decide AFTER 10-20 manual taggingsreadyDesignclaude
·App · features·3 months ago
DO NOT BUILD YET. This ticket holds the emerging naming schema so the decision survives until the
evidence exists. The prerequisite is Tom manually tagging 10-20 tickets through the #3667 hand tool
("it's something I have to take 10-20 of the tickets to really understand better") -- his typed
names and descriptions ARE the requirements gathering.
THE FORMULA AS CURRENTLY UNDERSTOOD (Tom, 2026-08-08, from ticket #390):
Ticket: "Locations rail: define a video's location once and have every beat inherit it"
Feature: "Location Inheritance" <- the TITLE; the thing ADDED; what an exec cares about
Widget: "Location Rail" <- tag: where it lives
Tool/App: "Beat Board" (= Transcript; Tom now treats those as one surface) <- tag
Area: app-transcript <- derived; also a tag
Full sentence, composable from the parts, possibly the description or the display line:
"{Feature} added to {Widget} on the {App}" -> "Location Inheritance added to Location Rail on
the Beat Board" -- something an executive can read and understand.
Widget and area are TAGS; the Feature is what we care about. The full sentence may end up being
what we want rendered -- undecided until the 10-20 sample exists.
THE CARVE-OUT -- COOKBOOK SELF-DESCRIBES (Tom): "every ticket won't have that formula -- cookbook
recipes will basically write themselves -- the step IS the feature and we know exactly where it
fits." A cookbook step's address {software}/{stage}/{task}/{step}.py already carries feature name
and every tag deterministically. So the schema work is for APP/CORE features only; cookbook
capabilities derive mechanically from the registry, no human naming needed. This is not automation
returning by the back door -- it is reading an ontology that already contains the answer, vs
guessing one that does not.
WHAT TO DO WHEN THIS TICKET IS PICKED UP (after the tagging session):
1. Read the capabilities Tom created: names, descriptions, and the tickets he grouped. Extract the
actual pattern he converged on (it may not be the formula above).
2. Decide: structured fields (feature/widget/app) vs free text + composed display sentence vs
status quo. Migrate the hand-tagged capabilities into whatever is chosen WITHOUT losing his text.
3. Implement cookbook self-naming as its own path (kind=cookbook capabilities minted from the
cookbook registry, addressed tags, zero human input).
4. The "you can now ___" test remains the gate on any rendered feature name.
RELATED: #3667 (the hand tool that gathers the evidence -- must land first), #3607 (surface
derivation), #3610 (the timeline these names render on).
--- AMENDED 2026-08-08 ---
WORKED EXAMPLES 2-4 (Tom, same session — the pattern "keeps coming up"):
#81 "Make the Generate Button Functional on the Visual Block for the script Row"
Feature: Generate Image | Widget: Visual Block | App: Transcript (Beat Board)
#82 "Unpublished cast/location pills go red + click to the design version view"
Feature: asset status & link to design view | Widget: Visual Block | App: Transcript
#88 "on the transcript page i want to see the media in the playback area that corresponds to the clip"
Feature: media playback | Widget: playback area | App: Transcript
WHAT THE SAMPLE ALREADY SHOWS (4 of the 10-20):
- The decomposition held on every ticket tried so far, including messy conversational titles.
- #81 and #82 SHARE Widget "Visual Block" but carry DIFFERENT Features — direct confirmation that
Widget is a tag (where) and Feature is the unit (what was added). Grouping by widget would have
wrongly fused them.
- App has converged to "Transcript" (= Beat Board — Tom treats them as one surface) across all
four; App behaves like a stable coarse tag.
- Feature-name polish varies ("Generate Image" vs "asset status & link to design view") — the
schema must accept rough names; polish is a later editorial pass, not a capture requirement.
--- AMENDED 2026-08-08 ---
DERIVABILITY SPLIT (Tom: "the user is going to get the name of the widget/app wrong more often
than not" — so how much can we derive?):
Feature = JUDGMENT -> typed free text, always. Only the human knows what was added.
Widget = VOCABULARY -> derived where possible, PICKED from a controlled list otherwise,
never free-typed. Free-typed tags fork ("Visual Block" / "visual
widget" / "media block") even from one careful user.
App = VOCABULARY -> ~fully derivable TODAY: area_id -> tool mapping already exists
(app-transcript -> Transcript). All four worked examples derive
correctly with zero input.
Existing sources for the Widget vocabulary, in order:
1. WorkItem.ui_keywords — tickets already carry visible on-screen labels; that IS widget vocab.
2. The walkthrough binding model — page -> control registry already names surfaces and controls.
3. Template/partial filenames touched by the ticket's commits (visual_block.html etc.).
When derivation is ambiguous: offer a SHORT pick-list of that app's known widgets. Picking from
six options cannot misspell; typing can. Same principle as the cookbook carve-out one level down —
derive where the ontology contains the answer; constrain the choice where it half-contains it.
Consistent with the ids-in-lists/labels-in-text convention.
#3669Feature-name prefill: local LLM (qwen) drafts a 2-3 word name from the ticket subject, seeded by Tom's own tagsreadyNew featureclaude
·App · features·3 months ago
Tom, during the naming-schema session: "it seems like we could analyze the subject to derive a 2-3
word Feature description from it with a local llm - qwen maybe?"
SCOPE — A PREFILL, NOT A TAGGER. This does not reopen the no-automation rule (see #3667's final
direction): membership stays 100% manual, and the name field stays editable free text. The LLM only
DRAFTS the 2-3 word Feature name into the input when Tom selects tickets; he edits or replaces it.
Never auto-applied, never pushed as chips onto rows (that failure mode is documented on #3667).
PRECEDENT AND PLUMBING ALREADY EXIST:
- alc/services/feature_proposer.py get_llm(): StructuredLLM -> Claude if ANTHROPIC_API_KEY else
LOCAL OLLAMA QWEN. Tom's box runs the ollama path. Reuse it; catch-all -> empty prefill (the
proposer already degrades this way — an LLM failure must never block manual naming).
- The LLM-as-namer role was DECIDED long ago: message-based LLM membership guessing was demoted
after a fat-commit test (message named 4 things, files touched 35 areas); the surviving role was
"naming aid grounded in deterministic facts". This ticket is that role, finally built.
THE KEY DESIGN POINT — SEED FROM TOM'S OWN TAGS: the few-shot examples are the capabilities he
hand-named through #3667, live from the registry (name <- the ticket titles it groups):
"Locations rail: define a video's location once..." -> "Location Inheritance"
"Make the Generate Button Functional on the Visual Block..." -> "Generate Image"
"on the transcript page i want to see the media..." -> "media playback"
So the prefill inherits HIS naming taste and improves as he tags — the manual session trains it.
Style constraints in the prompt: 2-3 words; the thing ADDED (noun phrase passing the "you can now
___" test); never the widget/app name (those are derived tags — see #3668's derivability split);
never a restatement of the area.
LATENCY: prefill must be non-blocking — the name field is immediately typable, the draft arrives
async and NEVER overwrites characters Tom has typed. If ollama is down/slow, nothing appears.
GATING: depends on #3667 (the hand tool this prefills). Pairs with #3668 (schema) — if the 10-20
sample session changes the naming shape, the prompt follows the schema, not the other way round.
ACCEPTANCE: selecting ticket #88 prefills something like "Media Playback" within a beat, editable;
typing first suppresses the prefill; ollama offline = clean empty field; nothing is ever applied
without Tom committing the form; membership derivation is untouched.
--- AMENDED 2026-08-08 ---
TEXT vs IMAGE (Tom: "or is that more image->text?"):
The SUBJECT is the source for the Feature NAME — it carries intent (what was asked to be added);
the screengrab shows the screen, i.e. WHERE, not WHAT. So name derivation stays text->qwen.
Image->text (VLM) earns a place one level down, as a WIDGET-TAG fallback: pill-filed tickets carry
a screengrab; when a ticket has the grab but empty ui_keywords, a VLM naming the pictured control
can supply the Widget vocabulary candidate (same pattern as the motion-library VLM search). Order:
ui_keywords first, VLM-on-screengrab fallback, always landing in the PICK-LIST, never free text
(see #3668's derivability split). Out of scope for the first build of this ticket — note only.
#3860Manage the 3ds Max glTF plugin as a pinned, self-installing dependencyreadyNew feature3ds Max setupclaude
·3ds Max·3 months ago
Write alc/plugins/max/setup/gltf.py so the Khronos glTF importer/exporter installs reproducibly instead of being hand-dropped — the same treatment mGear gets in alc/plugins/maya/setup/mgear.py. Surface: a pinned release constant (tag + zip name + download URL), is_installed(), install(force=False) that is idempotent and downloads/extracts/deploys, and setup() composing them. install() must select the subfolder matching the DETECTED Max version rather than hardcoding a year, and locate the plugin pair by globbing for KHRglTFImporter_*.dli / KHRglTFExporter_*.dle instead of hardcoding the zip nesting — so a future re-layout fails loudly rather than installing an inert tree (the exact rule mGear's install() follows for mGear.mod). Deploy the version's ColorManagement/ocio_configs/glTF_PBR_Neutral_Tone_Mapper alongside the binaries for 2024+. Wire it into alc.plugins.max.setup.setup() NON-FATALLY, the way maya's setup() calls mgear.setup() inside a try — an offline machine or a GitHub outage must not sink the base Max bootstrap. Done when: a clean machine runs max setup() and ends with a Max that imports and exports .glb, and re-running is a no-op.
#3861EnsureGltfPlugin preflight — self-heal a Max session missing the glTF pluginafter #3860blocked by #3860New feature3ds Max PipelineclaudeModel
·3ds Max·3 months ago
Add an EnsureGltfPlugin step at the head of the max model build recipe, so a Max session that started without the glTF plugin repairs itself instead of failing at ImportDesignGlb. It is the max analogue of maya's ensure_mgear: check whether the importer/exporter are live in THIS session; if not, load them at runtime, installing the pinned release first if it is absent from disk; return a small dict reporting loaded state and version. Because the recipe's very next steps consume the design GLB, this must run before them and must fail LOUDLY with an actionable message when it cannot repair — a silent skip turns into a confusing failure three steps later. Done when: a Max launched with no plugin installed runs the build recipe to completion unattended, and a farm node that missed setup does the same.
#3944resolve_path cannot decompose a path under an unregistered rootreadyNew featureMulti-Root Coordinatesclaude
·Core·2 months ago
AlchemyRouteEngine.resolve_path() should be able to decompose a path string that sits under a root the engine does not have registered - a tmp root in tests, a sandbox, a second studio volume. Today it identifies the root by scanning CFG.companies for prod_root/perforce_root and taking the LONGEST registered candidate that prefixes the path (alc/config/route.py, resolve_path); when nothing matches it defaults matched_root to prod_root, then strips a prefix that is not actually there, and the parse fails with 'RouteEngine could not resolve: <path>'. This is the REVERSE direction of #3943. That ticket fixed ASSEMBLY (dict -> path): path_at now honors the coordinate's own root, so a coordinate can be GROUNDED anywhere. Parsing (path -> dict) did not move, so the round trip is asymmetric: PathObject(root=tmp) resolves fine, but PathObject(<that resolved path string>) raises. Concrete cost today: tests/test_version_creation_guards.py still has to monkeypatch AlchemyRouteEngine().prod_root to tmp_path purely so its coordinates can be re-parsed - the root FIELD alone is no longer enough only because of this. Any test that round-trips a sandbox path pays the same tax. The design question to answer first: how does the parser know how many leading segments are root, given root is a single template token of arbitrary depth ('E:/Alchemy' is 2, 'C:/alc/p4v' is 3, '/Game' is 1)? Registered roots are what makes that unambiguous today. Options worth weighing: let a caller pass a candidate root in explicitly; register sandbox roots on the engine for the duration of a test via a documented helper/fixture; or infer the split by matching the remainder against route templates and requiring exactly one unambiguous answer.
Package the workstation client as native executables/installers for Windows and macOS (as in the previous Alchemy generation): local agent service with the DCC socket bridges and remote-server connection (tailscale). Deliberately NOT containerized - DCCs need native GPU/GUI access. Success test: installer on each OS installs the agent, it starts at login, and connects to a remote Alchemy server over the tunnel. Backlog capture - the counterpart to the Web App Dockerfile sprint, not part of it.
Drag an asset out of the panel's rail (or a grounded selection) and drop it into the level viewport, placing it — the one interaction a python-built panel cannot originate, since Slate drag-drop operations must start in native code. Build on the AlchemyUE C++ module: a UWidget subclass (or a UAlchemyUILibrary hook) that starts an FDragDropOperation carrying the Alchemy coordinate, and a drop handler that spawns/places at the cursor. The coordinate is already known — selection grounding (#3974) means the panel holds a PathObject — so the drop can place the RIGHT published asset rather than whatever is highlighted.
#4011Native transport + tab registration (run without the Python plugin)readyNew featureUnreal Alchemy Pluginclaude
·Unreal Engine·2 months ago
Move the socket transport and tab registration into the C++ module so the plugin works in contexts where the Python editor plugin is absent or disabled — packaged/-game builds, locked-down studio installs, and (for a Fab-style release) machines that never enable Python at all. Today both live in python: bridge_link dials out on a daemon thread, and the panel tab is spawned by EditorUtilitySubsystem from setup.startup. The C++ side would own the socket + tab, with python remaining the pipeline brain when it IS available.
#4263Start recipe crashes on SG status steps: class name casing mismatchreadyBug fixclaude
·Cookbook · alchemy · start · default·Alchemy·2 months ago
Every task bootstrap (po.start()) logs two crashes: cookbook.alchemy.start.default.ChangeSgStatusToInProgress has no attribute 'ChangeSGStatusToInProgress' (and the Task twin). The step name in default.json says 'ChangeSGStatusToInProgress' but the class inside the module is cased differently, so cli_runner's getattr(mod, name) raises. Harmless on projects with no tracker but prints a traceback on every project/task creation - including live demos. Fix the casing in default.json (or the class names) so the steps load; they should then no-op cleanly when no tracker is configured.
Opening the scene script editor modal a second time in one page session throws two console SyntaxErrors: 'Identifier _previewRAF has already been declared' and 'Identifier ALC_C has already been declared' - the swapped-in partial carries inline <script> blocks with top-level const/let declarations, which cannot be evaluated twice on one page. Handlers happen to survive (the first load's closures win) so it is functional, but the console fills with red during demos and any NEW code in those blocks after the first swap silently never runs. Fix: make the inline declarations idempotent - window-guarded (window.ALC_C ??= ...) or IIFE-scoped - following the same load-once/re-init discipline mountTrsEditor() already uses for transcript_editor.js.
#4339Home dashboard as the landing pageneeds detailNew featureHome dashboardclaude
·App · routes·Alchemy·3 days ago
Landing page for everyone. Top strip: Time to First Look (task start to review submission); more KPIs to be chosen after asking producers - additive. Below: live feed of the latest images/videos made at the studio, tutorials/walkthroughs, your short task list. Adds a Home row to the sidebar and becomes the default landing. Later ticket; Nav Reorg ships without it.
#3Create a Walkthrough for the process of creating backlog ideas through todo farmingneeds detailChorePipeline
·App · walkthroughs·Alchemy·3 months ago
Create a Walkthrough:
1) Click Pipeline Tab
2) Cilck Usage - this shows you todos
#1Visual Complexity Preflight Checkerneeds detail
·3 months ago
Preflight that enforces one focal point and a squint test; blocks busy visuals from review.
#92write a plan for moving dns records from godaddy to AWS - seems easier to manage headlessly long term.needs detail
·Service · cloud_studio·3 months ago
Some weaknesses already visible in the ticket: Application / DCC - doesn't apply here - but having something like IT Systems or Cloud Infrastructure would be helpful. Type - System would be helpful, System / Area - services - cloud or services - aws (or even more granular) would be helpful. Production Task - IT would be helpful. Essentially for this feature we to create a plan to move dns management to actual services so we can do dns management through an api rather than rely on logging in to GoDaddy.
A structured edit-review checklist (checklist template in the edit tool or a preflight step in the review pipeline) that flags: (1) shot duration below a configurable threshold on key message moments, (2) sections where VO word-count per second exceeds a readable pace, and (3) consecutive cuts under N frames on 'payoff' tagged clips. Surfaced as a report before the edit goes to stakeholder review.
·Cookbook · blender · build·Adobe After Effects·3 months ago
A reusable After Effects / Premiere motion-graphics template (MOGRT) that takes a flat list of text or image items and auto-generates a sequential 'pop-on' build with configurable hold duration between each item and a final 'all visible' hold before the next scene. Designers drop in their list; timing is consistent and review-ready by default.
A review-phase guideline + preflight step (automatable via a frame-sample script or manual checklist) that identifies slides/shots containing more than one animated element simultaneously. Flag any frame where a secondary animation (creature color change, moving character, slot machine) is active at the same time as a primary message graphic. Output a timestamped report for the editor to resolve before stakeholder review.
#10Double-check production names, edit any that are wrongneeds detailScript
·3 months ago
Harden scr publish to catch name+category issues before publish.
Sprints
171
Telemetry Truth
40
Drag items or batches here, or hit to add one.
One Fact One Home
11
#3780Two capabilities per ticket: the board's WorkItem.capability and the registry's ticket_numbers disagreereadyBug fixOne Fact One HomeFeature Attribution Workflowclaude
·App · features·3 months ago
The ticket grid (#3773) surfaces a split nobody can see from a single screen: a ticket carries TWO independent answers to 'which capability does this advance?'
1. WorkItem.capability - free text on the board, written at filing/wrap-up by the #3670 flow and by 'ticket_cli edit --capability'.
2. Capability.ticket_numbers - the features registry's first-class membership edge, written by hand in the ticket grid (#3667/#3717).
Observed on #3773 itself while building the grid: the board said capability='Feature Attribution Workflow' while the grid's Capability cell was EMPTY, because the registry had no ticket_numbers edge for it. Both are 'the capability', both look authoritative, and neither reads the other.
Consequences: the #3670 attribution ritual (asking the operator to name the feature at claim time) produces data the timeline and receipt never see, since those read the registry. And the grid's editor silently disagrees with what the card shows.
DECIDE, then build one of:
(a) the registry edge is the truth and WorkItem.capability becomes a WRITE-THROUGH - 'edit --capability' maps the ticket in the registry (minting by title, exactly like name_tickets does today), and the board field becomes a cache/mirror;
(b) the board field is the truth and the registry derives ticket_numbers from it on read;
(c) keep both but SHOW both in the grid, with a one-click 'adopt the board's answer' - the cheapest honest version.
(a) is the likely answer: the registry already owns the commit join and the move/split semantics that make re-mapping one action. Whichever wins, the grid should stop being able to show blank while the card shows a name.
Shareable Reports3
#3671Kanban: a ticket has a URL — /kanban?ticket=<n> opens the board with that card's modalreadyNew featureShareable ReportsTicket Deep Linksclaude
·App · kanban·3 months ago
A ticket number is the durable handle everyone uses — chat, PR bodies, commit subjects, receipts —
but there is NO URL that opens ticket #<n>. Verified: the board route
(/c/{cid}/project/{pid}/kanban) accepts only group_by; the modal route
(/kanban/item/{item_id}) is keyed by internal UUID and returns a bare fragment (no shell — hx-*
attributes dead when navigated directly). Consequence: links to tickets from anywhere outside the
app either don't exist or point at the wrong system (chat sessions have been linking #<n> to
GitHub, which is not where tickets live).
BUILD:
1. /c/{cid}/project/{pid}/kanban?ticket=<n> — full shell-wrapped board page that resolves the
NUMBER to its item (WorkBoard.get_by_number) and opens that card's modal on load. Number not
found -> board loads normally with a small "no ticket #<n>" notice, never a 500.
2. Honour the existing coordinate/history conventions: the URL must be shareable and
refresh-stable, play nice with the htmx shell scope, and not push duplicate history entries
(see the coordinate-routes rule from #3495: never push the URL the browser is already on).
3. A short alias worth having while in there: /t/<n> (company/project resolved from active
context) -> 307 to the full URL, so a bare number can be linked from anywhere.
4. Surface it: a copy-link affordance on the modal header, and ticket_cli show prints the URL.
5. Consumers to update once it exists: PR bodies and receipts/timeline (#3610/#3667 lanes) link
ticket numbers to this URL — the receipts' evidence trail should land people on the board, not
on GitHub.
ACCEPTANCE: pasting /c/cgl/project/ppe/kanban?ticket=3670 into a fresh browser opens the board
with #3670's modal open and working (htmx alive); /t/3670 redirects there; unknown number degrades
politely; the modal offers copy-link.
#3767Timeline: fold the receipt's numbers into the lanesreadyDesignShareable ReportsFeature Attribution Workflowclaude
·App · features·3 months ago
Tom: 'the receipt view is interesting — but that data could be integrated into the timeline. Until clients ask for specific views, simpler = better; the timeline does most of the jobs once hydrated.' Design task: bring the receipt's per-capability numbers (derived effort, sittings, active days, commit count, coverage context) INTO the timeline — likely the lane card's tooltip or an expanded card state — so one surface answers both 'when' and 'what it took'. Keep the receipt route alive until the fold proves out, then retire it from the header entirely. Honesty rules carry over: ~ marks derived, reported hours never summed with derived, no delivery claims.
#3772Timeline: a real filtering system — facets, date ranges, saved filters as named reportsreadyNew featureShareable ReportsFeature Attribution Workflowclaude
·App · features·3 months ago
Tom: "Filtering will be big — it's how executives will see the work they care about easily, and how
I as the studio architect create specific reports that are meaningful. We likely want a pretty
well-featured filtering system — even the ability to save filters. For example: just the Unreal
Engine features that were built this week."
The #3768 v1 (comma-OR text terms over title/tickets/subjects) is the seed. The full system:
1. FACETS, composable with text terms:
- date range (filed/active within: this week, last 2 weeks, custom from-to) — "built this week"
- area/App (from member tickets' area_id -> tool label, the derivation that already exists)
- type (feature vs bugfix vs chore — change_type majority, the #3768 glyph logic)
- repo (alchemy/cookbook), status (has unexplained / all-explained), person (when multi-user)
Grammar suggestion: keep the one text box but support tokens ("app:unreal filed:this-week
type:feature ue rig") so power use stays one line and the URL stays the state.
2. THE URL IS THE FILTER STATE (composes with #3671 ticket deep links): every filter combination is
a shareable link — sending an exec "the Unreal work this week" is sending a URL. This is the
cheap version of reports; build it before any report builder.
3. SAVED FILTERS: name a filter ("UE — this week"), it appears as a chip row on the timeline
header; relative dates stay RELATIVE when saved (this-week means the week you open it, not the
week it was saved). Storage: the feature registry (a top-level saved_filters key, round-trip-safe
like capability_naming). Saved filters ARE the meaningful reports — the exec clicks a chip.
4. Same honesty rules: hidden-lane count always visible; a filter that matches nothing says so and
offers clear; relative dates label themselves with the resolved range.
SEQUENCING: after #3667's evidence grows (filters are only as good as the tagging) and ideally
after #3671 (URLs) so saved filters land shareable from day one.
Plugin · premiere4
#1173Beat boards have no render leg: no animatic mp4, no OTIO, nothing to reviewreadyNew featurePlugin · premiereclaude
·App · transcript·3 months ago
The cookbook's semantic arc is start -> build -> render -> review -> publish. A beat board (scr) has build, publish, shelves and context-menus - and NO render recipe at all. There is no alchemy/render/scr.
CONTRAST, WHICH IS THE SPEC: alchemy/render/svz already carries RenderAnimatic.py, ExportOtio.py and ExportPremiereTimeline.py. A film scene can render its assembled timeline to a watchable animatic, hand an OTIO to editorial, and round-trip a Premiere timeline. A beat board - which is now a first-class timeline owner (#1024) with its own cast registry (#1025) and its own previs - can do none of those. The board is authorable and buildable but not watchable.
WHAT TO BUILD: alchemy/render/scr, mirroring the svz render leg against the board's own timeline (the one timeline_for(po) now resolves via TIMELINE_OWNERS): render the assembled beat board to an mp4 animatic with its voice track, and export its OTIO.
DECIDE AND STATE:
* Whether the svz render steps generalize to any timeline owner rather than being copied. They read a ScriptScene timeline, which is exactly what a board now hands back - copying them would re-bake the svz assumption one layer over, which is the mistake #1024 exists to stop.
* Where a board's animatic lands, and whether it becomes the board's reviewable media (the review/turntable fold).
OUT OF SCOPE: the previs 3D render path; MagiCam.
NOT THE FIX FOR THE MISSING PREVIS AUDIO - see notes.
Ensures all video clips on V1 don't have linked audio. THis makes for clean nesting/naming.
#321Add ExtendScript/QE DOM path to Premiere bridge for scriptable UnlinkreadyNew featurePlugin · premiereclaude
·Plugin · premiere·3 months ago
The Premiere UXP API (ppro, used by alc/plugins/premiere/bridge) exposes no unlink verb, no menu-command executor, and no link-state accessor (verified across all classes, live instances, Utils, and Constants). This blocks automating the 'Unlink V1 Clips' amc-publish step needed for clean video-only nesting in EnsureShotNames (createSubsequence otherwise drops blank audio clips on placement). ExtendScript/QE DOM via CEP CAN unlink A/V. Extend the premiere bridge to optionally run ExtendScript so Unlink (and other UXP-gap ops: link-state read, menu commands) become scriptable. For now the artist unlinks V1 by hand and UnlinkV1Clips is a detect-and-warn check.
#322amc publish MSD: derive shot media paths from an amc pathmap (PathObject/add_root), not render-dir-relative stringsreadyAdjust existingPlugin · premiereclaude
·3 months ago
manifest.py writes shot media as render-dir-relative strings (e.g. 'shots/INT_1000/INT_1000.mp4', 'shots/INT_1000/INT_1000.%04d.png'). These are NOT PathObject-relative, so ingest needs bespoke path-cobbling to rebuild the abs path. On a film there are millions of paths; anything outside structure multiplies glue code. Add an amc pathmap so each shot's quicktime + png-sequence resolve as PathObjects, and store them as alchemy-relative paths that add_root() reconstructs to abs on any network/remote machine. MSD path fields should ALWAYS be add_root-relative. Update manifest.py + declare PATH_FIELDS so msd_abs() absolutizes them.
Ontology · models7
#418save_standalone() writes to the globally-active project, not the asset's own projectneeds detailBug fixOntology · modelsclaude
·Ontology · models·Alchemy·3 months ago
An asset resolved from a NON-active project store must save back into that project. Today asset.save_standalone() resolves its write path from the global active company/project, so a script that does PROJECT_REGISTRY.get_project('loj', company_id='cgl') and saves a character writes the profile into whatever project happens to be globally active instead.
#531Give motion template layouts real slot geometry, so the transcript preview can draw where a template puts titlesreadyNew featureOntology · modelsclaudeScript
·Ontology · models·Alchemy·3 months ago
TemplateFamily slots (TXT_HEADLINE, IMG_LOGO) are typed but have NO geometry - the layout lives inside the .aep comp, so nothing outside After Effects can say where a template's headline sits. Add normalised slot rects to TemplateLayout (x/y/w/h as fractions of the variant frame, per slot), have the After Effects bridge read each comp's layer bounds per variant and write them back into the family manifest, then extend the transcript editor's preview guide layer (#trs-guide-layer, initPreviewFrame in app/tools/transcript/static/js/transcript_editor.js) with a 'Template' guide source alongside the existing delivery-variant safe zones - pick a family + variant, see its title/logo boxes over the shot.
#1383Asset saves never update the {env,prp,drs,chr}.json aggregates, and load() iterates aggregate keysreadyBug fixOntology · modelsclaude
·Ontology · models·3 months ago
Make the aggregated registry and the standalone profiles stop disagreeing, by fixing the write side rather than papering over it on read.
Today the two halves are inconsistent:
* SAVE writes the standalone profile ONLY. `AssetBase.save_standalone()` (alc/models/script/models/manifests.py:748) is what every asset edit calls -- app/routes/assets.py:349 (env/prp/drs dashboard), :409 (character dashboard), :458 (the /height endpoint), plus the prep band and the AI analysis passes. Nothing writes prp.json / env.json / drs.json / chr.json alongside it.
* LOAD treats the aggregate as the KEY INDEX and the standalone as the VALUE. `PropRegistry.load` (manifests.py:1121-1128) and its three twins (EnvironmentRegistry:1298, DressingRegistry:1198, CharacterRegistry:1013) iterate `registry.props.items()` parsed from the aggregate, then overwrite each entry from `<id>.json` when that file exists.
Two consequences, one cosmetic and one sharp:
1. Field values in the aggregate go stale and are simply wrong on disk. Harmless at read time, because the standalone override wins -- but the file is a lie to anything that reads it directly (a farm job, an external tool, a human).
2. THE REAL BUG: because the loop only visits keys present in the aggregate, a standalone profile whose id is MISSING from the aggregate is never read at all. The asset silently does not exist to the app -- no error, no log line, it is just gone. That makes the aggregate load-bearing for existence while being unmaintained on write. It is not a cache you can regenerate; it is a second source of truth that nothing keeps in sync.
Fix direction (implementer's call, but the constraint is: one write path):
either have the asset save paths update the aggregate too, or drop the aggregate's index role so that load() enumerates the standalone profiles on disk and uses the aggregate only for records that have no profile. Whichever way, after the change it must be impossible to create a standalone profile that the app cannot see.
Acceptance:
* Saving an asset leaves the aggregate and the standalone in agreement -- no second step, no manual resync.
* A standalone profile with no matching key in the aggregate is still loaded and still appears on the Props / Set Dressing / Environments dashboards.
* Round-tripping a registry does not drop unmodelled keys (see notes -- tnt/chr/chr.json).
#3275Carry the beat id into the exported timeline so a Premiere cut can come backreadyNew featureOntology · modelsclaudeScript
·Ontology · models·Alchemy·3 months ago
Stamp the BEAT ID onto the exported timeline's cells, in both the .otio and the FCP 7 XML,
so a cut that comes BACK from Premiere can be matched to the board it came from.
== WHY NOW ===============================================================
#3235 gave every exported cell a beat id in the Alchemy model (TimelineItem.metadata
["beat_id"], on clips AND gaps) and used it to make todo reconciliation a join rather
than a diff of free text. That worked.
But the id dies at the serializer. OtioAdapter.gap_to_otio() builds an otio.schema.Gap
with name + source_range and no metadata; the board's clip branch in item_to_otio()
(the `item.media_url and not item.manifest_relative_path` case) builds an otio.schema
.Clip with name + media_reference + source_range and no metadata. Compare clip_to_otio(),
which DOES carry `metadata={"alchemy": meta}` for manifest-backed clips -- the board's
cells simply never take that path.
So today the deliverable an editor opens has no stable handle on any beat. Clip NAMES
carry the block id or the first 40 characters of the description, both of which change
when someone edits the board.
== WHAT IT SHOULD DO =====================================================
1. Carry `beat_id` (and whatever else identity needs) into the OTIO clip/gap metadata
under the existing "alchemy" namespace, so the .otio round-trips it natively.
2. Get it into the FCP 7 XML too. FCP XML has no metadata element, so this needs a
decision -- a <comment>/<labels> field, a marker, or an encoding in the clip name.
Whichever it is, the requirement is that PREMIERE PRESERVES IT across a re-export
from Premiere's own side. Verify that rather than assume it: a field Premiere drops
on save is worse than no field, because it looks like it works.
3. Read it back. otio_gap_to_internal / the clip recovery path already recover
"alchemy" metadata for manifest-backed clips (see otio.py's "Recover Alchemy-specific
metadata" block); the board's cells need the same.
== THE TENSION, STATED SO IT IS DECIDED AND NOT DISCOVERED ===============
#3235 rejected text cards partly because they "put work-tracking information inside the
deliverable". A beat id looks like the same category of thing.
It is not, and the distinction is worth writing down: a text card is a TASK smuggled into
a picture -- it describes work, it is visible, and it has to be replaced. A beat id is
IDENTITY -- invisible, describes nothing, and is what makes a round-trip possible at all.
Every NLE interchange format carries ids for exactly this reason.
But it IS a decision, and Tom should make it before anyone builds: does the deliverable
get to carry Alchemy's ids?
== WHAT THIS UNBLOCKS ====================================================
Tom, while testing #3235 in Premiere: "i'm going to want to do things like update the
editor from premiere ... so there's parity there."
Parity means what goes out has to be identifiable when it comes back. The obvious first
round-trip is TIMING -- an editor re-cuts in Premiere, and the beats' screen_secs follow,
which is a clean mapping because screen time is already the authored quantity the lanes
are laid out against (#3235). Without a beat id that reconciliation would have to guess
from clip order, which breaks the moment anybody inserts, deletes or reorders a shot --
i.e. the moment anyone actually edits.
The Premiere bridge already exists (alc/plugins/premiere/bridge/server.py, the UXP panel)
and already publishes a sequence back as OTIO (#321 / the amc publish chain), so the
transport is there. The missing piece is the join key.
== SCOPE =================================================================
This ticket is the KEY ONLY -- stamp it, ship it, prove it survives Premiere. The
actual "update the board from a Premiere cut" feature is its own ticket and should not
start before this one lands.
#264Camera/sensor model system — normalized bodies + sensor modes from the vfxcamdb datasetreadyNew featureOntology · modelsclaude
·Ontology · models·3 months ago
Build a proper camera/sensor model as the real backing for the camera-body library (supersedes the hand-seeded 10-body stopgap in alc/config/cameras.py from #263). SOURCE DATA already exists: C:/Users/tmiko/PycharmProjects/storyforge/resources/cameras - 214 JSON files exported from the VFX Camera Database (vfxcamdb.com), each with {camera_make, camera_model, sensor_type, shutter_type, sensor_modes:[{resolution:[w,h], sensor_area_mm:[w,h], aspect_ratio}]}. A camera has MANY sensor modes (open gate / S35 / 16:9 / anamorphic), so the model is body + modes, not a single sensor size. MODEL: a normalized pydantic CameraBody/SensorMode in alc/models/camera/ (__init__.py is empty; query.py already holds a vfxcamdb scraper + CameraSpecs string-parse model - reconcile: scraper = acquisition pipeline that produced the storyforge JSONs, new model = clean runtime shape; dedup/merge, don't leave two camera models drifting). INGEST the 214 JSONs into an Alchemy resource/config location as the library data. CONSUMERS: #263's get_camera_bodies/set_camera_body point at this model; picking a body AND a mode yields the CineCamera filmback (sensor_area_mm) AND optionally the render resolution; Camera Caddy Camera Body dropdown gains a mode sub-select. Resolves the BP_MagiCam filmback caveat (#258) with real per-mode sensor data instead of nominal values.
#324Ingest commit doesn't register new assets in the category registryreadyBug fixOntology · modelsclaude
·Ontology · models·3 months ago
IngestManifest.commit() creates the version via create_empty_version() and copies sources with shutil.copy2(), but never registers the asset in its category registry (chr/env/prp/drs). It bypasses TaskManifest.commit, the only caller of register_asset() (the single registry writer). Fix: after each commit group's create_empty_version() succeeds, if entity_type=='assets' and parent in {chr,env,prp,drs}, call register_asset(self.project, group['parent'], group['child'], label=...). Honor the choke-point contract (idempotent, single writer). Note register_asset is guarded to CFG.project, so confirm the ingest commit runs with the target project active, or lift that guard for the ingest path. Real example: bse/imm chr 'ziya' was ingested but only showed up in the browse because a later scr publish happened to register it.
#225De-hardcode the feature registry path in resolve_candidate_filesneeds detailChoreOntology · modelsclaude
·Ontology · models·3 months ago
alc/models/work_item.py::resolve_candidate_files reads the feature registry from a hard-coded absolute path (E:/Alchemy/cgl/ppe/PUBLISHES/msd/features/registry.json) and does so synchronously on every hydration. Resolve it through the taxonomy layer's features_root()/registry.json helper instead (as PR #26 already did for the sibling read it touched), so it is portable across companies/projects and not pinned to the ppe board. While here, consider whether the per-hydration synchronous read should be cached. Surfaced during the cookbook sprint (PR #26).
App · transcript
711
#629RationalTime.value should be INT frames — one float→frame boundary, not one per callerneeds detailChoreApp · transcriptclaude
·Ontology · models·Alchemy·3 months ago
Durations should be carried as whole FRAMES. An int cannot be got wrong by a rounding error; a float second can, and did (the row-select bug on Create Script: two clips whose seconds rounded opposite ways ended up overlapping by one frame).
WHAT IS ACTUALLY THERE NOW (alc/models/edit/models.py:15)
class RationalTime(BaseModel):
value: float # The frame number or elapsed ticks count
rate: float # (e.g. 24.0, 30.0, or 1.0 for straight seconds)
Two problems in three lines:
1. value MEANS a frame number but is TYPED float, so nothing stops a fractional frame being stored. Today every producer happens to pass an int-valued float by convention - except alc/models/edit/otio.py:582 (FCPXML rate conform, value=rt.value * scale), which yields genuine fractional frames on a 23.976<->24 conversion.
2. rate=1.0 is documented to mean the value is straight SECONDS. So the same field means frames at 24 and seconds at 1 - one field, two currencies, and no way for a reader to know which without inspecting rate.
WHAT TO DO
- Type value as int and validate it on construction (reject/round explicitly, don't silently truncate).
- Give the float->frame conversion ONE home: a RationalTime.from_seconds(seconds, fps) classmethod that rounds once. Callers stop doing round(x * fps) inline (14 construction sites today) - so a rounding rule can never differ between two of them, which is exactly how the overlap happened.
- Decide what rate=1.0 means and stop overloading: either seconds get their own type/field, or 'seconds' stops being expressible here.
- Real-media durations (a wav is 1.8734s, STT word times are float seconds) legitimately arrive as floats: quantize at the BOUNDARY where they enter a timeline, and keep everything downstream integer.
- Audit the FCPXML rate conform for the fractional frames it can produce.
DONE WHEN: RationalTime.value is an int, there is exactly one seconds->frames conversion in the codebase, and a test pins that a ragged (non-frame-aligned) input still yields butt-joined integer spans.
#1175A board's previs has three time bases: picture cuts on estimates, voice plays on measured audioreadyBug fixApp · transcriptclaude
·App · transcript·3 months ago
A beat board's previs now imports both MagiCams and voice - and they run on different clocks. Picture ends at frame 485 while the last board holds for ~19 seconds with five more voice clips playing under it.
THREE TIME BASES FOR ONE BOARD, all emitted by the same to_script_model() manifest:
* audio_track - 16 clips, 933 frames total (MEASURED voice lengths)
* storyboard_track - 14 clips, 808 frames total (also voice-timed)
* blocks duration_frames - 16 blocks, 622 frames total (AUTHORED estimate)
* scene.duration_secs - 25.91s (~622 frames), agreeing with the estimate and with neither track
magicam times the camera cuts from the block estimates: the MagiCam marker deltas are 53, 26, 20, 43, 14, 48, 48, 38, 26, 43, 35, 39, 51 - an exact match for block.duration_frames. The audio strips are laid on the measured base: 68, 61, 44, 58, 40, 57, ... which is what storyboard_track also uses. So the cut points and the voice can never line up.
DECIDE AND STATE - THIS IS THE WHOLE TICKET: which clock is authoritative for a board.
* MEASURED voice wins: a beat's picture holds exactly as long as its line takes to say, and duration_frames becomes a pre-recording estimate that the rendered take supersedes. This is what an animatic normally means, and storyboard_track already agrees with it.
* AUTHORED estimate wins: the board's timing is a deliberate creative statement and audio gets trimmed or padded to it.
Pick one and make every consumer read it. Do NOT leave two tracks disagreeing and let each consumer choose - that is the defect.
ALSO: scene.duration_secs must follow whichever wins - the prep band and the schedule read it, so a board whose picture and duration disagree misreports its own length.
DONE WHEN: a board's previs plays picture and voice on one clock; the MagiCam markers and the sound strips agree; and scene.duration_secs matches what the previs actually is.
#1176One camera per beat: boards that belong to one setup should be frames on one MagiCamreadyNew featureApp · transcriptclaude
·App · transcript·3 months ago
Previs exports ONE MagiCam per beat. On the theaxe board that gave 14 cameras where roughly 6 boards are one continuous setup - successive drawings of the same shot, not 6 shots. The result over-complicates the scene and the artist has to reconcile 6 cameras that should have been one.
MAGICAM ALREADY DOES THIS - it was built for TVPaint that way. cookbook/../magicam/blender/build.py loops the clip's LAYERS into image_plane_01..10 under a SINGLE camera, and set_key_frames() keys the image sequence over time. One camera, many drawings, keyed. The capability is not the gap.
THE GROUPING ALREADY EXISTS TOO - AND NOTHING READS IT
alc/models/edit/models.py:1283 declares alongside , and models.py:89 defines a Shot clip - 'a shot segment on the shots track', carrying shot_type and its own clip_range. ScriptScene._build_shot_track() (script.py:512) already SEEDS it: a run of consecutive action blocks becomes ONE shot, each dialogue block its own shot - derived from the effective voice track so it stays frame-aligned. already means a human committed manual cuts and those win over the derived ones (script.py:586-596, 714-715).
And magicam explicitly THROWS IT AWAY: magicam/alchemy/models.py:118 says unknown fields 'e.g. shot_track, shots_locked, clip shot_type' are ignored via extra=ignore.
So this is a read-side task, the same shape as #1026: the write side is built and the consumer doesn't look at it.
WHAT TO BUILD
* magicam's alchemy adapter consumes shot_track: one MagiCam per SHOT, with each beat inside that shot becoming a keyed frame/plane on it, instead of one camera per block.
* The beat board editor surfaces the grouping so the artist can say 'these six beats are one camera' - i.e. edit the shot boundaries and set shots_locked when they do. There is currently no camera/shot distinction visible on the board at all, which is why it defaults to one-per-beat.
NAMING - DECIDE BEFORE BUILDING, THE WORD IS TAKEN TWICE
alc/models/framing.py already defines as the shot VOCABULARY - a composition of Framing + ShotRole + CameraAngle + CameraMovement, i.e. HOW a shot is described. alc/models/edit/models.py defines a different - a segment on the shots track, i.e. WHICH beats a shot spans. Two models, one word, both live. State which one the board's grouping means (it is the second) and how the two relate - because the first is the natural payload OF the second: framing, angle and lens are properties of the CAMERA, not of each drawing under it.
WORTH NOTING: framing.Shot is populated NOWHERE today - is None on all 16 blocks of the live theaxe board. A camera-level grouping is the natural place to hang it, and hanging it per-drawing was probably always the wrong altitude.
DONE WHEN: a run of beats that share a setup exports as ONE MagiCam with the beats as keyed frames; the artist can group and re-group beats into cameras on the board and the grouping survives a rebuild (shots_locked); and a board of 14 beats that is really 8 setups produces 8 cameras, not 14.
OUT OF SCOPE: camera placement (Tom: placement is not the bottleneck - 10 cameras in 3-4 minutes); the multiplane depth workflow itself.
#1177One camera per beat: boards that belong to one setup should be frames on one MagiCamreadyNew featureApp · transcriptclaude
·App · transcript·3 months ago
Previs exports ONE MagiCam per beat. On the theaxe board that gave 14 cameras where roughly 6 boards are one continuous setup — successive drawings of the same shot, not 6 shots. The result over-complicates the scene, and the artist has to reconcile 6 cameras that should have been one.
MAGICAM ALREADY DOES THIS — it was built for TVPaint that way. magicam/blender/build.py loops the clip's LAYERS into image_plane_01..10 under a SINGLE camera, and set_key_frames() keys the image sequence over time. One camera, many drawings, keyed. The capability is not the gap.
THE GROUPING ALREADY EXISTS TOO — AND NOTHING READS IT
alc/models/edit/models.py:1283 declares a `shot_track: Track` field alongside `shots_locked: bool`, and models.py:89 defines a `Shot` clip — "a shot segment on the shots track" — carrying `shot_type` and its own `clip_range`.
ScriptScene._build_shot_track() (script.py:512) already SEEDS it: a run of consecutive action blocks becomes ONE shot, each dialogue block its own shot, derived from the effective voice track so it stays frame-aligned with what the artist sees.
`shots_locked` already means a human committed manual cuts, and those win over the derived ones (script.py:586-596 and 714-715). So "the artist regrouped these beats and it must survive a rebuild" is already modelled.
And magicam explicitly THROWS IT AWAY: magicam/alchemy/models.py:118 documents that unknown fields — naming `shot_track`, `shots_locked` and clip `shot_type` — are ignored via the model's extra="ignore".
So this is a READ-SIDE task, the same shape as #1026: the write side is built, and the consumer does not look at it.
WHAT TO BUILD
* magicam's alchemy adapter consumes the shot track: one MagiCam per SHOT, with each beat inside that shot becoming a keyed frame/plane on it, instead of one camera per block.
* The beat board editor surfaces the grouping, so the artist can say "these six beats are one camera" — edit the shot boundaries and set shots_locked when they do. There is no camera/shot distinction visible on the board at all today, which is why it defaults to one-per-beat.
NAMING — DECIDE BEFORE BUILDING, BECAUSE THE WORD IS TAKEN TWICE
alc/models/framing.py defines a class named `Shot` meaning the shot VOCABULARY: a composition of Framing + ShotRole + CameraAngle + CameraMovement — HOW a shot is described.
alc/models/edit/models.py defines a DIFFERENT class also named `Shot`: a segment on the shots track — WHICH beats a shot spans.
Two models, one word, both live in the codebase right now. State which one the board's grouping means (it is the second) and how the two relate — because the first is the natural payload OF the second: framing, angle and lens are properties of the CAMERA, not of each drawing under it.
WORTH KNOWING: the framing vocabulary is populated NOWHERE today — the per-block `shot` field is None on all 16 blocks of the live theaxe board. A camera-level grouping is the natural place to hang it, and hanging it per-drawing was probably always the wrong altitude.
DONE WHEN: a run of beats sharing a setup exports as ONE MagiCam with those beats as keyed frames; the artist can group and regroup beats into cameras on the board and the grouping survives a rebuild; and a 14-beat board that is really 8 setups produces 8 cameras, not 14.
OUT OF SCOPE: camera PLACEMENT (Tom, measured: placement is not the bottleneck — 10 cameras in 3-4 minutes); the multiplane depth workflow itself.
#1328Route test — capture a feature from the header pillneeds detailNew featureApp · transcriptTomMotion Capture
·App · transcript·Alchemy·3 months ago
#1817Per-video style spine is only editable in Prompt mode, so the video's own look is invisible in the other tworeadyNew featureApp · transcriptclaudeScript
·App · transcript·Alchemy·3 months ago
The beat board's STYLE rail renders the Style Prompt textarea only when style_mode == 'prompt' (transcript_style.html:43); in Reference and Storyboard mode the spine is kept but hidden behind a hidden input. That was right when the spine ONLY fed prompt-mode image generation. It stopped being right with the style-frame column (#1748): a style frame is the FINISHED LOOK by definition, so it injects the resolved spine whatever mode the video is in - meaning a video in Storyboard mode now has a spine that drives real output and no field to see or set it. Tom hit this directly on loj/theaxe (Storyboard mode): changing things in the rail did not move the style-frame prompt, because the only control that would have was not on screen. WANTED: the video's effective spine is always VISIBLE in the rail - editable in every mode, or at minimum shown read-only with the inherited project value beside it and a note saying which passes read it. Tom: 'the video's style isn't visible - we're going to have to make that clearer later because this is a feature we want - video specific style.'
#1923Route test — capture a bug from the header pillneeds detailBug fixApp · transcriptTomMotion Capture
·App · transcript·Alchemy·3 months ago
#2734Route test — capture a bug from the header pillneeds detailBug fixApp · transcriptTomMotion Capture
·App · transcript·Alchemy·3 months ago
#2990A coordinate URL without ?u= / ?v= renders a degraded editor instead of resolving the versionreadyBug fixApp · transcriptclaude:Script
·App · transcript·Alchemy·3 months ago
Opening a canonical coordinate URL WITHOUT its query tail — e.g. /c/cgl/project/loj/transcript/at/videos/theaxe/SEQ/scr — leaves the PathObject with no user and no version. The page still renders, but degraded and silently: the phase-header context logs 'RouteEngine could not resolve: .../scr/default.msd' and is dropped, and the Previs Prep band never draws, so the whole previs control the band gates is absent. Adding ?u=tom&v=000.000 makes the same page render correctly.
Either the /at/ route should resolve the missing tokens to defaults (the logged-in user, the latest version) the way _editor_task does for the query-context path, or a coordinate missing user/version should be an explicit redirect to the resolved one — but a half-resolved coordinate should not quietly render a page with features missing.
#3221Shots holding BOTH a styled and a tween clip show the wrong one downstreamreadyBug fixApp · transcriptclaudePrevis
·App · transcript·Alchemy·3 months ago
A shot's style_frame folder can hold both style_frame.{fs}_styled.mp4 and style_frame.{fs}_tween.mp4 — two different Seedance results under two different stems. The scene timeline's scan (alc/services/media/prv_media.py: ensure_styled_offsets and the STYLE FRM/video cell builder) globs '*_styled.hero.mp4' FIRST and falls back to the square, so it silently prefers the styled clip whatever the artist last asked for.
Live example today: loj/theaxe 010_010_0120_MagiCam holds both, and the beat board's v0017 points at style_frame.0395_tween.hero.mp4 while the downstream scan reads the styled one. Board and timeline disagree about what this shot is.
#3206 fixed this going FORWARD — a take made live retires the other stem into the takes/ archive, so only one canonical clip exists per shot from the next pass or the first 'Make live' onward. What is left is the EXISTING shots on disk: nothing reconciles a shot that already holds both until somebody touches it.
Wanted: a one-off reconcile that walks a render version, and for each shot holding both stems, adopts both into the take archive (ShotTakes.adopt_canonical) and retires whichever stem the BEAT is not pointing at — i.e. makes the artist's choice the live one. Report what it changed rather than doing it silently.
#535Stale test: transcript-mode rows DO get a visual now (beat board)needs detailBug fixApp · transcriptclaudeScript
·App · transcript·Alchemy·3 months ago
tests/test_visual_lane_overlap.py::test_transcript_mode_row_gets_no_visual asserts a transcript-mode row is born WITHOUT a visual. It fails on main (verified by stashing all local work): TranscriptEdit.add_row now runs _ensure_row_visual for every row, because the beat board made row:visual 1:1. Decide which is true — either the beat-board invariant is universal and the test should assert the row DOES get its one visual, or transcript mode is meant to stay visual-less and add_row should skip _ensure_row_visual there. Then fix whichever side is wrong so the suite is green.
Quick entry · agent-ready tickets
13
#3295Add the panel Request field to the Blender and Substance panelsreadyNew featureQuick entry · agent-ready ticketsclaude
·Plugin · blender·Blender·3 months ago
The Premiere panel now carries the app header's Request field (#3276), and the filing route it uses is generic: POST /c/{company}/request/api (app/tools/kanban/request.py) takes text/kind plus software, window, company, project, alc_path, page, view_route, and answers with the card number and its /alc_do_ticket command. alc/plugins/premiere/bridge/server.py POST /request is the reference implementation — it grounds a PathObject from the open project, names software='premiere' so plugin_facets() routes the card to the plugin's own area, and points the evidence camera at the Premiere WINDOW because headless Chrome cannot photograph a DCC. Do the same for the Blender dashboard (bridge on :9876) and the Substance Painter panel (:20260): one field on the panel header row, everything else filled in from the live session.
#3315Ticket candidate files hydrate but are never selected, so the handoff prompt's Relevant Files block stays emptyreadyAdjust existingQuick entry · agent-ready ticketsclaude
·App · kanban·Alchemy·3 months ago
WorkItem.resolve_candidate_files() ranks real files (a Premiere request scoped to Plugin - premiere - edt resolved 24 files, with the three marker js ops correctly on top), but the selection filter at the end of that method only marks a candidate selected when score >= 8.0, or when a direct UI-keyword/template hit set the reason to html_ui / renders_template / explicit_note / cookbook_context. A request filed from a DCC panel has no UI keywords, so nothing clears the bar and _candidate_files_block() returns '' - the agent gets the area scope line but never the ranked file list underneath it. Either select the top N when there are no direct hits, or lower the threshold for area_default candidates.
Quick entry is one field, and the one thing it cannot express is the single strongest signal the ticket enricher has: the on-screen LABEL of the thing being talked about. WorkItem.resolve_candidate_files() scores a file far higher when a ui_keyword appears in its templates/markup (reason html_ui / renders_template), and those direct hits are also what flips candidates to selected — so a request filed with no keywords ranks its area's files but surfaces none of them.
Add an inline convention to the request text, parsed at capture time. Proposed: a '#uikey ' marker consuming a comma-separated list to end of line —
the marker sync button should run on open #uikey Sync markers, Task type
— stripped out of the title before the card is created and passed through as ui_keywords. Worth a look at the alternatives before committing: inline [Bracketed Labels] anywhere in the sentence (no trailing-position rule, reads naturally, but collides with prose), or ackticked labels (familiar from markdown, same collision risk). Pick one and teach it in the field's placeholder and tooltip.
Parse it in file_request() in app/tools/kanban/request.py — that is the single composition every capture surface already goes through, so the web header pill and every DCC panel (Premiere today, Blender/Substance under #3295) get it at once, with no per-surface UI. add_item() already takes ui_keywords; nothing downstream needs to change.
Ontology3
#77Ontology page: inline reassign/rename to fix flagged litterreadyNew featureOntologyclaude
·Alchemy·3 months ago
On the Ontology page (Pipeline tab), let a user act on a lint finding in place instead of only seeing it. Add inline edit affordances on each page/tool row to correct a bad label, route, or workspace assignment — mirroring how the Features/Activity Report tool lets you retype a file's classification. Edits write to app/navigation.py's registries (TOOL_PATHS / SIDEBAR_NAV / SECTIONS) or a companion override store, then re-run lint_ontology() so the finding clears live. Turns the monitoring surface from read-only into a fix surface.
#78Ontology page: background thumbnail sweep for full render-lint coveragereadyNew featureOntologyclaude
·Alchemy·3 months ago
The Ontology page's identical-render lint (average-hash comparison of captured page thumbnails) only covers pages that have been captured, so today it's near-empty until someone clicks Capture on each row. Add a background sweep that captures thumbnails for all registered pages/tools without a manual per-row click — self-retriggering like the Features tool's cache-step indexer, honoring the screengrab service's per-page cost, and best-effort (a failed page never breaks the batch). Result: the look-alike-render flags have full coverage automatically, so Script-vs-Transcript-style pages that render nearly identically are caught without manual effort.
#79Ontology page: fold in the code-side area/feature taxonomyreadyNew featureOntologyclaude
·Alchemy·3 months ago
The Ontology page currently monitors only the UI->pages taxonomy (navigable tools/routes from app/navigation.py) and links out to the Activity Report for the code-side areas/features. Fold the code-side taxonomy in so 'areas & features' live on the same monitoring board: join the FeatureRegistry (alc/models/feature.py — areas + derived features) alongside the pages lens, and extend lint to cross-check them (e.g. an Area with no owning page, a page whose owning workspace has no matching code area, orphaned features). Keep it a pure reducer; reuse alc/services/ontology.py. Goal: one ontology surface covering both what the app OFFERS (pages) and what it IS (features), not two separate tools.
alchemy
276
#495[cookbook] [alchemy] [dsn] Implement the Generate Model From T-Pose buttonreadyNew featurealchemystub
·Cookbook · alchemy · build · dsn·Alchemy·2 months ago
Generate a character model from the identified t-pose file in the design task.
#4285[cookbook] [alchemy] [scr] Implement the scr shelf/menureadyNew featurealchemystub
·Cookbook · alchemy · menus · scr·Alchemy·2 months ago
#4429[cookbook] [alchemy] [dsn] Implement the Ensure Required Views buttonreadyNew featurealchemystub
·Cookbook · alchemy · publish · dsn·Alchemy·2 days ago
Ensure the required views for this category are met. alc\plugins\alchemy\tasks\dsn\tags.py contains the REQUIRED_TAGS dict we can pull from.
#4437[cookbook] [alchemy] [dsn] Implement the Ensure Design Camera buttonreadyNew featurealchemystub
·Cookbook · alchemy · publish · dsn·Alchemy·2 days ago
Ensure we create a blend file for the design camera based on the data in the json file we've created in our breakdown and scale work. the camera should include the ground plane a blender camera with our camer's settings, and an image plane at 30% attached to the camera to for a complete reference
#334[cookbook] [alchemy] [dsn] Implement the Ensure Headshot buttonreadyNew featurealchemystub
·Cookbook · alchemy · build · dsn·Alchemy·2 months ago
Create a Headshot the same way we do on the character dashbord.
#335cookbook Design: alchemy build Generate Model should only use tpose images for generating the model for a character. (for environments it can use "front or "mainneeds detailNew featurealchemyTomDesign
·Cookbook · alchemy · build · dsn·Alchemy·2 months ago
unreal
611
#325[cookbook] [unreal] [prv] Implement the Import Connected Rigs buttonreadyNew featureunrealstub
·Cookbook · unreal · build · prv·Unreal Engine·3 months ago
Find all published Rigs connected to this scene and import them into Unreal - we'll essentially be running the unrea / rig / build recipe on each one.
#336[cookbook] [unreal] [prv] Implement the Ensure Scene Level buttonreadyNew featureunrealstub
·Cookbook · unreal · start · prv·Unreal Engine·3 months ago
Ensure that there's a [SceneName]_SEQ_lvl witha ground plane and a sky at a minimum
#337[cookbook] [unreal] [alchemy] Implement the Render Manager buttonreadyNew featureunrealstub
·Cookbook · unreal · menus · Alchemy·Unreal Engine·3 months ago
Opens up the Render Manager so you can submit renders from the UI.
#338[cookbook] [unreal] [alchemy] Implement the HDRI Picker buttonreadyNew featureunrealstub
·Cookbook · unreal · menus · Alchemy·Unreal Engine·3 months ago
Launches the HDRI tool
#339[cookbook] [unreal] [alchemy] Implement the Camera Caddy buttonreadyNew featureunrealstub
·Cookbook · unreal · menus · Alchemy·Unreal Engine·3 months ago
Launches the Camera Caddy
#340[cookbook] [unreal] [prv] Implement the Ensure Sequence Levels buttonreadyNew featureunrealstub
·Cookbook · unreal · build · prv·Unreal Engine·3 months ago
INT_SEQ_lvl — parent: empty container, streams the rest
INT_SEQ_lay_lvl lay ground, env, set
INT_SEQ_prv_lvl prv previs blocking/proxies
INT_SEQ_cam_lvl cam the mCams + plates
INT_SEQ_lgt_lvl lgt HDRI sky, lights
#4026[cookbook] [unreal] [prv] Implement the Import Design GLB buttonreadyNew featureunrealstub
·Cookbook · unreal · build · prv·Unreal Engine·2 months ago
Import the GLB file from the design publish
#4030[cookbook] [unreal] [prv] Implement the Setup HDRI buttonreadyNew featureunrealstub
·Cookbook · unreal · build · prv·Unreal Engine·2 months ago
Setup the HDRI for the scene, start with a basic sunny day like: mondarrain_3_thumb, put it in the lighting sublevel, ensure that level is in the location level (it already should be)
·Cookbook · unreal · build · prv·Unreal Engine·2 months ago
Ensure that a project exists for the location this scene or video is connected to.
#326[cookbook] [unreal] [prv] Implement the Import Environment Model buttonreadyNew featureunrealstub
·Cookbook · unreal · build · prv·Unreal Engine·3 months ago
Import the environment fbx if it's available. Import and attach the textures if they are available as well.
#267UE Motion NLE via Sequencer — realize the motion Timeline in UE + motion-library accessreadyNew featureunrealclaude
·Unreal Engine·3 months ago
Motion-clip sequencing/editing for characters in UE, using UE SEQUENCER as the native NLE (it's already excellent — do NOT port Alchemy's NLE UI), fed by the Alchemy motion library. This is the mtn.create.apply_sequence(Timeline) leg for UE — the contract already anticipates it ('sequence realization building native NLA/Time-Editor/Sequencer', smart_task/mtn.py). Depends on #266 (retarget_clip primitive + calisthenics).
RECOMMENDED APPROACH — leverage native, sync the data:
(1) apply_sequence(Timeline) for UE: given an Alchemy Timeline (source-of-truth NLE model — ordered MotionClips per character with clip_start_frame/clip_duration/source_in-out/blends, see alc/models/motion.py), build/populate a Level Sequence: one skeletal-animation track per character, each clip a section at its frame, retargeted via retarget_clip (#266). Sequencer's native trim/blend handles crossfades. Mirror blender mtn create.py apply_sequence + set_track + bake_track.
(2) Motion library access in UE: reach the library from the connected session — pre-land library clips as AnimSequences at library coordinates + retarget-on-demand (MVP: Sequencer asset browser); a RigCaddy-style motion picker (see rig_caddy/timeline.py, ui.py) later. Reuse MotionLibrary catalog + previews.
(3) Blocking bridge: rig-task marks already attach a clip (rig.edit.set_mark_motion); apply_sequence lays those clips at mark frames so director's blocking populates Sequencer.
(4) Round-trip / source of truth: Alchemy Timeline is authoritative (1:1 NLE). Timeline->Sequencer build first (previs); an mtn.query read-back of Sequencer sections -> Timeline keeps the 1:1 later.
OPEN DECISIONS (triage): authoritative surface (recommend Timeline-drives-build, Sequencer read-back later, NOT Sequencer-as-master); library-in-UE (pre-land AnimSequences + Sequencer browser for MVP vs in-session picker); retarget caching (cache baked AnimSequences at coordinates, idempotent like image_to_mesh — retarget-per-character-clip is expensive).
REFERENCE: smart_task/mtn.py (apply_sequence/set_clip contract), blender/tasks/mtn/create.py (apply_sequence/set_track/bake_track/calisthenics_turntable to mirror), alc/models/motion.py (MotionClip/Timeline/MotionLibrary), rig_caddy/timeline.py (app-side NLE), rig task marks. Dependency: motion library needs UE-accessible clips (same as #266).
Two cosmetics from #234: (1) 'No module named numpy' fires in an optional HDRI-light path during build_turntable inside headless UE (caught; the step passes; we don't add lights). Either guard the numpy import so it never logs, or install numpy carefully (UE bundles its own — risk of version clash, hence excluded from setup requirements). (2) The Plugin loader logs 'unreal is missing query/create module' because Plugin.model_post_init expects flat alc.plugins.unreal.{create,query} modules, but the etymology puts ops under tasks/{task}/{verb}. Make the loader not warn for a DCC that uses the tasks layout (or provide thin shims).
test_parity.py reports 3 pre-existing contract-drift failures (unrelated to any one feature): (1) unreal mdl/create.py import_file adds game_folder/asset_name kwonly args beyond the ModelCreate contract signature — either lift them into the contract or drop them from the op; (2) unreal prv/query.py is missing contract ops get_shot_cameras, list_sequences (PrevisQuery); (3) maya prv/query.py is missing the same two PrevisQuery ops. Reconcile concrete<->contract so contract-⊆-concrete holds with exact signatures.
#656Work items reference area_ids the feature registry no longer has (svc-unreal, alc-plugins, plugin-unreal-cam...)readyBug fixPlugin · unrealclaudeReference
·Alchemy·3 months ago
Several board items carry an area_id that FeatureRegistry has no entry for: svc-unreal, alc-services, alc-plugins, plugin-premiere-amc, plugin-unreal-cam, plugin-alchemy-camera-caddy. Because the card renders its area via area_names.get(area_id), those cards show NO area at all, and anything driven by the taxonomy (candidate-file resolution via path_prefixes, the new sprint planner's code-path grouping) degrades for them. Decide and implement one of: (a) re-register the missing areas from their ids/paths so the taxonomy claims them again, or (b) migrate the items onto the areas that superseded them. Then add a guard so an item can't be saved with an area_id absent from the registry.
#3862Third-party plugin redistribution — carry licenses and pin upgrades deliberatelyafter #3860blocked by #3860ChorePlugin · unrealclaude
·3 months ago
Alchemy's setup() bootstraps now install third-party software onto machines Alchemy does not own — mGear onto Maya, and with #3860 the Khronos glTF plugin onto 3ds Max. On a client or farm machine that is redistribution, and each license has terms that travel with it. Do three things. (1) Make the installers deploy the upstream LICENSE and NOTICE next to the binaries: the Khronos release zip ships NEITHER, so gltf.py must fetch them from the repo rather than assume the zip is self-describing. (2) Write down the pinning policy the pinned-release constants already imply — a version bump is a deliberate, tested change, not a float, and the ticket that bumps it says what was verified. (3) Add a short section to the docs listing every third-party dependency Alchemy installs on a user's behalf, its license, and where it lands on disk. Done when: a fresh install carries its licenses, and one page answers 'what does Alchemy put on my machine and under what terms'.
Two non-fatal wrinkles from the #234 headless run: (1) UnrealEditor-Cmd segfaults on SHUTDOWN after the recipe completes (rc 3221225477 / 0xC0000005). The work + the 'ALC HEADLESS: recipe complete' marker land first, and run_recipe_headless keys success off the marker not rc — but a farm job may treat rc!=0 as failure, so make the exit clean (or make the marker the authoritative success signal for Deadline). (2) A MovieScene section-range ensure fires during build_turntable (Ensure: SectionRange upper bound >= NewStartFrame, MovieSceneSection.cpp:270) — set the transform/camera-cut section ranges so the ensure doesn't trip.
#262UE MagiCam: extended camera controls (DOF / aperture / focus / shake / roll / pan)readyNew featurePlugin · unrealclaude
·Plugin · unreal·Unreal Engine·3 months ago
Extend BP_MagiCam (#258) and cam.edit (#260) BEYOND the Blender MagiCam 1:1 set - these are wanted here (supersedes the 'do not add' note in #258). Group by mechanism: (A) NATIVE CineCamera, near-free - focus distance (manual + tracking), aperture/f-stop, depth-of-field enable. Expose as set_focus(distance)/set_aperture(fstop)/set_dof(bool) + Caddy controls. (B) ROTATION CHANNELS on the rig components - roll (camera Z-rot) and pan (yaw), add as controls alongside Tilt: select_control gains 'roll'/'pan', or dedicated set_roll/set_pan. (C) PROCEDURAL - camera shake via a UE Camera Shake source / handheld noise component on the rig, with an intensity control (the meatier bit; previs-good-enough). All keyable via key_camera and surfaced in the Camera Caddy UMG (#261). NOTE (follow-up, not now): once proven here, back-port these same controls into the Blender MagiCam rig (magicam-blender caddy) so the two stay at parity.
#235Pin Unreal launch to the .uproject's engine version (avoid 5.6→5.7 convert prompt)readyAdjust existingPlugin · unrealclaude
·Alchemy·3 months ago
open_project() currently launches with Unreal.detect() (the newest local install, e.g. 5.7). Opening a project authored in an older engine (forest.uproject is 5.6) makes UE show a version-select / conversion prompt. Read the target version from the .uproject's EngineAssociation (or let the user pick) and launch the matching UnrealEditor when installed, falling back to detect() with a clear note. Surface the chosen version in the connection header (it already shows 'UE <ver>').
#237init_unreal.py hardcodes ALC_ROOT/CB_ROOT — derive them for portabilityneeds detailChorePlugin · unrealclaude
·Plugin · unreal·Alchemy·3 months ago
alc/plugins/unreal/setup/init_unreal.py hardcodes ALC_ROOT (C:/Users/tmiko/PycharmProjects/alchemy) and CB_ROOT (E:/Alchemy/cgl/config/cgl-cookbook). This is UE's startup hook that puts alc + the version-scoped site-packages + cookbook on sys.path. Derive these instead: ALC_ROOT from the file location (walk up from __file__ to the repo root), CB_ROOT from an env var (ALC_COOKBOOK_ROOT, exported by configure_unreal_python_env / the launch) or a small sidecar the project-env writer drops next to the .uproject. Blocks running Alchemy-in-UE on any machine but Tom's.
#257Auto-track detected Unreal version instead of a pinned default in setup.UnrealreadyChorePlugin · unrealclaude
·Plugin · unreal·Unreal Engine·3 months ago
alc.plugins.unreal.setup.Unreal has a hard-pinned 'version' class default (now '5.8'). detect() already picks the latest installed UE under Epic Games, but any code path that constructs Unreal() directly (not via detect()) uses the pinned default, which goes stale on every engine upgrade — as happened going 5.6 -> 5.8 (deps installed into the wrong version-scoped site-packages until re-pinned). Make the default resolve to the detected install so an engine upgrade needs no code edit: e.g. compute version lazily from get_local_versions()[-1] (or the running editor path like init_unreal._ue_version does), keeping an explicit override for pinning.
App · web ui7
#425to switch versions i have to refresh the page.needs detailBug fixApp · web uiTom
·Cookbook · blender · refresh · srb·Alchemy·3 months ago
#1595Versions view: image-sequence rows still have no right-click menureadyBug fixApp · web uiclaude
·App · web ui·Alchemy·3 months ago
On the versions view file zones, the right-click context menu now works on file rows and folder rows (#1594). The third row kind -- an image SEQUENCE row (kind == 'sequence' in app/templates/partials/version/_browse_row.html, rendered with the 'seq' badge and film icon) -- still has no oncontextmenu handler, so right-clicking a sequence does nothing. A sequence is not a single path (it is a frame pattern like foo.####.exr plus a directory), so it needs its own menu shape rather than reusing the file one: likely 'copy folder path', 'view in Explorer', and possibly delete-the-whole-sequence. Decide which actions make sense before wiring it.
#2146Version view back arrow does not push its URL, so the address bar stays on the version coordinatereadyBug fixApp · web uiclaude
·App · web ui·Alchemy·3 months ago
The back arrow in the version view header should carry hx-push-url so clicking it writes the asset-grid URL to the address bar, matching the task pills (which do push, via the server's HX-Push-Url response header).
#249Agent-chat: surface full traceback on step errorsreadyNew featureApp · web uiclaude
·Cookbook engine·3 months ago
When a recipe/preflight step errors, the Alchemy agent-chat surfaces only a one-line message (e.g. 'Hit an error during Render Turntable: module unreal has no attribute EditorAssetLibrary'), composed in cli_runner.run_preflights. Carry the FULL Python traceback into the event/bug payload and render it in the chat (collapsible if long) so debugging is self-sufficient without digging into Deadline/worker STDOUT. PreflightCheck.fail_check/update already has the feedback; capture traceback.format_exc() at the except site in cli_runner and include it in the emitted event + log_bug traceback.
#26run a check to see if we can find areas in the app where we're not updating the footer path as we clickneeds detailBug fixApp · web ui
·App · web ui·Alchemy·3 months ago
we need to come up with a way to test the places we click on within the app - especially when browsing side links and versions - to ensure we're updating the path_object / footer when navigating the app. Probably derive some test for that so that we don't have inaccurate or stale links and maybe even come up with a more foolproof system for it in general.
#29revisit phase prediction on the version view in alchemyneeds detailApp · web ui
·App · web ui·Alchemy·3 months ago
this is the version view - it's a known area - we have a system for tracking / predicting which phase we're currently on - we need to revis it as it's not terribly useful right now. as a side note we need an App * Version View in the taxonomy
#330Tracker: drag-and-drop column reordering with saved layoutreadyNew featureApp · web uiclaude
·App · web ui·3 months ago
Let the user reorder Tracker columns by dragging column headers, and persist the order per project+tab (localStorage, like the existing AlcTracker column-hide prefs). On render, re-apply the saved order by reordering both the header cells and every body cell keyed by data-col. Fixed columns (name, rollup Status, spacer, Priority) stay pinned; only the movable middle columns (task-status + info fields) reorder. Must survive HTMX swaps (re-apply order after each swap, same as AlcTracker.apply). Consider a small drag handle and drop indicator on the th.
Alc · plugins2
#239Headless UnrealEditor-Cmd run mode for the unreal Scene gatewayneeds detailNew featureAlc · pluginsclaude
·Alchemy·3 months ago
The unreal Scene gateway currently runs recipes against the CONNECTED editor over the bridge (run_preflights in the app venv). Add a true headless mode: launch UnrealEditor-Cmd on the world's .uproject (already Alchemy-configured by configure_unreal_python_env) with a python bootstrap that runs run_preflights(recipe, scene_file) and exits — the analog of blender's plugin.setup.run_recipe. Wire it so Scene._run_recipe(headless=True) spawns it when no editor is connected (or always, per a flag).
#238Refactor BuildModelTurntable (unreal mdl build step 2) from cgl.* to alc opsneeds detailChoreAlc · pluginsclaude
·Alchemy·3 months ago
unreal/build/mdl/mdl.json step 2 (BuildModelTurntable) is still old cglumberjack PreflightCheck/cgl.* code, so Run All imports jack (step 1, refactored) then fails on the turntable. Refactor it to a new alc.cookbook.preflight.PreflightCheck subclass using the unreal common/ldv turntable ops (dispatched via the executor to the connected UE), mirroring the ImportLatestModel refactor done in #234.
App · code_editor2
#34"Process Script" button should be able to process a .fdx file and make a .json file the Script Editor can read.needs detailNew featureApp · code_editorTomScript
·App · code_editor·Alchemy·3 months ago
#35in the Script Editor - when in transcript mode i should be able to see the characters listed on the sideneeds detailNew featureApp · code_editorTomScript
·App · code_editor·Alchemy·3 months ago
App · cookbook2
#422i'd like a direct link to the recipe shoown in phase details so i can edit it in the cookbook in a new tabneeds detailNew featureApp · cookbookTomDetails
·App · cookbook·Alchemy·3 months ago
#527Phase Details console stays silent when Run All is submitted to the Deadline farmreadyNew featureApp · cookbookclaude
·App · cookbook·Alchemy·3 months ago
When the Deadline toggle is on, Run All returns only a job_id (batched) or a RunResult (chained) and the new Phase Details console prints nothing, so a farm submission looks identical to nothing happening. The console should log the submitted job name/id, the mode (batched/chained), and any submission error, the same way local runs now log their step messages.
i want a button that will allow me to draw a box to collect a screen grab and attach it to the work item / kanban card. this will allow users to be very specific with images in their requests. We can put this button within the Reference Files Section.
#226alc_sprint: assemble worker branches into one committed sprint/integration branchreadyNew featureApp · kanbanclaude
·App · kanban·3 months ago
Make the multi-worker path of alc_sprint end in a single, testable integration branch instead of N loose worker branches. Standard flow: (1) fan out one worker per item/cluster in isolated worktrees (as today); (2) each worker COMMITS its work to its branch (workers currently leave changes uncommitted in the worktree - the orchestrator had to commit them by hand this session); (3) orchestrator creates sprint/<name>-<YYYYMMDD> off the CLEAN COMMITTED main ref (never the shared working tree, which may hold another session's uncommitted work) in its own dedicated worktree; (4) merge all worker branches into it (--no-ff), resolve conflicts, run tests + an in-app smoke on the MERGED tree (a clean auto-merge is not a correct merge); (5) commit the sprint branch and STOP - hand it to the user for testing. On approval: push + open PR. Anything found in testing is fixed on the sprint branch, not scattered. OPEN DESIGN QUESTION to settle: board state model - do items flip to done at per-agent verification (as now), or to an intermediate in-review/on-branch state that only becomes done on sprint-branch approval? The user leaned toward the latter (wait-for-testing, address-on-branch). Consider a sprint_cli helper (e.g. 'integrate') that automates worktree creation + ordered merges + reports conflicts, so the orchestrator does not hand-roll git each time. Ties to alc_do_ticket wrap-up conventions.
App · script2
#404Render Storyboards style key is a hardcoded machine-local pathreadyChoreApp · scriptclaudeScript
·App · script·Alchemy·3 months ago
DEFAULT_STORYBOARD_STYLE in app/tools/script/script_viewer.py:789 pins the scene-wide drawing hand to a literal absolute path (E:\Alchemy\_storyboard_test\tani_board.png) that only exists on Tom's box. Move the style key somewhere addressable — a published design/style asset resolved through PathObject, or a project Style setting — so the Render Storyboards pass draws with the same hand on any machine (and on the farm) instead of silently falling back to no style reference.
#31Convert the script reader timeline playbar to the shared AlcPlayback widgetneeds detailChoreApp · script
·3 months ago
cookbook2
#526[cookbook] [alchemy] [dsn] Implement the Copy Files to Render buttonreadyNew featurecookbookstub
·Cookbook · alchemy · build · dsn·Alchemy·3 months ago
Ensure that all image files, glb files, and any mov files are copied to the render directory of this version's pathObject so that they will show up as review media, as well as be ready for publish.
#271[cookbook] [alchemy] [dsn] Implement the Ensure Character Rig Type buttonreadyNew featurecookbookstub
·Cookbook · alchemy · publish · dsn·Alchemy·3 months ago
if we're dealing with a chr (pathObject parent) we need to know that a Character type is set - humanoid for example, quadraped, bird, etc...
Script Start Previs
60
Drag items or batches here, or hit to add one.
Script Start Unreal3
#61Clarify Script vs Transcript tool naming + add per-page visual confirmation to the taxonomy/tool pickerreadyNew featureScript Start UnrealclaudeScript
·App · shell·Alchemy·3 months ago
Two editor pages look nearly identical but are different tools with different core functions: the transcript tool renders a script-editor face (its page header reads 'Create Script') at /transcript/.../scr, while the separate 'Script' tool renders the script reader at /.../read. This ambiguity mis-routed sprint tickets (agents built left-panel + playhead features against the Script tool when the user meant the Transcript editor). Two parts: (1) Disambiguate the tool taxonomy/labels so it's obvious which tool/page you're on and what each owns (rename/relabel so 'Script' vs 'Transcript' is honest, or merge if they've converged). (2) In the taxonomy/tool-picker (and ideally the feature-ontology view), attach a per-page visual confirmation - a still captured via the existing screengrab service - so each registered page carries a thumbnail of what it actually renders. Early-stage, this keeps pages honest and surfaces ontology litter (duplicate/near-duplicate pages, stale routes).
#1009Unreal previs: create the LOCATION'S UE PROJECT if it doesn't exist yetreadyNew featureScript Start UnrealclaudePrevis
·Plugin · unreal·Alchemy·3 months ago
A location in Alchemy IS a whole Unreal project, not a level inside one. Locations live as {location}.uproject under the Perforce root - C:/alc/p4v/{company}/{project}/PERFORCE/{location} - and every sequence shot at that location is built in that project. Any sequence at Jack's homestead, now or in three months, is in the "homestead" project.
The previs build assumes that project already exists. For a location that has never been to Unreal, it does not, and there is nothing to build into.
BOTH LEVELS OF THIS ARE REAL - DO NOT COLLAPSE THEM
* the LEVEL for the sequence: already handled, by the existing EnsureSequenceLevels step (order 0 of cookbook/unreal/build/prv/prv.json). Untouched by this ticket.
* the PROJECT for the location: missing, and this ticket. It sits one level up, and BEFORE EnsureSequenceLevels - you cannot ensure a level inside a project that does not exist.
THE CREATION ALREADY EXISTS - REUSE IT, DO NOT RE-SCAFFOLD
The Locations page's "+ New Location" already does exactly this work: app/routes/unreal.py::unreal_create_location calls create_location(perforce_root, company_id, project_id, world), which writes a minimal content-only {world}.uproject plus Config/DefaultEngine.ini with the Alchemy plugins enabled (via configure_unreal_python_env, the same call open_project runs), and lets the engine hydrate Binaries/Intermediate/Saved on first launch. So this step is a caller, not new scaffolding. Take ue_version and connect_session deliberately rather than by default - a headless previs build has different needs from a person clicking New Location.
WHERE IT RUNS. Note it does not need Unreal: it writes two files on disk. That is what lets it be the FIRST step and still work headless - the thing it creates is the thing everything after it needs UE for. Order it before EnsureSequenceLevels and make sure the connection/open that follows targets the project it just made.
NAMING IS THE CONTRACT. Establish explicitly how an Alchemy location maps to the project name (homestead -> homestead.uproject) and use one rule, so a location created by this step and one created by hand from the Locations page are the same project rather than two that differ by a case or a slug.
IDEMPOTENT, like every other link in these chains: creating it twice is harmless, an existing project is never replaced or re-scaffolded, and its contents are never disturbed. The rest of the prep chain already reads this way (Process Designs sweeps and fires only what is missing).
SAY WHICH IT DID. Created-vs-found is exactly the kind of thing that is invisible until someone wonders why their project is empty - log it, and surface it in whatever the previs build reports back. It should also show up on the Locations page afterwards like any other location.
DONE WHEN: building previs in Unreal for a location with no UE project creates one under the Perforce root and proceeds; the new project appears on the Locations page and opens in Unreal; building again finds it and proceeds without touching it; EnsureSequenceLevels still owns the sequence's level; and the step reports which of created-or-found happened.
#1010Unreal previs: an Import Design GLB step, covering every asset on the boardreadyNew featureScript Start UnrealclaudePrevis
·Plugin · unreal·Unreal Engine·3 months ago
Add an ImportDesignGlb step to the Unreal previs build that brings in the published design GLB for EVERY asset on the board - characters, props, dressing and environments - not just the environment.
WHY IT IS WORTH ITS OWN STEP
Unreal does not care about geo count. The reason Blender builds a proxy at all - viewport weight, clean topology for lookdev - simply does not apply, so the heaviest thing we publish is also the cheapest thing to hand Unreal. And the design GLB exists EARLIEST: the dsn chain publishes it before any model or rig follows. That means a previs can stand up with something visible for every asset on a board whose chain is only part-way done, instead of waiting for the whole design -> model -> rig chain to finish for every last prop.
That is the real value: previs is for judging cameras and timing, and a stand-in at the right size in the right place does that job. An empty level does not.
DECIDE AND STATE: always, or fallback? Two readings, and they behave differently:
* ALWAYS - import the design GLB for every asset regardless of what else exists. Simple, and the previs always has everything.
* FALLBACK - import it only where no model/rig has been imported by ImportConnectedRigs / ImportEnvironmentModel, so a rigged character is not shadowed by a static copy of itself.
The second is almost certainly what is wanted for characters (a rig and a static duplicate in the same spot is a mess), and the first is fine for dressing and props. Pick per asset kind if that is the honest answer, and write down the rule.
ORDERING. It has to run after the project and level exist (#1009, EnsureSequenceLevels) and its relationship to ImportConnectedRigs / ImportEnvironmentModel depends on the decision above - fallback means it runs after them and fills gaps; always means order barely matters. Say which and why in the step's docstring, because the next person will ask.
PRECEDENT TO REUSE: cookbook/blender/build/mdl/ImportDesignGlb.py already exists as the Blender counterpart, and its docstring is explicit that the GLB lands in the DSN collection and is NOT promoted to the mdl hierarchy - it stays the hero. Take the naming and the meaning of "design glb" (the published dsn output) from there rather than inventing a second vocabulary for the same file.
IDEMPOTENT, like every link in these chains: importing twice must not duplicate actors, and a re-run must not disturb what is already placed.
PLACEMENT: assets need to land somewhere sane - at origin is not good enough for judging a camera. Say what placement this step gives them, even if the first pass is "wherever the design says" or "at the location's origin, to be moved by hand".
DONE WHEN: a previs build brings in a visible stand-in for every asset the board declares; the always-vs-fallback rule is implemented and written down; re-running does not duplicate anything; and a board whose props only have designs still produces a previs with those props in it.
blender
30
Drag items or batches here, or hit to add one.
provenance
60
Drag items or batches here, or hit to add one.
Asset Scale Integrity
40
Drag items or batches here, or hit to add one.
Previs · shot finishing
120
Drag items or batches here, or hit to add one.
Cookbook · repo rigor
30
Drag items or batches here, or hit to add one.
Cookbook · cleanup1
#173431 cookbook steps are dead on import: stale alc.plugins.preflight path and 4 files that do not compilereadyBug fixCookbook · cleanupclaude
·Cookbook · blender · build · srb·Blender·3 months ago
27 cookbook step files import 'from alc.plugins.preflight.preflight_check import PreflightCheck'. That module does not exist - alchemy has no alc/plugins/ package at all; the class lives at alc.cookbook.preflight.preflight_check, which is what every WORKING step imports (see blender/publish/rig/CleanScene.py). These steps therefore raise ModuleNotFoundError on line 1: the runner cannot load them at all, so any recipe containing one dies at that step in a live DCC. THE FIX for those 27 is mechanical - swap the import path. The affected files, from tests/known_issues.py::STALE_ALC_IMPORTS: blender/build/srb/{AddEnv,CreateWorldCollection,EnableStoredViews,ImportBoardsAsMulticam,RefreshImages,SetFps}.py; blender/build/svz/{AddCharacters,AddEnv,AddStoryboards,EnableStoredViews,SetFps,SetupSceneCollections}.py; blender/pre_render/wkb/{Render,SetPath,ShowInExplorer}.py; blender/publish/svz/{CreateLayoutMsd,ExportCamears,ExportGeometry,ExportStoryPoses,Publish}.py; blender/publish/wkb/{ExportCameras,ExportGeometry,ExportStoryPoses,Publish}.py; blender/tasks/{srb,svz,tex}/task.py. SEPARATELY, 4 files do not even parse as Python and need real repair or deletion, not an import swap (tests/known_issues.py::UNPARSEABLE_STEP_FILES): blender/refresh/srb/CheckForNewLayers.py and blender/refresh/srb/UpdateImagePlaneVersions.py both literally declare 'class Check For New Layers(PreflightCheck):' with spaces in the class name; maya/publish/shd/ExportShader.py uses a Python-2 print statement; unreal/build/prv/EnsureSequenceLevels.py is not valid UTF-8. FIRST DECIDE WHETHER THESE ARE DEAD CODE. srb (storyboard), svz (story viz) and wkb (workbook) may simply be abandoned - if a task is no longer run, DELETE the steps and their recipes rather than repairing imports nobody calls. Ask before deleting. DONE MEANS: the corresponding entries are removed from tests/known_issues.py and the cookbook suite still passes - the ledger asserts in both directions, so a fixed entry left behind fails the build just as a new breakage does. Note blender/publish/svz/ExportCamears.py is misspelled in the repo itself; renaming it is a separate decision because the recipe JSON references that spelling.
maya0
Drag items or batches here, or hit to add one.
Beat board · video column
30
Drag items or batches here, or hit to add one.
Seedance · video modes
91
#2534A 'has dialogue' flag that gates whether the lipsync prompt is insertedreadyNew featureSeedance · video modesclaude
·Service · ai seedance·2 months ago
An explicit per-beat "has dialogue" flag that controls whether the lipsync portion of the
prompt is INSERTED. It gates composition; it does not change a word of what gets composed.
Today the lipsync suffix is appended implicitly, based on whether a voice take auto-resolves
for the shot (resolve_shot_voice, via styled_video in alc/services/ai/shot_finish.py:242-254).
Nothing states the intent and nothing can override it.
What it should do:
- A per-beat flag on the visual/beat, surfaced on the video payload modal next to the other
per-submission controls, reading as "has dialogue" / lip-sync this shot.
- Flag ON -> the lipsync aspect of the prompt is inserted, EXACTLY as it composes today.
- Flag OFF -> it is not inserted, and no dialogue audio is sent.
- DEFAULT must reproduce today's behaviour: derive the initial value from whatever the
current auto-resolution decides, so an untouched beat submits precisely what it submits
now. This is an override, not a new decision.
NON-NEGOTIABLE: the composed prompt for a dialogue beat with the flag ON must be
BYTE-IDENTICAL to what the pre-change code produces. Lock it with a test. The dialogue path
is production-proven with weeks of tuning behind it and is not being redesigned here — see
the guard card #2507. This ticket adds a switch in front of it and nothing else.
Why it is needed and not speculative:
1. VO / narration. A beat carrying narration over a shot where nobody is speaking on camera
currently auto-resolves the voice and instructs the model to lip-sync it — so a character
mouths narration. The flag is the off-switch for that, and there is no other one.
2. The mode dial (#2452) already needs this distinction. "prompt" mode and "tween" mode
(#2493) cannot lip-sync — tween clips have no audio at all. Prompt assembly has to know
whether the lipsync aspect applies, and an explicit flag says it rather than inferring it
from a chain of resolution calls.
UE · rig turntable
40
Drag items or batches here, or hit to add one.
UE · recipe execution3
#3129[cookbook] A 'required: false' step still runs after an upstream failurereadyBug fixUE · recipe executionclaudeRig
·Cookbook · unreal · build · rig·Unreal Engine·3 months ago
Steps marked required:false are not gated by prior-step failures — they run anyway and can do real, polluting work in a broken build. Observed twice on 2026-08-04 with unreal/build/rig: CreateRigFromDesignGlb crashed, ImportGlbRig and AutoRig were correctly 'Skipping (prior failure)', but BuildRigTurntable (required:false) RAN, imported genMan plus six motion clips into the project as retarget source, and only then failed with "no asset at '/Game/assets/chr/zhou/rig/default/chr_zhou_rig'". The user had to hand-clear the polluted content folder before each retry. 'required: false' should mean the recipe can pass without this step, not that the step is immune to gating. Decide and implement the intended semantics: a non-required step should still be skipped once an upstream step in the same recipe has failed.
#3130[unreal] The UE python env drifts from the app venv, so alc imports crash inside UnrealreadyChoreUE · recipe executionclaude
·Plugin · unreal·Alchemy·3 months ago
Unreal's python environment (E:/Alchemy/cgl/config/plugins/unreal58/site-packages) is a hand-curated ~38-package subset, while the app venv carries ~400 more. Any alc code path that reaches a dependency outside those 38 dies inside UE with an ImportError that looks like a bug in whatever step happened to touch it. Hit twice on 2026-08-04 in a rig build: 'No module named xmltodict', then 'No module named pandas' once the first was installed — both pulled in via alc/models/script/ (the asset manifests live under the screenplay module). Those two specific imports were made lazy (see the fix/ue-rig-env-and-hdri PR), but the structural gap remains and will recur with a different package. Decide the intended contract and implement it: either the UE env is generated from the app's dependency set rather than curated by hand, or alc guarantees an import-clean core that never reaches heavy optional deps, with a test that asserts it.
#3131[unreal] App-side recipes boot a headless Unreal they never usereadyChoreUE · recipe executionclaude
·Alchemy·2 months ago
run_cookbook(software='unreal') trampolines the WHOLE recipe into a headless UnrealEditor-Cmd, including steps that touch no Unreal API. Two concrete cases from 2026-08-04: (1) unreal/publish/rig is pure file-copy plus MSD commit, yet each publish pays a ~20s engine boot and takes the project lock, so it cannot run while an artist has the project open; (2) CreateRigFromDesignGlb is an app-side Meshy HTTP round-trip whose own docstring says 'app-side (the Meshy round-trip needs requests + routing context UE doesn't have)' — running it inside UE is why a missing pandas in the curated UE env could kill it, and it holds the engine open for minutes waiting on a cloud call. Let a recipe or step declare that it runs app-side, so only the steps that genuinely need the editor pay for one. Tom's framing: do the Meshy glb before even opening UE — UE should open only when there is something to import.
App · image_editor3
#424this page should show ALL images in the directory so that we can confirm what they are tagged as.needs detailNew featureApp · image_editorTom
·App · image_editor·Alchemy·3 months ago
#3114Verify the expression paint-over round trip keeps the generated base layerreadyBug fixApp · image_editorclaude
·App · image_editor·3 months ago
#3059 publishes a committed expression frame with a one-layer manifest beside it (alc/services/ai/expression_harvest.py::_write_base_layer, via alc.services.media.layer_doc), marking the harvested frame as the `generated` base layer so a paint-over lands ON TOP of it and "what did the machine make" stays answerable forever.
The write side is unit-tested (tests/test_expression_slots.py::test_the_generated_frame_is_kept_as_its_own_base_layer). What is NOT verified end to end is the ROUND TRIP through the image editor:
1. click a filled expression slot on a character's dsn version view (it calls launchInternalEditor on the committed PNG)
2. confirm /load-layers reads the manifest and shows ONE layer named "<Label> (generated)" rather than treating the PNG as a flat image
3. paint on a new layer and save
4. confirm /save-layers keeps the generated base as its own bottom layer with generated=true and entity_id=<character> intact, and does not flatten it into the painted result
Step 4 is the one that matters: the whole point of not flattening on publish is that a later correction never starts from the corrected image. app/tools/image_editor/image_editor.py rebuilds layer entries from what /load-layers handed the client, so the flag SHOULD survive - but nobody has run it on a dsn file rather than a storyboard board.
#3205Delete Layer silently refuses to delete the bottom layerreadyBug fixApp · image_editorclaude
·App · image_editor·Alchemy·3 months ago
In the image editor's Layers panel, Delete Layer does NOTHING when the selected layer is the bottom of the stack — no deletion, no message, no disabled state on the button. The guard is app/tools/image_editor/static/js/editor_overlay.js:1329 — `if (layer.canvasId === 'editor-canvas') return;` — an unconditional early return with no user feedback.
The bottom layer is not conceptually undeletable; it is just the one that happens to be mapped onto the base editor-canvas by loadBoardLayers(). When there is more than one raster layer, deleting it should promote the next layer up into the base canvas (Photoshop lets you delete the Background when other layers exist). Only a single-layer stack has nothing to promote, and THAT case should disable/grey the button rather than no-op.
Whatever the chosen behaviour, the button must never look like it worked and do nothing.
Core3
#913PathObject.asset_details should resolve from its OWN project, not the ambient current onereadyBug fixCoreclaude
·Core·Alchemy·3 months ago
#833 fixed the SYMPTOM properly — a headless run now binds alc.context.scope from the asset path, so PROJECT_REGISTRY.current answers for the job. This is the root fix underneath it: a path-derived lookup should not need an ambient scope AT ALL.
PathObject.asset_details (alc/path/path_object.py:996) reads PROJECT_REGISTRY.current five times — character/prop/dressing/environment/sequence registries. But already carries company and project. Asking a global which project we are in, while holding a coordinate that says so, is the whole bug class: it can only ever be right by luck or by someone remembering to bind a scope first.
THE CHANGE IS SMALL. All five reads are inside that one property, and asset_details has NO other callers in the repo — the only references are the three inside PathObject.height. store_for(po) already exists in alc/services/project.py doing exactly this resolution, with a docstring saying falling back to the active project is the silent cross-project read it exists to prevent. So: asset_details resolves via store_for(self).
DECISION 1 — what happens when the coordinate has no project. store_for raises ValueError rather than guessing. asset_details currently returns None for an unrecognised parent, and height then explodes with AttributeError on NoneType (that IS the #833 crash). Recommend: raise an error that NAMES the asset and project when the asset is genuinely missing, rather than returning None for callers to trip over. A missing asset should be loud (see the no-silent-fallback convention), and in-repo nothing depends on the None.
DECISION 2 — a real inconsistency found while measuring, decide it here. PathObject.height (path_object.py:1016) reads:
chr -> self.asset_details.physical_traits.height_cm or 170
env -> self.asset_details.height or 600
else -> self.asset_details.height or None
The cookbook's CreateScaleCube does . For chr and env that else branch is UNREACHABLE — the / guarantees a truthy value, so an asset with no scale set silently gets a default cube instead of the warning the step was written to give. Either the defaults are right and the cookbook's warning is dead code, or the defaults are wrong and are hiding unset scales. Pick one and say so.
OUT OF SCOPE: the other 130 PROJECT_REGISTRY.current call sites. They are legitimately ambient (a request, a DCC session) and #833's scope already grounds them. This ticket is only about lookups that are derivable from a coordinate they already hold.
DONE WHEN: asset_details resolves from self.company/self.project via store_for; a farm job gets the right asset with NO scope bound at all; a missing asset or a projectless coordinate raises something that names what it was looking for; and decision 2 is implemented rather than left ambiguous.
#3201Status 'nte' (Notes) is not marked started=True, so revision work reads as never begunreadyBug fixCoreclaude
·Core·Alchemy·3 months ago
Decide whether Status(code='nte') should carry started=True. A task sitting at 'Notes' means a reviewer requested changes on submitted work — that work demonstrably started — but nte is currently absent from STARTED_STATUSES, so anything deriving 'has work begun' treats it as untouched.
#323PathObject.publish crashes in _update_downstream_statuses: 'self.shot' AttributeError (should be self.child)readyBug fixCoreclaude
·Core·3 months ago
po.publish() copies source+render to the major version + publish location and commits the task MSD successfully, then raises AttributeError: 'PathObject' object has no attribute 'shot' at path_object.py:1681 (elif self.shot == 'SEQ') inside _update_downstream_statuses. PathObject has no 'shot' field (it's parent/child) so publish crashes AFTER it has already landed on disk, leaving downstream statuses unset. Fix the attribute (self.child / correct shot token). Repro: po.publish() on an amc version where parent=INT, child=SEQ.
Service · media4
#420Media page re-thumbs every review on every renderreadyBug fixService · mediaclaude
·App · media·Alchemy·3 months ago
app/tools/media/media.py::_load_reviews globs every reviews/*.json and calls review.media.ensure_thumb() on EVERY page render. With playlists (#419) the whole Media tool re-renders on each playlist mutation (create / add / move / remove / select), so a project with hundreds of flagged items pays a full manifest scan plus a per-file thumb existence check — and possibly ffmpeg/AlcSequence thumb generation — on every click. Generate thumbs at flag time (flag_for_review) and lazily from the thumb route, or cache the scan per request, so reading the Media page is a read.
#1174shot_audio still resolves through svz - the same hardcoding #1024 just removed, one module overreadyBug fixService · mediaclaude
·Service · media·3 months ago
alc/services/media/shot_audio.py resolves a previs shot's dialogue audio by assuming the svz task owns the timeline - exactly the assumption #1024 removed from ImportStoryvisTimeline, left in place here.
TWO HARDCODINGS, BOTH IN shot_audio.py:
* shot_block_type() globs 'scene_{seq}_timeline.json' under an svz_root derived by string-swapping /VERSIONS/render/ -> /VERSIONS/source/ - a scene-shaped filename on a constant task, which is the exact pair of assumptions #1024 named.
* its fallback calls PROJECT_REGISTRY.current.script.get_scene(seq), which raises for a video: a board's parent is not in the project script's scene list. This is the same failure that produced "svz.refresh: scene 'theaxe' not found".
WHY IT HAS NOT BITTEN YET: it is only reached when encoding per-shot mp4 previews with dialogue muxed in (alc/plugins/blender/create.py ~2045). Board previs shots do not encode previews yet. The moment they do, a board's shots either get no dialogue or throw, depending which of the two paths is hit first.
THE FIX ALREADY EXISTS: alc/services/timeline.py resolves the timeline manifest for ANY coordinate via TIMELINE_OWNERS (shots->svz, videos->scr). shot_audio should ask it rather than rebuilding the path, and its block-type fallback should read the owning task's model rather than the project script's scene list.
DONE WHEN: a board's shot resolves its dialogue audio through the same code path a scene's does; neither raises for a coordinate whose timeline is not owned by svz; and the dialogue/action distinction (action blocks stay silent) is unchanged.
#1788scan_media_rows reads a BOARD's camera dirs with the scene naming rule, so board shots key off the wrong tokenreadyBug fixService · mediaclaudePrevis
·Service · media·Alchemy·3 months ago
scan_media_rows() parses a camera dir as {seq}_{block}_{take}_MagiCam and takes block=parts[1], take=parts[2]. That is right for a SCENE (010_0010_001_MagiCam) and wrong for a beat-board VIDEO, whose cams are named with the block LAST: E:/Alchemy/cgl/loj/VERSIONS/render/videos/theaxe/SEQ/prv/default/tom/000.014/high/010_010_0010_MagiCam - there parts[1]='010' (the scene) and parts[2]='0010' (the block). Every board therefore collapses into a single take_id key '010_0010' style pairing that does not match the script block ids the document emits (0010, 0020 ...). Anything joining a board's shots through scan_media_rows - the scene timeline media-rows endpoint, ensure_styled_offsets, and the cookbook shelf alchemy/shelves/prv/RenderStyleVideo.py which states the same {seq}_{block}_{take} assumption in its docstring - is keying board renders off the wrong token. WANTED: one place that answers 'which camera dir belongs to block X' for BOTH shapes. #1748 already added prv_media.previs_cam_dirs_by_block(render_dir, block_ids), which sidesteps the parse entirely by matching endswith('_{block}_MagiCam') against ids the caller already knows; the fix is probably to give scan_media_rows the same treatment (or to have it take the owning document's block ids) rather than adding a second parse rule.
#332Scene/shot thumbnails generated on publish (uniform with asset thumbs)readyNew featureService · mediaclaude
·Service · media·3 months ago
PathObject.get_thumb_path(thumb_type='asset') is entity-type-agnostic: for a scene it resolves to PUBLISHES/render/shots/{scene}/SEQ/.thumb/thumb.png — the SAME .thumb/thumb.png convention as any asset, served via /alcroot/{path-relative-to-PROD_ROOT} (StaticFiles mount, app/main.py:214). Confirmed the path resolves for scene INT; nothing writes the file yet. Work: (1) During the animatic (amc) publish — and optionally previs — generate the scene's asset-level thumb .thumb/thumb.png from the animatic's first frame / preview, mirroring how the design (dsn) publish writes an asset's thumb (see AlchemyAsset.asset_thumb + PathObject.asset_thumbnail). Use the OIIO media service for the frame grab. (2) Add a SceneAsset.asset_thumb property (build a PathObject with entity_type='shots', parent=scene_id, child='SEQ', then get_thumb_path(thumb_type='asset')) returning {exists, path} root-relative, so templates consume it uniformly. Then app/templates/partials/asset_grid.html:133 (currently forces asset_thumb=None for Scenes) and the Tracker thumbnail column (ticket #329) can both display it with no special-casing. Unblocks #329.
Plugin · blender2
#1172A board's previs opens with the cast standing in shot 1's camerareadyBug fixPlugin · blenderclaude
·Plugin · blender·Blender·3 months ago
ImportCharacterRigs lays the registered cast out on a grid starting at world origin (0,0,0). The MagiCams the timeline step creates sit at (-0.003, 0.0, 1.692). So the cast lands 0.31-1.56m in front of camera 010_010_0010, occluding that shot's image plane at 5.73m: a board previs opens as a dark mass with the beat art hidden behind jack's rig control shapes.
NEITHER STEP IS WRONG ALONE - it is the seam between the grid origin (#1026) and the camera origin (#1024), which is why it passes every step's pass_criteria and was only caught by rendering frame 1 through the camera rather than inspecting the scene graph.
FIX BELONGS IN THE PLACEMENT, NOT THE CAMERA: the cameras are where the board says they are. Offset the grid origin off the camera axis, or better, place the cast relative to the imported location's bounds rather than world zero - a cast standing inside the environment it is registered in is the honest default and stops being a coincidence of both origins being near (0,0,0).
DECIDE AND STATE: whether placement is relative to the location bounds or a fixed offset, and what happens when no environment imported (no bounds to place against).
#1871A previs shot whose overscan pre-pass fails renders clean and silently has no shot manifestreadyBug fixPlugin · blenderclaudePrevis
·Plugin · blender·Blender·3 months ago
In create.py the overscan pre-pass is wrapped in a bare try/except that prints and continues: 'except Exception as exc: print(f"[overscan] pre-pass failed ({exc}); rendering hero framing")'. os_manifest stays None, the render proceeds at hero framing, and 90 lines later the manifest write is guarded by 'if os_manifest is not None' - so the shot renders perfectly, produces clay frames and mattes, and has NO shot_manifest.json. Nothing downstream can tell that shot apart from a good one by looking at it, and the style pass (which needs the manifest for its anchor frame and presence list) fails only when somebody spends a pass on it. The manifest-write except clause has the same shape. WANTED: a shot that renders without its manifest is a MARKED failure, not a print - the reason persisted beside the render (a .failed/.log sidecar, or the manifest written with an error field) so the state is inspectable after the fact, and the render reported as incomplete rather than successful. Re-rendering is not a workaround: the pre-pass throws again for the same shot-specific reason.
Premiere Pro Headless5
#3323Record the Alchemy and AME web-service ports in premiere/setup, with a preflight for the 8080 collisionreadyNew featurePremiere Pro Headlessclaude
·Plugin · premiere·Adobe Premiere Pro·3 months ago
premiere/setup should own the port facts for headless rendering: the Alchemy app port (8080) and the AME web-service port. A preflight check confirms the AME web service is NOT configured on the Alchemy port and reports where it actually is, so the collision is caught by a check instead of by a failed render.
#3324Headless Premiere render: AME web-service client that renders a .prproj with no GUIreadyNew featurePremiere Pro Headlessclaude
·Plugin · premiere·Adobe Premiere Pro·3 months ago
A premiere render module that submits a .prproj to the Adobe Media Encoder web service and polls to completion, with no Premiere and no UXP panel involved. Owns the service lifecycle the way the ComfyUI bridge does with ensure_running(): start ame_webservice_console.exe if it is not up, wait for readiness, submit, poll, return the real output path.
#3325Establish which sequence AME renders from a multi-sequence .prproj, and make publish emit a single-sequence projectafter #3324blocked by #3324ChorePremiere Pro Headlessclaude
·Plugin · premiere·Adobe Premiere Pro·3 months ago
Determine what AME renders when a .prproj contains more than one sequence, then guarantee the pipeline never depends on the answer - most likely by having the publish step emit a project holding exactly the sequence we want rendered.
#3326Deadline MediaEncoder plugin has no entry for AME 2025/2026, so farm Premiere renders cannot resolve the executablereadyBug fixPremiere Pro Headlessclaude
·Plugin · deadline·Deadline·3 months ago
Farm-side headless Premiere rendering should resolve the AME web-service executable for the versions actually installed here (2025 and 2026).
#4527storyboard pro test 2, screen grab test.needs detailNew featurePremiere Pro HeadlessuserStoryboard
·Storyboard Pro·a day ago
Analytics & Effort Attribution
201
#3687Kanban modal autosave: a stale form silently clobbers concurrent edits, with no history stampreadyBug fixAnalytics & Effort AttributionFeature Attribution Workflowclaude
·App · kanban·3 months ago
POST /kanban/item/{id}/autosave writes the WHOLE form as it was rendered. If anything else edits
the item between the modal's render and its autosave — the ticket CLI, a skill flow, another
session — the autosave silently writes the stale values back over it. Last-write-wins, per-field,
no conflict detection, and (worse) autosave leaves NO stamp in status_history, so the overwrite is
invisible: updated_at doesn't even move past the legitimate edit it destroyed.
PROVEN 2026-08-08 on ticket #3670: `ticket_cli edit 3670 --capability "Feature Attribution
Workflow"` round-tripped and Tom SAW the value in his open modal; minutes later the field read ''
on the board, no history entry, no registry mint at done-time. Server log shows the anonymous
writer: GET modal -> POST autosave 204. The wrap-up capability flow (#3501/#3670) lost its input
before it ran. Recovered by re-editing + re-running done — but only because a human was watching.
This is the second stale-writer clobber this sprint (FeatureRegistry.commit() used to replace the
on-disk document wholesale — fixed by merging onto disk). Same class, other store: the board.
FIX DIRECTION (per-field merge, not locking the human out):
1. The modal form carries each field's RENDERED (original) value; autosave submits current +
original; the server writes ONLY fields where current != original. An untouched field can then
never clobber anyone. This also makes autosave idempotent for free.
2. Failing that, client-side dirty tracking (write only fields the user actually changed).
3. Autosave writes that DO change something leave a trace — a lightweight history/ledger stamp or
at minimum a log line naming the fields, so a silent writer can never again be a mystery.
4. Keep the WorkBoard.editing() lock as-is — it correctly prevents torn writes; this bug is about
STALENESS, which a lock cannot see.
ACCEPTANCE: reproduce the #3670 sequence (open modal -> CLI edit a field the modal user never
touched -> autosave fires) — the CLI edit survives; a field the user DID edit still saves; an
autosave that changes nothing writes nothing; any field-changing autosave is visible in history
or logs.
Features · ticket grid
11
#4526Test of Storyboard Pro Alchemy Ticket Integration - not a real ticket.readyNew featureFeatures · ticket griduserStoryboard
·Storyboard Pro·a day ago
Creative Timeline
70
Drag items or batches here, or hit to add one.
Max · initial plugin build
220
Drag items or batches here, or hit to add one.
Max Sprint Retrospective
38
#3869Audit the other DCC plugins against the ontology the Max build was written toneeds detailDesignMax Sprint Retrospectiveclaude
·3 months ago
Audit every existing DCC plugin against the ontology as it stands after the fresh 3ds Max build, and produce a decision document + follow-up sprint plan rather than code. The Max build is the first plugin written entirely against the current contracts, so it is the clean reference: compare blender, maya, unreal and substance to it and report (1) which still carry the pre-contract root create/edit/query/delete vocabulary and how much, (2) task coverage per DCC against the smart_task contracts, (3) which contracts have no implementation anywhere, (4) where Max had to diverge from the reference implementations and whether the divergence should propagate back. Output is a written analysis with a recommended sequence of follow-on sprints, sized. Deliberately NOT an implementation ticket.
#3870Migrate the Blender plugin off its root create/edit/query vocabulary onto the contractsafter #3869blocked by #3869DesignMax Sprint Retrospectiveclaude
·Plugin · blender·Blender·3 months ago
Port the Blender plugin off its pre-contract root vocabulary and onto the smart_task contracts, so blender dispatches the way unreal and max do. Scope the work first: alc/plugins/blender/create.py (2381 lines), edit.py (1934) and query.py (360) are still imported by roughly 52 cookbook steps across 77 call sites. Produce a per-phase, per-task breakdown of those call sites, then migrate them to get_task() / the tasks/{task}/{verb}.py modules, retiring the root modules when the last caller is gone.
#3871default_executor() silently returns a BridgeExecutor for unregistered softwareneeds detailBug fixMax Sprint Retrospectiveclaude
·Plugin · smart_task·Alchemy·3 months ago
default_executor() should fail loudly for a software id that is not registered, instead of silently handing back a BridgeExecutor. Today alc/plugins/smart_task/executor.py ends with an unconditional return BridgeExecutor(software=software), so any string at all - including a typo - yields a plausible-looking executor that only fails much later, at the socket, with a connection error pointing at the wrong problem. Raise (or at minimum warn) when software is absent from _DCC_PROBE. Then re-examine the acceptance criteria of the DCC-registration tickets that assert on this function.
#3872Five dev/ modules abort pytest collection, so a bare pytest run executes nothingneeds detailBug fixMax Sprint Retrospectiveclaude
·Alchemy·3 months ago
Make a bare pytest run work again. Five modules under dev/ fail at import time, and because collection errors abort the whole run, plain pytest currently executes nothing at all - it exits on 5 collection errors before a single test runs. Either fix the imports, mark the modules as non-tests, or move them out of pytest discovery. Whichever way, the goal is that pytest with no arguments collects and runs the suite.
#3873Document the worktree verification rule so parallel agents cannot silently test the wrong treeneeds detailChoreMax Sprint Retrospectiveclaude
·Alchemy·3 months ago
Write down the rule that makes verification inside a git worktree trustworthy, so parallel sprint agents stop silently testing the wrong tree. The package is installed editable into the shared venv by a finder that hard-points at the MAIN checkout, so a worktree agent running that venv can import alc from main and see none of its own edits. The finder is APPENDED to sys.meta_path, which means the stdlib PathFinder still wins when the worktree root is on sys.path - so the working rule is: run the shared venv interpreter with cwd set to the worktree root, use the -c or -m invocation forms (NOT python path/to/script.py, which puts the scripts directory on sys.path instead of cwd), and prove it with a print of alc.__file__ before trusting any result. Put this in the sprint skill and/or the plugins CLAUDE.md, wherever agents will actually read it.
#3874Remove hardcoded machine-specific path fallbacks from the in-DCC bridge pluginsneeds detailChoreMax Sprint Retrospectiveclaude
·Alchemy·3 months ago
Drop the hardcoded machine-specific sys.path fallbacks from the in-DCC bridge plugins once each DCC startup script sets the path itself. The Substance plugin (alc/services/ai/substance_bridge/painter_plugin.py) carries literal C:/Users/tmiko/... and E:/Alchemy/... roots, and the new Max plugin (alc/services/ai/max_bridge/max_plugin.py) inherited the same pair for consistency. Max already puts a __file__-derived repo root ahead of them plus an ALC_MAX_BRIDGE_PATHS env override; decide whether that pattern should be the standard, then remove the hardcoded pair from both. Check the other bridge plugins for the same thing while in there.
#3886ExportInfo.units is the class default on every MSD, and it is wrong for glbneeds detailBug fixMax Sprint Retrospectiveclaude
·Ontology · models·Alchemy·3 months ago
Make ExportInfo.units truthful per artifact instead of leaving the class default in place. ExportInfo in alc/models/version.py declares units with a default of cm, and nothing overrides it at publish time - so every MSD on disk claims cm regardless of what the file actually is. For a .glb that is simply wrong: glTF is metres by specification. A consumer that trusted the passport and converted from cm would shrink every model by 100x. Set units (and confirm up_axis and scale) from the artifact actually being written, at the point of publish, for every DCC leg that writes an MSD.
#3910convert() passport lookup can break a re-run when a scene .msd sits beside the sourceneeds detailBug fixMax Sprint Retrospectiveclaude
·Service · mesh·Alchemy·3 months ago
convert() reads an ExportInfo passport from a sibling .msd next to the source file and uses its up_axis to decide whether a corrective rotation is needed. That is right for a design glb, whose passport describes the file. It is WRONG when the sibling .msd describes the SCENE rather than the file being converted - which is exactly what a publish produces. Make the passport lookup only apply where the .msd genuinely describes the source file, or require callers to opt in, so a conversion that worked on the first run cannot break on the second because a sibling manifest appeared next to it.
MCP Compliance
143
#3906Premiere MCP server over Streamable HTTP on the existing bridge appreadyNew featureMCP ComplianceMCP Serverclaude
·Plugin · premiere·Adobe Premiere Pro·3 months ago
Stand up alchemy-premiere as an MCP server instance. Premiere does NOT follow the Blender/Maya/Max shape, but the differences sit BELOW the MCP boundary and none of them are compliance obstacles: MCP defines client-to-server, and what the server does to reach its backend -- dial out, wait to be dialed, send Python, send JavaScript -- is invisible to the client. Serve MCP over Streamable HTTP mounted on the FastAPI app the bridge ALREADY runs on 20261, rather than stdio like the Python DCCs. Streamable HTTP is a first-class transport in the spec, not a downgrade, and this topology is cleaner: one process, one hop, and the server directly owns the /ws/panel WebSocket it needs. A stdio subprocess would instead have to open a second connection back to this app. Adding the MCP endpoint must not disturb the existing /exec, /menus, /menu/run, /todos and /request surfaces. Panel-not-connected is already first-class in the bridge (the panel WebSocket is None until it dials in, and there is a status endpoint) -- map that state to an isError tool result per #3895, with text telling the artist to open the Alchemy panel in Premiere. Never hang waiting for a dial-in.
#3907After Effects MCP server over Streamable HTTP on the existing bridge appafter #3906blocked by #3906New featureMCP ComplianceMCP Serverclaude
·Adobe After Effects·3 months ago
Stand up alchemy-after-effects as an MCP server instance, completing the DCC set. AE's bridge is the same shape as Premiere's -- FastAPI + uvicorn on 20262, a CEP panel that dials OUT over /ws/panel, a status endpoint, and a panel reference that is None until the panel connects -- so serve MCP over Streamable HTTP mounted on that existing app, exactly as #3906 does. Reuse whatever adapter #3906 produces rather than writing a second one; if AE turns out to need its own, say so on the ticket, because that means the Premiere solution was too narrow. The one real difference is the exec payload: AE has no UXP, so the panel runs ExtendScript (JSX) through CSInterface.evalScript against the AE DOM (app.project, comps, layers, the render queue), where Premiere runs JavaScript. That difference sits below the MCP boundary and should not reach the tool surface. AE implements one task (vid) and generates zero tools until #3905 lands. Same rule as the other narrow DCCs: advertise only the ops AE actually implements.
#3908MCP conformance sweep across every DCC serverafter #3906after #3907blocked by #3906, #3907New featureMCP ComplianceMCP Serverclaude
·Plugin · smart_task·Alchemy·3 months ago
The sprint-closing card: one parameterized conformance suite run against EVERY Alchemy MCP server (blender, maya, unreal, substance, max, premiere, after-effects) rather than seven per-DCC test files. Assert, for each: the initialize handshake returns a protocolVersion and the capabilities the server actually implements; tools/list is well-formed MCP (inputSchema, not input_schema) and every advertised tool is dispatchable; the advertised list matches that DCC's REAL task coverage, so no server offers an op its DCC cannot serve; a failing op returns isError with the message as content rather than a JSON-RPC error; a disconnected DCC returns an actionable message rather than hanging; and concurrent calls to one bridge serialize. Where a DCC is not running, the suite must SKIP with a clear reason, never silently pass -- a green run against seven closed applications would be the worst possible outcome here.
--- AMENDED 2026-08-11 ---
The suite must run across BOTH transports, not just stdio: five stdio servers (Blender, Maya, Unreal, Substance, Max) and two Streamable HTTP servers (Premiere on 20261, After Effects on 20262). The same conformance assertions apply to both -- handshake, well-formed tools/list, dispatchability, coverage matching real implementation, isError semantics, backend-unavailable behaviour, serialization. Transport must be a parameter of the suite, not a fork of it; two parallel test bodies would defeat the point of the card. Add one transport-specific assertion each: a stdio server must not write anything to stdout that is not a JSON-RPC message (log output on stdout corrupts the stream -- logging goes to stderr), and an HTTP server must leave the bridge's existing routes working while MCP is mounted.
max1
#3911[cookbook] [max] [alchemy] Implement the alchemy shelf/menureadyNew featuremaxstub
·Cookbook · max · menus · alchemy·3ds Max·3 months ago
premiere1
#3945[cookbook] [premiere] [edt] Implement the edt shelf/menureadyNew featurepremierestub
Source hardcoded operational roots from config: LIBRARY_ROOT X:/ALC_MOTION in alc/models/motion.py, Deadline spool dir E:/Alchemy/temp/spool in alc/services/deadline/create.py + previs_plan.py, sys.path roots in alc/services/ai/max_bridge/max_plugin.py, and the absolute film_strip_border.svg path in alc/plugins/alchemy/tools/shot_navigator/ui.py. Current values become config defaults so Windows behavior is unchanged. Success test: grep for drive-letter literals under alc/ hits only docstrings/examples/dev-main blocks; app boots with all roots resolved via config; setting a POSIX root (e.g. /data/Alchemy) produces valid PathObject paths.
#3949Platform-guard Windows-only imports for Linux bootneeds detailChoreWeb App DockerfileDocker Deployclaude
·Core·2 months ago
Make the server package importable on Linux: guard or lazy-load the win32/pywin32 imports in alc/config/env.py, alc/services/media/window_capture.py, and alc/plugins/after_effects/install.py so they load only at call time on Windows. Success test: on a Python without pywin32, 'from app.main import app' succeeds; calling window capture off-Windows raises a clear WindowsOnly error at call time, not import time.
#3950Dockerfile: build and boot the web app on Linuxafter #3949blocked by #3949New featureWeb App DockerfileDocker Deployclaude
·App · shell·2 months ago
Add a Dockerfile at repo root that builds the FastAPI web app on python:3.11-slim: installs ffmpeg + OpenImageIO tools (oiiotool), pip-installs requirements into the image, and CMDs uvicorn on :8080 (reload=False, matching the Windows-reload convention). Success test: 'docker build' completes from a clean checkout; 'docker run -p 8080:8080' starts with no import errors and GET / returns 200.
#3951docker-compose: volume + config wiring for a deployable instanceafter #3950blocked by #3950New featureWeb App DockerfileDocker Deployclaude
·App · shell·2 months ago
Add docker-compose.yml wiring the web app image to a host-mounted production root: Alchemy root + alchemy.json config provided via bind mounts/env vars, port 8080 published, restart policy set. Every required env var documented inline in the compose file. Success test: on a machine with only Docker installed, 'docker compose up' with a seeded sample project directory serves the app on :8080 and project pages render media from the mounted root.
Add a smoke-test script runnable against the composed container: key routes return 200 (shell, projects list, a project shots page), an exr-to-png conversion via the OIIO media service succeeds inside the container, and preview/thumb/proxy paths resolve under the POSIX root. Success test: script exits 0 against 'docker compose up' with the seeded sample project. This script IS the sprint acceptance gate: when it passes, the sprint is done.
#3953Docker deploy runbook in the handbookafter #3952blocked by #3952ChoreWeb App DockerfileDocker Deployclaude
·Docs·2 months ago
MkDocs handbook page documenting the Docker deploy path: build the image, run locally via compose, deploy to a single EC2/VM (ports, volumes, config), and how to run the smoke-test acceptance gate. Success test: a reader with Docker and the repo can go from clean checkout to a serving instance using only this page.
Windows Installer6
#3955Add --test self-check mode to the server entry pointneeds detailNew featureWindows InstallerWorkstation Installerclaude
·App · shell·2 months ago
Add a --test flag to the Alchemy entry point (venv and future frozen exe alike): boot the FastAPI app headless, request /, exit 0 on 200 or nonzero on any import/boot failure, within 25s. This is the same harness contract the old build scripts rely on (launch_alchemy(exe, --test) in cglumberjack build_win32.py/build_darwin.py). Success test: 'python -m app --test' style invocation exits 0 from a clean venv; breaking an import makes it exit nonzero with the traceback on stderr.
#3956Port PyInstaller spec + build_win32 to the alchemy repoafter #3955blocked by #3955New featureWindows InstallerWorkstation Installerclaude
·2 months ago
Adapt cglumberjack cgl/apps/alchemy/alchemy.spec + build_win32.py to this repo: Python 3.11, FastAPI server entry (no PySide6), collect app/static + app/templates + OTIO adapters as datas, keep the install_openimageio step (oiio_bin.zip from alchemy-dependencies-bin S3 into _internal/openimageio). Local one-command build - the old GH workflow yaml is the dependency spec only (Actions are dead org-wide). Success test: pyinstaller build from clean checkout produces dist exe; exe --test exits 0; running exe serves :8080 and GET / returns 200 with static assets loading.
#3957Port installer + incremental updater infra from cglumberjackafter #3956blocked by #3956New featureWindows InstallerWorkstation Installerclaude
·2 months ago
Port cgl/apps/alchemy_installer (downloader, multi_file_download_manager, manifest_utils, updater, constants) into this repo: semver master-manifest with ordered_versions, per-version file manifests, cumulative-diff incremental updates from S3. Verify the old buckets (alchemy-windows, alchemy-windows-installer, alchemy-dependencies-bin) and AWS creds are still live, or re-point at current ones. Success test: on a machine without Alchemy, the installer exe downloads the latest manifested build and installs it; publishing a new version and running the updater applies only the diffed files.
#3958Versioned S3 release publish from the Windows buildafter #3956after #3957blocked by #3956, #3957New featureWindows InstallerWorkstation Installerclaude
·2 months ago
Port generate_manifest, upload_alchemy_to_s3, update_master_manifest, get_next_alchemy_version so build_win32 ends with a versioned release: build, --test smoke pass, semver bump, upload, master manifest updated. One command from clean checkout to published release. Success test: running the build command publishes a new semver version to the bucket, ordered_versions in the master manifest includes it, and the installer/updater on another machine picks it up.
#3959End-to-end Windows install test (sprint acceptance gate)after #3958blocked by #3958ChoreWindows InstallerWorkstation Installerclaude
·2 months ago
On a clean Windows machine or VM with no Python installed: run the installer exe, app installs, launches, serves the localhost UI, and connects to a remote Alchemy server over the tunnel. Write the checklist down as the repeatable release QA doc. Success test: fresh machine to working client without ever touching Python or a terminal; checklist passes end to end. This is the sprint acceptance gate.
#3960Code-sign the Windows exe and installerneeds detailChoreWindows InstallerWorkstation Installerclaude
·2 months ago
Sign the built exe and installer with a code-signing cert so SmartScreen does not flag downloads for white-label clients. Success test: freshly downloaded installer on a stock Windows machine runs without the unrecognized-app SmartScreen interstitial (or shows the signed publisher name).
Stand up and document a local Mac build environment (GH Actions are dead org-wide; the paused alchemy-darwin-build.yaml is the dependency spec only): Python 3.11, PyInstaller, ffmpeg, fileicon (brew), AWS creds. Success test: a clean Mac following the runbook page reaches a successful PyInstaller build of the alchemy spec.
#3962Port build_darwin.py: .app build on macOSafter #3961after #3956blocked by #3961, #3956New featureOSX InstallerWorkstation Installerclaude
·2 months ago
Adapt cglumberjack cgl/apps/alchemy/build_darwin.py to this repo on top of the shared PyInstaller spec: .app bundle, alchemy.icns, fileicon application, --test smoke launch. Success test: .app builds on the Mac build machine; open of the .app serves :8080 and GET / returns 200; --test invocation exits 0.
Port cgl/apps/alchemy_installer/build_darwin.py: build the Mac installer artifact against the shared manifest/updater infra, upload to the installer bucket. Success test: on a second Mac with no Alchemy, the installer downloads the latest manifested .app build and installs it; the updater applies an incremental version diff.
#3964Codesign + notarize the .app and installer (Developer ID)after #3962blocked by #3962ChoreOSX InstallerWorkstation Installerclaude
·2 months ago
Sign the .app and installer with a Developer ID certificate, notarize via notarytool, staple the ticket. Without this Gatekeeper blocks the app outright on client machines - this is a hard requirement for distribution, unlike Windows SmartScreen. Success test: on a stock Mac with default Gatekeeper settings, the installed app opens normally with no right-click-open workaround.
#3965End-to-end OSX install test (sprint acceptance gate)after #3963after #3964blocked by #3963, #3964ChoreOSX InstallerWorkstation Installerclaude
·2 months ago
On a clean Mac with no dev tools: run the installer, app installs, opens without Gatekeeper friction, serves the localhost UI, and connects to a remote Alchemy server over the tunnel. Write the checklist as the repeatable release QA doc. Success test: fresh Mac to working client without touching a terminal; checklist passes end to end. This is the sprint acceptance gate.
Unreal Alchemy Plugin
100
Drag items or batches here, or hit to add one.
MagiCam Control Rig
81
#4301Hand-built control environment from one concept (no AI)readyNew featureB-3Concept-to-Layout A: Prove itMagiCam Control RigclaudeConcept
·Alchemy·4 days ago
Build one small environment by hand from the concept: ground truth for layout accuracy and a timed baseline (the 'two days by hand' demo opening). Record what technical chores take the time.
MagiCam Native UE Plugin
229
#3982MagiCam UE plugin: standalone plugin architecture + skeleton, shaped for a Fab store releasereadyDesignMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Design the MagiCam UE plugin as a first-class NATIVE plugin with a store release as the bar: decide the boundary vs AlchemyUE (what ships on Fab with ZERO Alchemy/Python dependency: the rig, the native Camera Caddy panel, TVPaint import, lighting tools - vs what stays pipeline-side: Alchemy Timeline import, farm/publish hooks), the module layout (.uplugin + editor module reusing the native UI patterns from the AlchemyUE C++ module just merged to main - splitter/named-slot Slate/UMG work from the Unreal Alchemy Plugin sprint), the engine-version matrix (5.6/5.7/5.8 recompiles, keep the C++ surface dumb so the matrix stays cheap), asset strategy (Control Rig asset + BP generated from magicam_rig_spec.json vs baked content), and licensing (the Blender extension is GPL-3.0 - the UE plugin is a separate codebase and must NOT inherit that; pick a store-compatible license). Deliverable: a design doc next to MAGICAM_PORT.md + a compiling skeleton plugin.
--- AMENDED 2026-08-13 ---
BOUNDARY SHARPENED (Tom, 2026-08-13). The original spec framed this as a binary - what ships with ZERO dependency vs what 'stays pipeline-side: Alchemy Timeline import'. The real line is not which FEATURES ship, it is who ORCHESTRATES, and Tom drew it in two statements:
'Alchemy Timeline is a shape - a .json file - that translates to a model that's readable as an import artifact - we don't want to be so closed off that we can[not] run functions from it. With blender for example we do both.'
'But we have core python functions that can run things, so a timeline build would be outside of the core functionality of this plugin.'
THE PLUGIN IS A CAPABILITY SURFACE, NOT AN ORCHESTRATOR.
CORE (ships on Fab, self-contained, zero Alchemy dependency): the MagiCam rig, the native Camera Caddy, TVPaint clip_export.json import, the lighting tools. These are things a Fab customer with no Alchemy install uses standalone. TVPaint import is core precisely because it is a MagiCam-native feature, not an Alchemy one.
NOT CORE: building a timeline. Assembling a previs from an Alchemy Timeline is Alchemy's core Python functions orchestrating - read the timeline model, then call plugin capability per shot. That work lives pipeline-side (cookbook + alc/plugins/unreal) and is exactly what #1008 is. The plugin does not own it and should not grow a timeline builder.
THE REQUIREMENT THIS PUTS ON THE ARCHITECTURE - and it is the important one:
The plugin must be OPEN. Every core capability has to be reachable from outside the plugin, so Alchemy's Python can drive it: rig creation, camera-per-shot, plate assignment, plane depth, lens, keying. In practice that means the C++ surface is exposed (UFUNCTION / BlueprintCallable, editor-scriptable and reachable from the Python plugin when it is present) rather than sealed inside Slate button handlers. A capability that only a panel click can reach is a capability Alchemy cannot orchestrate.
So the design rule for every core feature: the PANEL and ALCHEMY are two callers of the SAME underlying op. This mirrors what the pipeline side already does - cam.edit verbs are the single source of rig behaviour, and the PySide caddy is just one caller. Keep that shape when it goes native.
The Alchemy Timeline JSON matters as the INTERCHANGE SHAPE, not as something the plugin must parse. Deciding whether the plugin also reads it natively is optional and can be deferred - note the cost if it ever does: a store plugin in the wild would then be reading files produced by Alchemy versions it has never heard of, which turns an internal format into a published, versioned contract. Not owning the timeline build avoids paying that.
Related: #4011 is doing the equivalent for AlchemyUE (native transport + tab registration, running without the Python plugin). Do not make PythonScriptPlugin a hard dependency of the MagiCam plugin the way AlchemyUE.uplugin does - feature-detect it.
--- AMENDED 2026-08-13 ---
SEQUENCING CHANGED - PYTHON FIRST, NATIVE PLUGIN AFTER (Tom, 2026-08-13):
'I also think we're going to have to have a certain measure of magiCam code in python simply to allow alchemy to function well. For the first pass of getting all this to work in Unreal we can potentially remove some of the stringent "this must be a plugin in UE" requirements and save that for after we have a working pipeline - and it sounds like we want to start with Python to make this as easy to build as possible (and we already have a lot of the working code for that).'
So the store-plugin bar is NOT the bar for the first pass. Build the Unreal previs path in PYTHON against what already works, prove the pipeline end to end (#1008 is the acceptance test), and port to C++/Slate afterwards once the shape is known.
This does not weaken the boundary recorded above, it re-orders it. The rule that survives untouched is the important one: the panel and Alchemy are two callers of the SAME underlying op. A Python-first implementation satisfies that just as well as a C++ one - cam.edit verbs are already the single source of rig behaviour and already have two callers.
Why this is the right order, concretely:
- The per-shot half is ALREADY BUILT and proven in Python: create_magicam_from_shot, _register_in_sequence, _ensure_level_sequence, camera cuts, media sections, _fit_playback_range, _key_rig_pose. Rewriting that in C++ before anything has assembled a full timeline would be porting an unproven design.
- A 'certain measure of magiCam code in Python' is permanent, not scaffolding: Alchemy orchestrates the timeline build (see the boundary note above), and that orchestration is Python by definition. The Python layer is not a stepping stone to be deleted - it is the Tier-2 side of the two-tier model.
- What the C++ port then owns is the CORE: rig, native caddy, TVPaint import, lighting - the things a Fab customer uses with no Alchemy present.
Practical consequence for this ticket: #3982's 'compiling skeleton plugin' deliverable can be deferred. The design doc still has value NOW (boundary, module layout, engine matrix, licensing) because it is what the Python-first work should be shaped to become - but it should be written knowing the first implementation is Python, and it should record which Python surfaces are expected to migrate into the C++ core later.
Knock-on for the sprint: #3983 (native Camera Caddy) and #4015 (retire the PySide caddy) both sit behind the native plugin existing, so both move later than #1008. The PySide caddy is therefore load-bearing for longer than the supersede note on #3983 implies - do not retire it while the Python path is the working pipeline.
#4031select_control adopts the rig into whatever sequence is open - thread adopt through to engage_rigreadyBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Unreal Engine·2 months ago
cam.edit.select_control calls engage_rig with the default adopt=True, so pressing a Main/Dolly/Height/Tilt control button can write a stray Control Rig binding into whatever Level Sequence happens to be open. The native Camera Caddy deliberately passes adopt=False when a camera is picked, but that care is bypassed one layer down the moment a control button is pressed. Thread an adopt parameter through select_control (default False for UI-driven calls, or resolve the rig's own shot sequence rather than adopting the open one), and make the panel pass it explicitly. Acceptance: with an unrelated sequence open, pressing a control button on a rig that no shot sequence carries must not add a binding to that open sequence.
#4083Declared editor state: assert the environment Alchemy needs, and show deviations instead of behaving strangelyreadyDesignMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Nearly every surprise in the first real day of driving the UE tools was a dependency on AMBIENT STATE nobody declared - the tool adapted to whatever it found instead of establishing what it needs, so problems were discovered by tripping over them. Real examples from 2026-08-14: Sequencer's camera-cut lock was globally ON (set by an unrelated legacy op) and hijacked three viewports; the AlchemyUE plugin was absent from the project so a C++ panel silently did not exist; the bridge was not listening so the panel sat disconnected in silence; the Control Rig asset was not resident so controls did not draw; the component's transient mapping was stale so drags moved nothing; viewport Realtime decides whether a drag reaches the camera at all. Build a DECLARED STATE contract: (1) name the state Alchemy requires of an editor session - plugin installed and enabled, bridge connected, viewport layout established (#4082), camera-cut lock off unless a feature asked for it, Realtime on, rig asset resident, mapping valid; (2) a CHECK that reads all of it and returns a per-item pass/deviation report, cheap enough to run on demand and non-mutating; (3) an ASSERT that puts the session into that state (Set Layout is one piece of this, not a standalone feature); (4) SURFACE it - the caddy / Alchemy panel shows 'N things are not as expected' with what and why, instead of the tool behaving strangely and the artist guessing. Also expose the check as a PREFLIGHT the app can run BEFORE launching the editor, so a project missing the plugin is reported on the location card rather than discovered inside UE. Design the state list as data (one place naming each item, how to check it, how to assert it, and whether asserting is safe) so adding an item later is a data change - same shape as the rig spec driving the rig, validator and docs.
#3984Rig authoring without the bridge: generate BP_MagiCam + Control Rig asset from magicam_rig_spec.json nativelyafter #3982blocked by #3982New featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
A store user has no Alchemy Python bridge, so the plugin itself must be able to create a MagiCam rig: ship magicam_rig_spec.json + the spec shape curve data as plugin content, and implement Create MagiCam Rig natively (editor C++ or baked assets regenerated at build time) producing the same BP + Control Rig asset that #345 authors over the bridge - identical control names, hierarchy main->dolly->height->tilt, per-channel locks/limits, wire shapes from the spec polylines, camera under tilt, 10 autoscaling planes with opacity material. The spec JSON stays the single source of truth: plugin build regenerates assets from it rather than hand-maintaining a second rig definition.
#3987Parity gate: validator green on the native plugin rig as the release barafter #3984blocked by #3984New featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Unreal Engine·2 months ago
Wire the #3980 parity validator UE adapter to the natively-generated rig and make a green run the release gate for every plugin build: control-by-control conformance (names, hierarchy, locks, limits, shapes, attachments) plus the behavior probes (plane autoscale, lens snap) against a rig created by the plugins own Create MagiCam Rig - proving the native generator, the bridge-side builder and the Blender rig all satisfy the same spec. Add a one-command runner suitable for pre-release (headless editor commandlet or scripted editor run).
#3988Fab store release package: compliance, docs, demo content, versioningafter #3984blocked by #3984ChoreMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Everything between a working plugin and a store listing: Fab submission compliance (self-contained, no external Python/pip deps in the shipped plugin, naming/category/metadata rules), builds for the supported engine matrix, a demo map with a rigged sample shot showing the caddy driving the rig, documentation (quickstart + per-control reference generated FROM the rig spec so docs cannot drift from behavior), screenshots/video capture for the listing, versioning + changelog convention, license text, and a submission checklist with the pricing/free decision left explicitly for Tom.
#4006Lens chooser affordance - decide AFTER the native caddy exists, do not over-build the rigreadyAdjust existingMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The lens_chooser control is the lens preset slider (travel clamped to +/-47.25, snapping focal length through 16/24/35/50/85/120/200) but reads as a bare cube with nothing to slide along - Tom left it untouched in a shape-tuning pass because he could not tell what it did. DO NOT solve this by building a track and label strip into the Control Rig yet. Tom's direction 2026-08-13: the lens interaction will most likely live in the MagiCam Camera Caddy UI we are going to build (#3983), so building a full slider rig-side first risks duplicating it. What he DOES want preserved is the principle: being able to change the lens directly in the viewport without going to a panel is valuable and should survive whatever the caddy does. So: sequence this after #3983, then decide the split - what the caddy owns vs what stays a grabbable in-viewport control - and give the rig-side control only as much visual affordance as that decision needs.
#4015Retire the PySide Camera Caddy once the native UE panel supersedes itreadyChoreMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · alchemy camera caddy·Unreal Engine·2 months ago
Delete the PySide Camera Caddy once the native UE panel (#3983) is real and in use, so the rig has ONE control surface instead of two drifting ones.
Tom, 2026-08-13: 'once the plugin exists, i don't care about this pyside ui anymore.' The native panel supersedes it - the PySide caddy is a reference implementation while #3983 is being built and a dead surface after.
Scope:
- alc/plugins/alchemy/tools/camera_caddy/ (the widget package, incl. its __main__ standalone entry)
- alc.plugins.unreal.tools.launchers.launch_camera_caddy and its re-export from alc/plugins/unreal/tools/__init__.py
- the cookbook's Camera Caddy button, if it still points at the PySide launcher (see #339, cb-unreal-menus-alchemy) - re-point it at the native panel rather than deleting the button
- any docs/menu entries naming the PySide caddy
Do NOT touch cam.edit. The verb layer is the single source of rig behaviour, it outlives both UIs, and that is precisely what makes this deletion cheap. Nothing in this ticket should change rig behaviour - if removing the UI requires a verb change, the native panel was not actually at parity and that belongs back in #3983.
Acceptance: the native panel does everything an artist used the PySide caddy for, the PySide package is gone, no import of it remains anywhere, the test suite is green, and the Camera Caddy entry point an artist reaches from the Alchemy menu opens the NATIVE panel.
#3985Native TVPaint import: clip_export.json to multiplane camera rigs in UEafter #3982blocked by #3982New featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Port the MagiCam TVPaint importer natively: parse clip_export.json (TVPaint 11 AND 12 schemas - layer schema identical, clip/camera-level fields differ), build one MagiCam rig per clip with its layers on depth-staggered image planes, textures assigned, plane visibility per layer, matching what magicam/blender build.py does on import. C++ JSON parsing in the plugin (no Python dependency for store users).
MagiCam Functional
40
Drag items or batches here, or hit to add one.
Mag0
Drag items or batches here, or hit to add one.
Schedule Pipeline
31
#4195The Schedule smart task owns the schedule's MSD, so a plan can version up and publishreadyNew featureSchedule PipelineSchedule Smart TaskTom
·Plugin · smart_task·Alchemy·2 months ago
Give the 'sch' production task a smart task that OWNS the schedule's data — MSD creation and reading — so a schedule versions up and publishes like every other task, instead of living as loose JSON in the cookbook.
Three layers, and the point of the ticket is putting each value in the right one:
1. COMPANY DEFAULTS — the cookbook. Default head count per department (today hardcoded as DEFAULT_SEATS = 5 in alc/services/schedule.py:~57), default day rate, and anything else that is a studio norm rather than a decision about this film. Tracked cookbook config, the fallback a new schedule starts from.
2. THIS SCHEDULE'S VALUES — the MSD for the sch task, versioned and publishable. project_start, target_end, headcounts, row_order (the hand-ordered rows from #4185), deliveries, day_rate, milestones. The schedule is fluid — head counts get argued over — so these are decisions about THIS plan, they need history, and a re-plan must not silently overwrite the plan the crew is working to.
3. VIEW STATE — stays local, never production data. day_px (zoom), collapsed lanes, and the 'groups' category filter are how one person is looking at the chart.
Deliverable:
- Add alc/plugins/smart_task/sch.py — a SmartTask subclass for 'sch' (the code already exists in production_tasks.json as 'sch | Schedule'; there is simply no brain for it, unlike dsn/mdl/rig/tex/ldv/edt/amc/prv). Give it an msd_data_model for the plan payload and get_msd_data() composing it. SmartTask already supplies the rest: .msd validates the payload, and commit_version() writes path_object.version_msd.data and commits (alc/plugins/smart_task/task.py:217-277) — that IS version-up-and-publish.
- Rewire app/tools/schedule/schedule.py to ask the smart task rather than the filesystem. Delete _config_dir/_settings_path/_load_settings/_save_settings and the deliveries equivalents (schedule.py:47-84), and the routes that write them (/settings, /settings/anchor, /settings/reset, /settings/row-order, /settings/row-order/clear).
- Follow the Bid tool's publish shape where it fits: the Schedule already reads the latest PUBLISHED bid from prd/docs/bid (app/tools/bidding), so a published schedule becomes an artifact the rest of the pipeline can point at the same way.
- Migrate what exists rather than dropping it — see notes.
--- AMENDED 2026-08-21 ---
MECHANISM — corrected after reading the code. MSD is 'Magic Scene Description' (docs/architecture/msd.md). There is NO msd.get()/set() API: task-scoped data is stored by resolving a PathObject for the task coordinate, writing into its version manifest's free-form data dict, and committing.
po = resolve_location(project_id, sync_root='VERSIONS', entity_type='assets',
parent='prd', child='docs', task='sch',
user=CFG.alc_user, version='000.000', recipe_type='publish')
if not po.task_msd.user_versions: po.start()
vmsd = po.version_msd; data = dict(vmsd.data or {})
data['schedule'] = plan; vmsd.data = data; vmsd.commit()
Working head is 000.000. version_msd at alc/path/path_object.py:996; VersionManifest.data at alc/models/version.py:194, commit() at :507.
VERSION UP AND PUBLISH follows the Bid tool exactly (app/tools/bidding/bidding.py:1660-1720): ensure_working_version(po) — po.start() first time, else po.version_up_minor(copy_source_files=False) (the helper lives at app/tools/script/script_viewer.py:350-358) — write the payload into new_po.get_source_path(dirname=True), then new_po.run_cookbook(recipe_types=['publish']). The publish recipe major-bumps and pushes the tracker. Read published versions back the way _bid_versions/_published_payload do (bidding.py:883-927): majors X.000, excluding 000.000.
THE POINT OF THE SMART TASK: Bid does all of the above INLINE in the web tool. That is the thing to not repeat. The sch smart task owns MSD creation/reading and the version/publish calls; app/tools/schedule/schedule.py asks the smart task and never touches PathObject, version_msd or run_cookbook itself. Today the Schedule tool touches none of them — its only pipeline touchpoint is reading the published bid via _latest_bid (schedule.py:141-149).
GROUNDWORK THAT ALREADY EXISTS: prd/docs is a production-documents-only pseudo-asset accepting scr/bid/sch/dck (alc/services/project.py:31); 'sch | Schedule' is a registered production task in the cookbook's production_tasks.json (verified against the live cookbook — 32 codes, sch present); and a cookbook 'alchemy/start/sch' recipe is already referenced (docs/guides/porting-a-dcc.md:336). What is missing is any Python that constructs a prd/docs/sch PathObject — a repo-wide grep finds none.
FULL SETTINGS INVENTORY to place across the three layers — project_start (:317), target_end (:318), groups (:343), collapsed (:347), day_px (:363), day_rate (:375), headcounts (:382), row_order (:434). Two more are computed per render and never persisted: priorities and milestones (schedule.py:245-253). Deliveries live in a separate sidecar with records shaped {id, name, date, scene_ids, asset_ids} (schedule.py:560-565); asset refs are 'group:slug' resolved live against the registries. POST /settings/reset currently keeps only project_start and groups (schedule.py:405-412) — that split should be re-derived from the three layers rather than preserved as-is.
alchemy prv0
Drag items or batches here, or hit to add one.
prv shelf1
#1599[cookbook] [alchemy] [prv] Implement the prv shelf/menureadyNew featureprv shelfstub
·Cookbook · alchemy · context-menus · prv·Alchemy·3 months ago
Pipeline Videos
60
Drag items or batches here, or hit to add one.
Nav Reorg
90
Drag items or batches here, or hit to add one.
Batches and To Do
90
Drag items or batches here, or hit to add one.
Design to Layout
213
B-1Concept-to-Layout C: Assemble and show it4
#4311Layout/set dressing: place built assets per manifestafter #4308blocked by #4308New featureB-1Concept-to-Layout C: Assemble and show itConcept to Environmentclaude
·Alchemy·4 days ago
Import built assets and place them per manifest relative to the ground plane, scale anchors and design camera. Blender first; result handed to Unreal.
#4312Overlay validation loop: design-camera render vs conceptafter #4311blocked by #4311New featureB-1Concept-to-Layout C: Assemble and show itConcept to Environmentclaude
·Alchemy·4 days ago
Render from the design camera, overlay on the concept art, and have an agent iterate placement until they line up. The before/after moment for the demo.
#4313End-to-end prototype: one small environment, 5-10 assetsafter #4312after #4301after #4309blocked by #4312, #4301, #4309New featureB-1Concept-to-Layout C: Assemble and show itConcept to Environmentclaude
·Plugin · smart_task·Alchemy·4 days ago
Run the full pipeline on one small environment (room or street block): one design camera, 3 anchors, 5-10 assets including one variant family. Compare against control A3 for accuracy and time.
#4314Demo video: two days by hand vs the automated pipelineafter #4313blocked by #4313New featureB-1Concept-to-Layout C: Assemble and show itConcept to Environmentclaude
·Service · videos·Alchemy·4 days ago
Open with the by-hand build and what was removed (wiring/technical chores), then the automated run with the concept-vs-render overlay as the centerpiece. Novice-friendly for non-technical decision-makers; motivated by directors' #1 pain, fast environments.
B-2Concept-to-Layout B: Build the assets6
#4305Fill-in generation: clean unoccluded image per manifest assetafter #4304blocked by #4304New featureB-2Concept-to-Layout B: Build the assetsConcept to Environmentclaude
·Alchemy·4 days ago
For each manifest asset generate a clean, unoccluded image filling in what the concept hides, in the same world style.
#4306Six-ortho generation in the format the Blender harness expectsafter #4305blocked by #4305New featureB-2Concept-to-Layout B: Build the assetsConcept to Environmentclaude
·Alchemy·4 days ago
From each fill-in image output the six orthographic views in exactly the input format of the existing ortho->Blender model harness.
#4307Style-lock mechanism and per-environment style contact sheetreadyNew featureB-2Concept-to-Layout B: Build the assetsConcept to Environmentclaude
·Ontology · models·Alchemy·4 days ago
Apply whatever recipe spike A1 proved (shared style reference/seed/prompt scaffold) so all assets in an environment share a style. Emit a style-consistency contact sheet per environment as an inspectable output.
#4308Manifest-to-harness runner: build every asset via ortho->Blenderafter #4306blocked by #4306New featureB-2Concept-to-Layout B: Build the assetsConcept to Environmentclaude
·Plugin · smart_task·Alchemy·4 days ago
Run each manifest asset's six ortho views through the existing agent harness (~15 min/asset): enforced scale, automatic turntable with scale reference, checkpoints at every stage. Report per-asset status.
#4309Variant families share a base mesh and derive variantsafter #4308blocked by #4308New featureB-2Concept-to-Layout B: Build the assetsConcept to Environmentclaude
·Alchemy·4 days ago
Variants in a family share a base mesh with derived variants for consistency and instancing in Unreal.
#4310Side-by-side mesh-model comparison on the same harnessafter #4308blocked by #4308New featureB-2Concept-to-Layout B: Build the assetsConcept to Environmentclaude
·Service · mesh·Alchemy·4 days ago
Run the same assets through the harness with different mesh-building models and compare results side by side, proving the harness is model-agnostic. Must not block the first end-to-end run.
B-3Concept-to-Layout A: Prove it23
#4300Breakdown-accuracy spike: model-produced asset manifest vs hand scorereadyNew featureB-3Concept-to-Layout A: Prove itConcept to Environmentclaude
·Alchemy·4 days ago
Have a model produce an asset list (name, category, variant group, rough scale, region) from the concept image, then hand-score missed assets and bad variant grouping. Report a hit rate.
#4303Environment manifest schema reusing the Alchemy ontologyafter #4298blocked by #4298New featureB-3Concept-to-Layout A: Prove itConcept to Environmentclaude
·Alchemy·4 days ago
JSON manifest spine: asset name, category, variant group, rough real-world scale, position/region in the design. Reuse the Alchemy asset ontology and the single register_asset() choke point; no parallel schema. Every later stage reads it.
#4304Breakdown task: concept + anchors to manifest with variant familiesafter #4300after #4303blocked by #4300, #4303New featureB-3Concept-to-Layout A: Prove itConcept to Environmentclaude
·Alchemy·4 days ago
Task that takes the concept image and anchors, identifies every asset, groups visually similar assets into variant families (e.g. 4 tree variants), and writes the manifest. Hand-correctable output. Informed by spike A2.
#4458Task map: chores and phase buttons (design)readyDesignWorkflow FeatureWorkflowsclaude
·App · pipeline_viewer·Alchemy·2 days ago
Build/Render/Review chores run INSIDE a phase button and have no button of their own to film. Design what a video for them is (an overview per phase?) before building. Not scoped.
storyboard_pro
12
#4528[cookbook] [storyboard_pro] [srb] Implement the Storyboard shelf/menureadyNew featurestoryboard_prostub
·Cookbook · storyboard_pro · shelves · srb·Storyboard Pro·a day ago
#4579[cookbook] [storyboard_pro] [srb] Implement the Ensure Layer Names buttonreadyNew featurestoryboard_prostub
·Storyboard Pro·7 hours ago
Check the rename tool's manifest to ensure that all layers are named or ignored.
Cloud Publish Sync
2
19
B-31Sync A: Foundations210
Drag tickets here.
B-32Sync B: Push and pullafter B-31 Sync A: Foundations4
#4604Content-addressed blob store with pluggable object-store transportblocked by #4608, #4614New featureB-32Sync B: Push and pullCloud Publishclaude
·54 minutes ago
Build the alc/services/sync/ blob layer: put(path)->hash, has(hash), get(hash, dest) against an object store, keyed files/<hash>, skip-if-exists so identical content is never re-uploaded (this is what keeps published light). One transport interface with an S3 implementation first; downloads for other sites go through short-lived signed URLs so a laptop never needs bucket credentials. Parallel multipart upload, retries with backoff, hash verified after download. Hash each file ONCE per publish and reuse the result across delta check, manifest, and upload (JHCS hashed three times). Depends on: Sync manifest schema ticket.
#4605Push-on-publish: sync step at the end of the publish recipeafter #4604blocked by #4604, #4608, #4614New featureB-32Sync B: Push and pullCloud Publishclaude
·Cookbook engine·54 minutes ago
Add a final step to the publish recipe that builds the event from the published files (msd source_files and render_files, sequences collapsed to a range), skips entirely if there is no real delta vs the cumulative manifest, uploads blobs, then writes the delta manifest and finally the ledger entry LAST so a half-finished push is never visible as an event. Order is copy, verify, then register; register_asset stays the single choke point and records the event id. A failed push leaves the local publish intact and surfaces the error (no silent fallback) with a retry button. Per-show toggle, off by default, like the ShotGrid sync. Depends on: manifest schema ticket and blob store ticket.
#4606Pull and apply engine: cursor, resumable events, fetch-on-demand vs mirrorafter #4604blocked by #4604, #4608, #4614New featureB-32Sync B: Push and pullSite Syncclaude
·54 minutes ago
Pull side of sync: list ledger events after this site's last_applied cursor, download manifests, fetch only blobs whose local hash differs, apply in event order. Per-event state (cloud_only, downloading, applied, error) kept locally, resumable after a crash, cursor advanced only after an event fully lands. A missing manifest is a reported error, not a crash (JHCS returned None and failed). Two modes per project: mirror everything, or fetch on demand when an asset is opened, plus a pin option for offline work. Applies deletions from the manifest. Depends on: manifest schema ticket and blob store ticket.
#4607Site registry and per-site acknowledgementsafter #4606blocked by #4606, #4608, #4614New featureB-32Sync B: Push and pullSite Syncclaude
·54 minutes ago
Model each place that holds a copy (workstation, laptop, studio, farm, cloud) as a Site with id, name, and path root mapping. When a site finishes applying an event it writes an ack (site id, event id, timestamp, file count, state) to the store, so anyone can answer which sites have this publish. This is the data the activity view reads. Also holds per-site path mapping so the same relative path resolves correctly on each OS and drive. Depends on: pull and apply engine ticket.
B-33Sync C: Visibility and opsafter B-32 Sync B: Push and pull4
#4609Sync activity view: events, per-site state, live progressafter #4607blocked by #4607, #4604, #4605, #4606New featureB-33Sync C: Visibility and opsSync Activityclaude
·54 minutes ago
A page under the project tools showing the ledger: event list (task, user, version lineage, file count, bytes, time), expand to see the delta manifest files, and a site matrix showing which sites have applied which event, with state per cell. Live upload and download progress per event over the existing websocket (EventListener-style callbacks feeding /ws/agent or a sibling channel). Manifests were the most informative thing in JHCS, so make them readable: diff between any two cumulative snapshots. Depends on: site registry ticket.
#4610Agent chat messages for sync events (replace email notifications)after #4605after #4606blocked by #4605, #4606, #4604, #4607New featureB-33Sync C: Visibility and opsSite Syncclaude
·App · routes·54 minutes ago
When an event is pushed, applied on a site, or fails, log it as a recipe-style event with details.chat_messages so the existing agent_watcher delivers a curated bubble in the app chat over /ws/agent (e.g. Rig v004 published, now on laptop). Replaces the JHCS Mailgun email. Curated, per-event not per-file. Depends on: push-on-publish and pull engine tickets.
#4611Scoped sync credentials and per-project bucket via the control planeafter #4604blocked by #4604, #4605, #4606, #4607New featureB-33Sync C: Visibility and opsCloud Publishclaude
·54 minutes ago
Per-project bucket (or prefix) config stored with project settings, and short-lived scoped credentials issued by the control plane (WorkOS identity, AWS account from #4598) so a site can push and pull only for projects it is a member of. No long-lived keys in project files. Signed URL issuance for blob reads. Define what a laptop session is allowed to do. Depends on: blob store ticket.
#4613Sync integrity audit and blob garbage collectionafter #4604after #4606blocked by #4604, #4606, #4605, #4607ChoreB-33Sync C: Visibility and opsSite Syncclaude
·54 minutes ago
Audit command: walk the ledger and verify every manifest blob exists in the store and matches its hash, and that every site ack matches what the site actually holds. Report missing, corrupt, and orphaned blobs. GC command with dry-run default that deletes blobs referenced by no manifest, with a retention window. Depends on: blob store and pull engine tickets.
B-34Sync D: Local engineafter B-31 Sync A: Foundations1
#4612Local engine agent: registration and run-location routingblocked by #4608, #4614DesignB-34Sync D: Local engineLocal Engineclaude
·54 minutes ago
Design then build: a small agent on a laptop that runs the same cookbook and Blender bridge code against the laptop's own Blender, connects OUT to the server over a websocket like /ws/agent (no inbound ports), registers as an available engine, and executes steps the server routes to it. Add a run-location field to task and step runs (farm, server, my-machine) with a sensible default chosen by preflights and a per-run override. The Blender bridge stays at 127.0.0.1:9876 on whichever machine runs Blender; only the agent crosses the network. Output of a local run becomes a normal publish that feeds the sync pipeline. First deliverable: a design doc in dev/ and a spike that runs one step on a second machine.
Batches not in a sprint
B-4Environment Scale Reference50
Drag tickets here.
B-5Per-File Media30
Drag tickets here.
B-6Meshy Spend Tracking30
Drag tickets here.
B-7Asset list viewTo Do0
Drag tickets here.
B-8glb-height-scale-pivot30
Drag tickets here.
B-17creative-kanban-v150
Drag tickets here.
B-20Storyboard Pro Panel Spike30
Drag tickets here.
B-21Storyboard Pro Panel Build50
Drag tickets here.
B-22Storyboard Pro Panel Parity50
Drag tickets here.
B-23Harmony Panel60
Drag tickets here.
B-24subtitle-translation-editor190
Drag tickets here.
B-25Linked Folder Ingest90
Drag tickets here.
B-26Storyboard Pro exports130
Drag tickets here.
B-27accounts-roles-2026-10-27164
#4590View as user (super admin): see the site as Monika / David / any memberreadyNew featureB-27accounts-roles-2026-10-27View As Userclaude
·Service · identity·5 hours ago
Super admin picks a member from the people/org list and the whole app renders as that user: request_identity honours a signed alc_view_as cookie ONLY when the real session is super admin. Persistent banner 'Viewing as X - Exit'. Defaults to READ-ONLY (writes blocked with a clear message) with an explicit toggle. Audit log of start/stop. Exit restores true identity. Used to critique manager/artist experience.
#4591Onboard Gossman (Monika, manager) and Sleep Tight (David, manager)after #4590blocked by #4590ChoreB-27accounts-roles-2026-10-27claude
·App · routes·5 hours ago
Claim gossman and sleeptight as orgs (Create organization), invite monika as manager scoped to Gossman and david as manager scoped to Sleep Tight, accept as each, verify with View as that each sees only their company and assigned projects. Also decide how they reach the app (served instance vs own install).
#4592Experience critique pass: walk manager and artist views, file fixesafter #4591blocked by #4591ChoreB-27accounts-roles-2026-10-27claude
·App · shell·5 hours ago
Using View as, walk every page as manager then artist; list dead ends, leaked items, confusing empty states; file each as a ticket.
#4597People registry is install-wide: scope people.json per companyreadyBug fixB-27accounts-roles-2026-10-27Company Peopleclaude
·Service · people·4 hours ago
people.json is one file for the whole install (cookbook config_files), so People pages in every company list everyone and 'other project' counts mix companies (e.g. CGL's amv/jlt guests show on Gossman projects). Key the registry by company (people_{company}.json or a company map), migrate the existing file by project->company ownership, and filter the People page, share picker and org sync to the active company.
B-28MagiCam variable planes22
#4595[magicam] Stopgap: raise the bundled rig to 16 image planes until variable plane count landsreadyChoreB-28MagiCam variable planesMagiCam Variable Planesclaude
·5 hours ago
Extend the bundled rig template (magicam/blender/resources) from 10 to 16 image planes with their control bones and update the plane-count assumption in build.py/tests. Done = the shot_04 export imports all 11 layers through Open in Blender.
#4602[magicam] Verify variable plane count in Unreal: build 11 planes into CR_MagiCam and run the parity validatorreadyChoreB-28MagiCam variable planesMagiCam Variable Planesclaude
·2 hours ago
Open the right Unreal project (see #3999), run alc.plugins.unreal.magicam.build.build_all(planes=11) and verify: CR_MagiCam has 11 depth-slot controls, BP_MagiCam 11 PlaneRoot/Plane components, verify_control_positions is clean, and 'python -m alc.plugins.alchemy.tools.magicam_parity --dcc unreal --live --planes 11' has no FAIL rows. Also check a 10-plane rebuild still works. Done = all of that green, or the defects filed.
B-29public-edge-2026-10-283
#4599Rebuild embed.alchemypipe.com (task-map embed host) in the cglumberjack AWS accountneeds detailChoreB-29public-edge-2026-10-28Public Edgeclaude
·Service · identity·2 hours ago
Re-provision the static S3+CloudFront embed host in account 966725470643 using cloud/maps/provision.ps1 + provision_finalize.ps1 (new bucket, OAC, directory-index function, ACM cert in us-east-1), republish with deploy.ps1, repoint the GoDaddy CNAME for embed.alchemypipe.com, update cloud/maps/infra.json, confirm the Wix 'Embed a Site' iframe loads /task-map/.
#4600Script the public edge relay (EC2 + Caddy + SSH tunnel) so it is reproducible from the reponeeds detailChoreB-29public-edge-2026-10-28Public Edgeclaude
·Service · identity·2 hours ago
Check in cloud/edge/ with a provisioning script (key pair, security group with SSH limited to a given CIDR, t4g.nano AL2023, Elastic IP, Caddy user-data for app.alchemypipe.com) plus a teardown script and a README covering the GoDaddy A record, the ALC_PUBLIC_EDGE=1 requirement, and updating the SSH CIDR when the operator IP changes. Current relay: i-02ac006f8f586db1c, EIP 34.196.107.208, SG alchemy-edge.
#4601Run the edge tunnel automatically on login (Windows scheduled task)needs detailNew featureB-29public-edge-2026-10-28Public Edgeclaude
·Service · identity·2 hours ago
Install a Windows scheduled task (at logon, restart on failure) that runs dev/edge_tunnel.ps1 hidden, plus a status/stop helper, so app.alchemypipe.com comes back after a reboot without a manual PowerShell window. Also ensure Alchemy itself starts with ALC_PUBLIC_EDGE=1 whenever the tunnel task is installed.
New sprints and batches stick around empty; one clears itself once its last ticket leaves. Drag a batch onto a sprint to plan it.
To Do
3
B-7 Asset list view
#4431Asset list: show an environment's linked Assets as a columnreadyNew featureB-7Asset list viewLinked Assetsclaude
·App · routes·2 days ago
EnvironmentAsset now has an 'assets' list of category/name refs (drs/palmB) written by the environment publish (botanicGardens holds 15). The asset list (tracker grid) builds its Columns menu from the model's fields, so 'Assets' should appear there once the app is restarted on the new model: confirm it does, and render it as pills grouped by category that open the asset's dashboard on click, not a comma string. On the Dressing/Props tabs add a derived 'Environments' column (which environments list this asset), looked up from the environments' assets lists, never stored on the asset.
#4432Asset list: columns come up empty for published environmentsreadyBug fixB-7Asset list viewclaude
·App · routes·2 days ago
The Environments list shows Description, Scenes, Height and Room as blank for botanicGardens even though it is Published. Checked 2026-10-25: the registry record itself is empty for all four (description '', scenes [], height None, room None), while the environment's assets hold real values (palmB height 190.3 cm, benchA 40.0, both with descriptions). So this is mostly missing data, not a render fault: decide where each environment field is meant to come from (description from the design publish, height/room from the breakdown or the curated set, scenes from the script) and write it on publish; and where a value is genuinely unset show a quiet dash so empty-by-design is distinguishable from broken.
#4433Asset list: column headers don't line up with their cellsreadyBug fixB-7Asset list viewclaude
·App · web ui·2 days ago
In the asset list (Environments tab, Dressing tab likely the same) each header should sit directly over its column of cells. Reported from a screenshot where the field headers (Description, Scenes, Height, Room) float with nothing beneath them and the whole header strip reads as unrelated to the row. Make header and cell widths agree: a fixed or shared width per column (table-layout / colgroup) instead of each header sizing to its own label while cells size to their content, so empty columns keep their width and header and cell stay together; keep the Priority column pinned right.
In Progress
6
#4467Workflow runs are tutorial VERSIONS (PathObject), not run_NNN foldersclaude:wf-fNew featureB-18Workflow F: Versions + publishWorkflow FeatureWorkflowsclaude
·App · walkthroughs·Alchemy·2 days ago
Today a workflow run writes into a numbered folder INSIDE the tutorial's version (.../Main/tom/001.001/high/runs/<id>/run_003) - the only place in the app that versions inside a version. Replace it with Alchemy's own mechanism: each run mints the NEXT MINOR VERSION of the tutorial coordinate through PathObject (po.start() to register the coordinate if needed, then version_up_minor() - see alc/path/path_object.py) and writes its outputs into that version (video, 720p proxy, poster, per-step stills, timeline.json, narration outputs, the tutorial page; filenames {Name}.* per the alchemy_tutorials pathmap's filename token). Media goes in the version's render tree, the workflow spec/timeline in source (follow PathObject's source/render conventions). No runs/run_NNN folders or hand-built paths anywhere: the Make bar, batch runner, library page and narration all list and open VERSIONS via PathObject. Old runs under runs/ are left alone (not migrated, not deleted).
#4468Publish replaces promote: clean runs publish via PathObject; readers use the published areaafter #4467claude:wf-fNew featureB-18Workflow F: Versions + publishWorkflow FeatureWorkflowsclaude
·App · pipeline_viewer·Alchemy·2 days ago
Replace 'promote' with Alchemy's PUBLISH. 'Promote this run' becomes Publish: PathObject.publish() (makes the next MAJOR version and copies it to the published latest, updates downstream) on the tutorial coordinate. A CLEAN run publishes itself automatically (Tom: 'for now'; keep a single constant to switch it off; 'clean' = the existing run_is_clean rule: failed cleanup, missing result picture, skipped step); the Publish button remains for a run that was not clean. Everything that read the promoted files (promoted.json, {Name}.mp4 beside the runs) must read the PUBLISHED area through PathObject.publish_object instead: the task map (app/tools/pipeline_viewer/tutorials.py), the public-bundle exporter (cloud/maps/export_web.py), the B-roll library and batch runner, the Make bar links, narration. Remove the word 'promote' from code, UI and docs. Keep show_on_public_map. Failures are loud, never silent.
#4580[storyboard_pro] [srb] Export to Harmony: shelf step wrapping ExportManager.exportToHarmonyclaude:batch-26New featureB-26Storyboard Pro exportsHarmony Exportclaude
·5 hours ago
Add an srb shelf step (cookbook storyboard_pro/shelves/srb) + an edt create op that exports the open scene to Toon Boom Harmony via ExportManager.exportToHarmony(exportDir, scenePattern): setOriginalScene(true) so layers stay drawing layers, setHarmonyFormat (default harmony-92-standalone; confirm Harmony 27 opens it, else harmony-781), and decide setNestPanelsInSymbols / setApplyTransformationOnDrawing / setAddCameraRefLayer / setAudioExport / setExportScenesReferenceTrack in a short spike by opening results in Harmony 27. Output lands in the render dir of the coordinate object. Must run as a long job (export takes over a minute): do not block the panel poll. Done = the exported scene opens in Harmony with the right layer names, timing and audio.
#4589Role-aware UI: hide nav/settings/org surfaces a role cannot useclaude:batch27New featureB-27accounts-roles-2026-10-27Role Aware UIclaude
·App · shell·5 hours ago
Nav, org modal, People page, Cost/Bidding, Settings show only what the role may use. Org management = admin; People grants = admin + managers for their projects; destructive tools hidden from artists. Server-side checks stay authoritative; hiding is polish.
#4608Publish portability preflight: no local or unpublished referencesclaude:4608-preflightNew featureB-31Sync A: FoundationsPortable Publishclaude
·Cookbook engine·50 minutes ago
Preflight that runs before the sync step: scan the published file (blend, psd, etc.) for references to paths outside published locations or outside the project root (WIP, user directories, drive-letter paths) and fail with the explicit list of offending references and what to publish first. Every step spelled out in the preflight message. Handle linked libraries and texture paths for Blender first; other DCCs via the same contract. Depends on: none. It can ship before sync exists and is useful on its own.
#4614Audit: what breaks when a laptop uses the workstation server and Deadlineclaude:4614-auditChoreB-31Sync A: FoundationsRemote Accessclaude
·50 minutes ago
Prove the remote-control case before building anything: from a second machine open the workstation app in a browser, run a Deadline build, open the result locally. Document every place that fails: path resolution across machines (UNC vs drive letter), is_local_request trust in alc/services/identity.py, global active-project state (#290), websocket reachability, any hardcoded 127.0.0.1. Output: a short report in dev/ with a fix list ranked by blocking vs annoying. No fixes in this ticket.
Done
597
#11on the kanban board backlog swim lane, replace the p1, p2 "widgets" with "Zones"Adjust existing
·App · kanban·Alchemy·3 months ago
these zones should just be boxes with colored outlines (like highlights) that appear when we hover over while dragging, whichever zone we drop into becomes the priority.
#4294Storyboard Pro: Alchemy menu (Build / Render / Review / Publish) via script packageNew featureStoryboard Pro Menusclaude
·Storyboard Pro·3 days ago
Add a Storyboard Pro script package (configure.js using ScriptManager.addMenuItem/addToolbar/addAction) that registers an Alchemy menu with Build, Render, Review and Publish items, first as hello-world buttons, then driven from the cookbook like the Blender menus. First step: test whether Storyboard Pro honors targetMenuId (Utils, or its own Alchemy menu) and find where user packages load from. Follow-ups: a live-session transport into the open app (batch -compile mode crashes on LayerManager.importImageAsLayer, MISSING SESSION on opacity), then an Import menu item that runs the TVPaint clip_export.json -> panels/layers rebuild (proven manually via ALC_Rebuild script). Parent: #4292.
#4296Storyboard Pro: generate the Alchemy toolbar/menu manifest from the cookbookNew featureStoryboard Pro Menusclaude
·Storyboard Pro·3 days ago
Make the Storyboard Pro Alchemy package cookbook-driven like the Blender menus: a cookbook storyboard_pro menus/shelf recipe (phase steps Build/Render/Review/Publish -> toolbar buttons, other steps -> Tools menu utilities) generates AlchemyMenu/manifest.json (and copies icons), via a Menu class + templates, and a deploy step installs the package into the Storyboard Pro user scripts packages folder. Buttons keep hello-world handlers until the live-session link (follow-up). Builds on #4294 (manifest.json is the hook).
#4396Design publish: curated asset registration, Name Assets picker, reload after publishBug fixCurated Asset Publishclaude
·3 days ago
Fixes from testing the design (dsn) publish on env botanicGardens. (1) Register Curated Assets / Create First Design Versions skip anything already registered or versioned instead of erroring (drop the published.json ledger + CollisionError), and run inside context.scope_for(env_po) so registry reads/writes hit the version's project, not the config default. (2) Publish errors name the asset by category/name(/variant), never the model's candidate id. (3) Name Assets modal: no concept selected shows a picker (thumbnail per already-broken-down image) or an error when none exist, instead of guessing the first *front* image. (4) After the publish phase finishes in the web UI the page reloads so the result shows in the published area. (5) FOLLOW-UP: shelf Name Assets button preflight (cookbook alchemy/shelves/dsn/NameAssets.py) must stop requiring a *front* image: use the version's main image first, otherwise list all images and let the artist pick.
#4398ui elements on this page should filter by their settings for example if Name Assets is only for environment assets - it should not be visible on the shelf. same goes for context menus.New featureuser
·Alchemy·3 days ago
#4399Generate Mesh: build from textured > clay > main image on Deadline, with on-page progress; asset descriptionsNew featureGenerate Meshclaude
·Service · mesh·3 days ago
A Generate Mesh shelf button on the design shelf builds the mesh (Meshy) from the best image the asset has - main_textured, else main_clay, else main - as a Deadline job with an on-page progress bar that finishes when the glb is written. Isolate Subject sets the cutout as main; Clay Image records main_clay; the dsn msd carries main_clay, main_textured and isolated. Describe Form (modal, Gemini vision) writes the asset description; the naming modal edits it; publish writes it to the asset and Ensure Description fails when it is empty. Characters gain a description field.
#4400the version thumbnail (up in the left hand corner) doesn't display a thumbnail for the version, the version thumbnail should be set as soon as either 1 image exists, or whenever an image is set to main.New featureuser
#4402dsn publish recipe: Publish step sorts before the environment stepsBug fixclaude
·Cookbook · alchemy · publish · dsn·Alchemy·3 days ago
In the dsn publish recipe, Publish has order 12 while Populate Profile Images, Register Curated Assets, Create First Design Versions and Copy Crops To Source are 13-16, so Publish runs before the env steps that register assets and copy crops. Publish should be the last step.
#4403our transform tool on the edit image window needs to be able to rotate like a photoshop transform tool where when i hover around the corner i get the rotation arrows and can rotate.New featureuserVoice Over
·Alchemy·3 days ago
#4404right now i am trying to set the _isolated image in this version's render director as "main" and i can't.Bug fixuser
·Alchemy·3 days ago
#4405dsn build: Generate Model step returns before building the meshBug fixclaude
·Cookbook · alchemy · build · dsn·Alchemy·3 days ago
The Generate Model step in the dsn build recipe has an early return right after it chooses its image, so it never builds a mesh. It should build the glb with Meshy when the glb does not exist, and pass without regenerating when it does (regenerating is the Generate Mesh button's job).
#4407when i load a version we should check for image descriptions and generate them if they are missing.New featureAuto-Describe Imagesuser
·Alchemy·3 days ago
#4408Height ruler button: front-orthographic glb viewer next to genMan with scale + cm height controlsNew featureHeight Scalingclaude
·App · image_editor·3 days ago
On the version page's HEIGHT field (props, characters, dressing), add a ruler-icon button that opens a viewer built on the Scale Reference stage's 3D viewer. It loads the asset's current glb in a front-facing orthographic view beside the genMan model (SCALE_HUMAN_MODEL). Controls: a Scale control (multiplier) and a Height control in cm; both resize the asset and stay in sync. Saving writes the resulting height back to the HEIGHT field in cm.
#4409if main is set and i can see the tag -i it should also be set where it says "slots" - in the toolbar - right now the thumb should show up there.New featureuser
·Alchemy·3 days ago
#4410when i click run all - i can see different steps passing in the console but i don't see their dots turn green until the entire recipe is doneNew featureuser
·Alchemy·3 days ago
#4411dsn Render recipe: isolate, describe, clay, mesh, height and scale reference in one Run AllNew featureDesign Render Automationclaude
·App · web ui·3 days ago
The dsn Render phase runs six idempotent Ensure steps in order: Clean Main (isolate subject), Asset Description, Clay Image, Design Mesh, Height (measures the glb and stores it flagged as measured; the Height ruler pulses amber until a person saves a height), Scale Reference (headless jpg of the height viewer beside the mesh). Saving a height in the viewer or the shelf Height box redraws the jpg. Any Run All or step run reloads the version page when it ends, pass or fail.
#4412add a way of deferentiating by category if i have an asset task selected in the header. All, Characters, Environemnts, Props, Dressing - we want to be able to have different videos for each because they have different steps.New featurePer-Category VideosuserVideo
·Alchemy·3 days ago
#4413Production tasks declare the file types they take in and deliverChoreclaude
·3 days ago
Fill in production_tasks.json with each task's file handoffs (inputs and outputs, with sources where verified) and todo_types, which the pipeline graph's failure-points view reads. Includes dsn outputs, mdl inputs/outputs and the max and unreal software ids on mdl.
#4414Register the Premiere render recipe in the cookbook registryBug fixclaude
·Adobe Premiere Pro·3 days ago
premiere/render/default/default.json (SaveSession, EnsureAlchemyPath, SetRenderPath, RenderTimeline) exists but the cookbook registry has no premiere > default > render entry, so the Premiere render phase is not wired to it. Add the registry entry.
Ensure front, back, left, right, top, bottom views exist for this asset.
#4416Common file tags: contract-declared tags on the version manifest, any taskNew featureFile Tagsclaude
·Plugin · smart_task·Alchemy·2 days ago
Tags are the files a version's publish needs, by name, with a description. The task contract declares them (TAGS in smart_task/<task>.py); the version manifest holds them (VersionManifest.tags, root-relative path); smart_task/tags.py (tag_file/untag/list_tags/tags_by_file/missing_required) is the only writer/reader; file rows show pills from it. dsn is the first user: main + six ortho views. Replaces dsn/roles.py with dsn/tags.py.
#4417[cookbook] [alchemy] [dsn] Implement the Ensure Design Sheet buttonNew featurealchemystub
·Alchemy·2 days ago
Uses the design sheet service for after effects to create a design sheet.
#4418[cookbook] [alchemy] [dsn] Implement the Design Sheet buttonNew featurealchemystub
·Cookbook · alchemy · shelves · dsn·Alchemy·2 days ago
Creates a design sheet using the design sheet service for after effects. If all required images are not present - it tells the user.
#4419[cookbook] [alchemy] [dsn] Implement the Ensure Required Views buttonNew featurealchemystub
·Alchemy·2 days ago
Ensure the required "tags" within the msd are filled out. Give the user a popup telling them what's missing if they are not there.
#4420Tag the scale reference as scale_ref wherever it is made or updatedNew featureclaude
·Service · mesh·Alchemy·2 days ago
Declare scale_ref on the dsn tag contract and write it to the version MSD from the one place the scale reference jpg is made (ensure_scale_reference), so both the Ensure Scale Reference step and the height viewer's Save keep the tag current. REVIEW_ORDER in dsn tags.py names scale_ref, so the custom per-category review (Create Mov Preview) can order its media by tag.
#4421when i click the edit button on an individual step for the recipe - it opens to the page, but it doesn't cary the path of the version I'm on with it currently.Bug fixVersion-Aware Step Editinguser
·Alchemy·2 days ago
#4422[cookbook] [alchemy] [dsn] Implement the Open Playlist buttonNew featurealchemystub
·Alchemy·2 days ago
If we're running this from the app - open the playlist in a new tab. If headlies - skip.
#4423Spend: price credit-billed providers (Meshy) in the rate tableNew featureB-6Meshy Spend TrackingSpend Pricingclaude
·Service · ai·Alchemy·2 days ago
Teach alc/services/ai/spend.py to price credit-billed providers. Add a 'meshy' provider to alc/services/ai/rates/ai_rates.json with unit 'credit': credits per task type (image_to_3d, rigging) plus a 'usd_per_credit' conversion, and bump the rates 'version'. Add a 'credit' branch to price_usage() returning credits * usd_per_credit, and a meshy_usage(task_type, task_response) helper that returns raw usage {'calls': 1, 'credits': N, 'task_type': ...}: read credits actually consumed from Meshy's task response if it reports them (cost_basis 'measured'), else fall back to the rate-table value (cost_basis 'nominal'). Raw credits are stored in usage so runs can be re-priced when the dollar rate changes. Extend tests/test_ai_spend.py to cover the credit unit, the measured vs nominal basis, and estimate_batch for meshy.
--- AMENDED 2026-10-25 ---
SCOPE (Tom): record whatever the provider gives us — credits OR USD. Do not block on a verified dollar price. The event details carry BOTH attrs: 'credits' (raw provider credits, null when unknown) and 'cost_usd' (null when no USD rate/figure is available, instead of a guessed number). usage keeps the raw credits. Any dollar conversion ('usd_per_credit') is optional in the rate table; if it is absent, cost_usd stays null and credits is still recorded. Rationale: a future store will hold credits and cost side by side, so the attrs must exist on the model now. Update spend_details() to emit 'credits' for credit-billed providers and keep existing nano_banana/seedance events unchanged (credits absent or null there).
--- AMENDED 2026-10-25 ---
MESHY PLAN RATES (Tom, from meshy.ai/pricing): the dollar value of a credit depends on the subscription plan. Pro $20/mo for 1,000 credits = $0.020/credit; Premium $40/mo for 3,000 = ~$0.0133/credit; Ultra $100/mo for 8,000 = $0.0125/credit. Tom is on Pro. Put these in ai_rates.json as a 'plans' table under the meshy provider (monthly_usd, monthly_credits, derived usd_per_credit) plus an 'active_plan' key set to 'pro', so cost_usd = credits * the active plan's usd_per_credit, and switching plans is a one-line data change. Store the plan name on the event (e.g. 'plan': 'pro') alongside rate_version so a run can be re-priced under a different plan; re-pricing from raw credits already works since credits are stored. Treat these as list prices from the pricing page; confirm against the Meshy account if they look off (extra top-up credits may be priced differently from plan credits). Optional follow-up idea, not in scope: a cost-per-plan comparison from recorded credits to show when upgrading pays off.
--- AMENDED 2026-10-25 ---
RETARGET (Tom): the Meshy plan table, active_plan and credits-per-task live in the SOFTWARE DB (software.json 'meshy' pricing block, created by #4425), NOT in ai_rates.json. Everything above that says 'ai_rates.json' means that pricing block. This ticket adds the 'credit' unit branch to price_usage(), the meshy_usage() helper, the credits/cost_usd(null-able)/plan event attrs, and tests; #4425 provides the registry data and loader.
#4424Meshy: record generation runs and spend as telemetry eventsNew featureB-6Meshy Spend TrackingMesh Telemetryclaude
·Service · mesh·Alchemy·2 days ago
Make Meshy runs show up on the task lane with cost, like nano-banana and Seedance. (1) alc/services/ai/telemetry.py: add a MESH kind ('Mesh Generation', action 'generate | mesh'); phase stays 'render'. (2) alc/services/mesh/meshy.py: wrap MeshyClient.image_to_3d and rig_humanoid in telemetry.run(telemetry.MESH, model=..., provider='meshy', account=spend.account_id(MESHY_API_KEY)), filling attrs with spend.spend_details(...) from the task response (uses the meshy_usage helper from the spend ticket). A FAILED task must still record status='failed' with its attempt in usage.calls. (3) Give the module-level meshy.run and run_rig their task address via telemetry.generating_for(output_path) (address_module only covers ops with an output_path param; check it applies, else wrap explicitly). Cache hits in mesh.image_to_mesh return before these run, so skipped steps must stay silent and add no cost. (4) Add tests mirroring the existing telemetry/spend tests: success event carries cost_usd/usage/rate_version/cost_basis/account, failure event is recorded, cache hit emits nothing.
--- AMENDED 2026-10-25 ---
SCOPE (Tom): record credits or USD, whichever the Meshy response gives us. The run event must carry both attrs ('credits' and 'cost_usd'), either of which may be null; a missing USD figure must not block or fail the event. Do not hard-code a guessed price. Failed runs still record the attempt (usage.calls 1).
--- AMENDED 2026-10-25 ---
NOTE: pricing is read from the software DB via #4425/#4423, not ai_rates.json.
Anything that costs money gets looked up from the software DB (software.json, loaded by SoftwareRegistry), not from a side file. Today Software has only a flat 'price' string + 'price_frequency', which cannot express per-call or credit billing. (1) Add a structured 'pricing' block to the Software model (alc/config/software.py) and round-trip it in to_dict()/SoftwareRegistry.save() without wiping the existing disk-only fields. Shape: billing model ('subscription' | 'per_call' | 'credit' | 'free'), currency, rate_version, and the rates for that model: per-unit rates (per image / per video-second keyed by resolution / per 1M tokens) for per_call, and for credit billing a 'plans' table (monthly_usd, monthly_credits, derived usd_per_credit) + 'active_plan' + credits-per-task-type. Keep the flat 'price'/'price_frequency' working for subscription tools shown in the UI. (2) Populate cookbook software.json entries: nanobanana (per_call, per image by model: current rates from alc/services/ai/rates/ai_rates.json), seedance (per_call, per video-second by model+resolution), meshy (credit; Pro $20/1,000 credits active plan, Premium $40/3,000, Ultra $100/8,000 — the entry already exists with price '20' monthly), and fill ElevenLabs and any other paid API in the registry that is blank. (3) Move spend.py's load_rates() to read these pricing blocks from the registry instead of ai_rates.json, keeping the same price_usage()/spend_details() outputs and rate_version stamping so existing nano_banana/seedance events and tests/test_ai_spend.py are unchanged; retire or reduce ai_rates.json to avoid two sources of truth. (4) A lookup helper, e.g. SoftwareRegistry.pricing_for(software_id, model=None), so anything (spend, estimates, UI) can ask what a tool costs.
--- AMENDED 2026-10-25 ---
TYPE FIELD (Tom): add a 'type' to the Software model saying WHAT KIND of thing it is, separate from 'category' (category = the discipline/UI column: 3d, texture, ai...; type = how it is delivered/run, which is what decides how it is billed and tracked). Values: 'desktop' (installed app, e.g. maya, blender, nuke), 'web' (browser/SaaS tool, e.g. shotgrid, notion, claude/chatgpt chat), 'ai_model' (hosted model called through an API and billed per call or credit: nanobanana, seedance, meshy, elevenlabs, kling, sora, runway), 'local_model' (model/tool run on our own hardware: trellis, comfyui, automatic1111, stable_diffusion). Default 'desktop' so existing entries keep working. Round-trip in to_dict()/save() like the other fields; backfill every entry in cookbook software.json (category 'ai' is the hard one and must be set by hand per entry, the rest can be inferred and reviewed). The pricing block's billing model should be validated against type (an 'ai_model' should not be 'free' with no pricing block; a 'local_model' is usually 'free'). Expose type as a filter/lookup (e.g. SoftwareRegistry.of_type('ai_model')) so spend and the software UI can list everything that costs per call.
·Cookbook · alchemy · publish · dsn·Alchemy·2 days ago
after the design publish - go through each asset we've created thumbnails/images for and run them through the render, review, publish chain.
#4434[cookbook] [blender] [lay] Implement the Import Published Assets buttonNew featureblenderstub
·Blender·2 days ago
Import the published models for this environment, this can happen in stages. prefer published models, if there are no published models, import the published design .glb file. this should be idempotent, so if run again it will attempt to ensure the published model is present, if not the design glb. it should use layout tooling for scene organization and blender linking etc...
#4435[cookbook] [blender] [lay] Implement the Build Design Camera buttonNew featureblenderstub
·Blender·2 days ago
Pull the camera data from the design publish and build the ground plane and camera accordingly with the design main image as the image plane on the camera with a sensible default opacity (maybe 30%)
#4438[cookbook] [blender] [lay] Implement the Snap Assets to Locators buttonNew featureblenderstub
·Blender·2 days ago
Snap Assets to their locators, if there are multiple locators for an asset create instances of the asset at that locator.
#4439Mesh().scale(): scale a glb to a height and save over itNew featureB-8glb-height-scale-pivotMesh Height Scalingclaude
·Service · mesh·2 days ago
Add a Mesh class in alc/services/mesh with scale(height_cm) that rescales a .glb uniformly so its bounding-box height (use glb_height_cm in height.py as the measure) equals the given height in centimetres, then writes the result back over the same file. Uniform scale only, no stretching. Idempotent: scaling to the height it already has is a no-op. Bake the scale into the geometry/node transforms so downstream consumers (Unreal, Blender, viewer) see correct size. Unit tests with a fixture glb.
#4440Mesh().pivot_to_bottom_center(): put glb origin at bottom centerNew featureB-8glb-height-scale-pivotGround Pivotclaude
·Service · mesh·2 days ago
Add Mesh.pivot_to_bottom_center() that translates a .glb so its origin sits at the bottom center of its bounding box (X/Z centered, Y min = 0 — respect the glb up-axis), saving over the file, so the model loads looking like it sits on the ground. Idempotent. Unit tests with a fixture glb verifying bbox min Y == 0 and center X/Z == 0.
#4441Run glb scale + bottom-center pivot when an asset's height changesNew featureB-8glb-height-scale-pivotMesh Height Scalingclaude
·App · routes·2 days ago
When an asset's height changes — typing in the Height field on the version page, or confirming the scale tool that opens from the ruler icon — and a glb exists for the active version, run Mesh.scale(height_cm) then Mesh.pivot_to_bottom_center() on that glb, saving over it. Runs after the height is stored (set_asset_height_cm). Surface failures to the user, no silent fallback. Refresh the viewer/shelf height glb afterward so the new size shows, and invalidate any scale-reference render that is keyed to height. No-op when there is no glb yet.
#4445i need another version up option on the versions + button - "copy {current version}" that would copy both the source and render files to the new version and relink existing msd tags, data, etc...New featureCopy Versionuser
·Alchemy·2 days ago
#4446clicked isolate subject and it failed. IsolateSubject failed: 'str' object has no attribute 'name'Bug fixIsolate Subjectuser
·Alchemy·2 days ago
#4447Cookbook: Workflow tab on a stepNew featureB-13Workflow B: Cookbook authoringWorkflow FeatureWorkflow authoringclaude
·App · cookbook·Alchemy·2 days ago
Add a Workflow tab beside Recipe / <Step>.py on a cookbook step (the tab strip is built in app/tools/cookbook/templates/partials/step_editor.html; add one route in cookbook.py). It finds the workflow whose tutorial coordinate (software/recipe_type/task/name) equals this step's and embeds the existing walkthrough editor + Run button + tutorial links. No workflow yet = an empty state: 'No workflow for this button yet' with a Create button (Create is the draft generator, a later ticket; until then it can open a blank workflow with the coordinate set). Make the editor embeddable: no full-page back arrow or height assumptions. Done when: Clay Image's tab shows its workflow, runs it, links to its tutorial; a step with none shows the empty state.
#4448Workflow: sample version picker filtered by the button's 'Applies to'New featureB-13Workflow B: Cookbook authoringWorkflow FeatureWorkflow authoringclaude
·App · walkthroughs·Alchemy·2 days ago
The button's 'Applies to' categories (e.g. Props, Set Dressing) say which assets it works on. In the workflow's 'Working on' picker, offer only versions of assets in those categories (reuse the existing picker, which lists real folders), and remember the choice on the workflow. Done when: for Clay Image, the picker's Parent options are limited to the categories it applies to.
#4449Create a draft workflow from a shelf buttonNew featureB-13Workflow B: Cookbook authoringWorkflow FeatureWorkflow authoringclaude
·App · walkthroughs·Alchemy·2 days ago
'Create' on the Workflow tab drafts a workflow from the button: steps Go to My Tasks / Click the task / Click <button label> / View the result; a Cleanup section; tutorial coordinate + name from the cookbook step; the clicked step's Tutorial detail filled from the button's Goal; sample version chosen via the picker. Before saving, verify the button's control on the live Version View for the chosen sample (an unverifiable control is an error, never a guess). v1 scope: Alchemy shelf buttons on version-view tasks (the Clay Image pattern).
#4450Auto-create the draft workflow when a button is createdNew featureB-13Workflow B: Cookbook authoringWorkflow FeatureWorkflow authoringclaude
·App · cookbook·Alchemy·2 days ago
Hook the create-step flow so a new shelf button starts with a draft workflow (no sample yet = a draft that asks for one). The cookbook step list marks buttons that have no workflow.
#4451Cookbook text vs what really happens (drift check)New featureB-13Workflow B: Cookbook authoringWorkflow FeatureWorkflow authoringclaude
·App · cookbook·Alchemy·2 days ago
Compare file names in a button's Goal text with the files its workflow runs actually produced (known_outputs) and warn in the Workflow tab when they disagree. Also fix the Clay Image Goal text now: it says <name>_isolated.png / <name>_isolated_3dclay.png, but the real run reads drs_statue_dsn_main.png and writes drs_statue_dsn_main_3dclay.png.
#4452Show the RESULT in the tutorial ('Picture in the tutorial')New featureB-12Workflow A: Results + latest runWorkflow FeatureWorkflowsclaude
·Service · media·Alchemy·2 days ago
Per step, choose what the tutorial shows besides the control crop: nothing more, the screen after the action (e.g. the opened editor), or the RESULT FILE itself (a plain file name in the working version, picked from existing/known outputs). The result file is copied into the run's shots/ before cleanup removes it and shown under the control crop with a 'Result' caption; a missing file is a warning in the timeline, never a silent gap. Step 4 of Clay Image should show the clay image.
#4453A stable 'latest' for each workflow's media (promote a run)New featureB-12Workflow A: Results + latest runWorkflow FeatureWorkflowsclaude
·Service · media·Alchemy·2 days ago
Runs are numbered folders; the task map and public embed need ONE address per button that always points at the good run. Add 'Promote this run' (or auto on a clean run) that writes {tutorial path}/{Name}.mp4 + poster + tutorial.html, plus a small web proxy (720p mp4) because full-size screen recordings are heavy for a public page. Numbered runs keep their history.
app/tools/pipeline_viewer/task_maps.py build_task_manifest(software, task) is the single source for the live task-map page and the static exporter. Add, per step, a `tutorial` entry when its workflow has a promoted run: {video, poster, page}. Buttons with one get a play badge; hover = poster preview, click = the video with a link to the tutorial page. Buttons with none look as today. (cookbook/task-map/alchemy/dsn is the page.)
#4455Public task-map bundle includes the tutorial videosNew featureB-14Workflow C: Task map videosWorkflow FeatureTask map tutorialsclaude
·App · pipeline_viewer·Alchemy·2 days ago
The static exporter (the one publish_task_map uses for the marketing embed) copies each promoted proxy video + poster into the bundle with relative paths, like icons. Opt-in per workflow ('show on the public map') so nothing unfinished leaks. Check size and hosting for large media.
#4456Run all workflows -> B-roll libraryNew featureB-15Workflow D: Run everythingWorkflow FeatureRun All Workflowsclaude
·App · walkthroughs·Alchemy·2 days ago
A batch that runs workflows one at a time (one browser at a time), reports pass/fail per workflow, and a library page indexing clips and stills by cookbook coordinate (feature -> latest run). Live runs of AI-backed buttons spend real money: confirm + a cost guard. Start serial; the farm later.
#4457Narration over the workflow videoNew featureB-16Workflow E: NarrationWorkflow FeatureWorkflowsclaude
·Alchemy·2 days ago
Link the video catalog's script rows to workflow steps; step holds follow the voiced row lengths; mux the table read onto the video. Use the timeline's page/concept timecodes so one recording can be retimed into marketing cuts.
#4459Creative Kanban: CreativeTicket model and per-project board storageNew featureB-17creative-kanban-v1Creative Kanbanclaude
·Ontology · models·2 days ago
New CreativeTicket model + CreativeBoard (sibling of WorkItem/WorkBoard in alc/models/work_item.py), stored PER PROJECT in the show's msd tree as an index file plus one JSON file per ticket, loaded/saved under the same cross-process lock pattern as WorkBoard.editing. Statuses: note, todo, in_progress, done, with status_history. Each ticket carries a source link (task_id, version, review_id, comment_id, asset id, frame, media relative_path) and empty slots for later agent handoff (candidate_files, capability/agent_ok). Per-board ticket number sequence. Unit tests for round-trip, split layout and lock.
#4460Creative Kanban: hydrate note cards from review commentsNew featureB-17creative-kanban-v1Creative Kanbanclaude
·App · kanban·2 days ago
Idempotent sync that walks every task's ReviewManifest in the active project and creates a CreativeTicket (status note) for each OPEN review comment, deduped on comment_id (pattern: pull_todos_from_notes in app/routes/version.py). Re-running adds only new notes; existing cards are untouched. Exposed as a Sync notes button on the board and a ticket_cli-style command. Done does NOT resolve the review comment in v1.
#4461Creative Kanban: board page with Notes / To Do / In Progress / Done columnsNew featureB-17creative-kanban-v1Creative Kanbanclaude
·App · kanban·2 days ago
Replace the greyed Creative Kanban placeholder in app/navigation.py (#4360) with a real ToolPath under My Work. Route /c/{cid}/project/{pid}/creative-kanban renders four columns (Notes, To Do, In Progress, Done) in the Code Kanban visual language, scoped to the URL's project. Cards show note text, thumbnail of the media/frame, asset + version chips and author. Sync notes button calls the hydrate endpoint.
#4462Creative Kanban: drag cards between columns and reorderNew featureB-17creative-kanban-v1Creative Kanbanclaude
·App · kanban·2 days ago
Dragging a card to another column persists its status and appends a status_history entry (lane, user, date); dragging within a column persists order. Reuse the Code Kanban move/reorder logic where it generalises. Survives page reload.
#4463Creative Kanban: card modal with deep link to the review noteNew featureB-17creative-kanban-v1Creative Kanbanclaude
·App · kanban·2 days ago
Clicking a card opens a modal showing the full note, its replies and attachments, asset/version, and a link that opens the review room at the note's frame. Text/spec are editable (autosave like the Code Kanban modal) and the card can be deleted.
One name for the thing: a Tutorial. Today it is 'walkthrough' (page, model, URLs, cookbook storage dir), 'workflow' (cookbook tab, sprint) and 'tutorial' (the output page and media). Rename user-facing text first (Pipeline menu 'Walkthroughs' -> 'Tutorials', the cookbook 'Workflow' tab -> 'Tutorial', Make bar, page titles, error messages), then the module/model/route names and the stored files (cookbook/walkthroughs/*.json -> tutorials), with redirects from the old /walkthroughs URLs. Run, version and publish keep their names. Mechanical: do it AFTER batch F (it touches the same files), in one pass, with the tests renamed alongside.
#4483Tutorial header: derive the label and description; remove title, description, stats, gauge and legendNew featureB-19Workflow G: Tutorial namingWorkflow FeatureWorkflowsclaude
·App · walkthroughs·Alchemy·a day ago
A tutorial's identity is its cookbook coordinate (software / parent / child, e.g. alchemy-dsn / clayImage), so its label and description are DERIVED, never stored: label = the cookbook step's label ('Clay Image'), description = the cookbook Goal. Remove `Walkthrough.title` and `Walkthrough.description` from the model and the editor (old JSON that still carries them is simply ignored - the model ignores unknown fields), and remove the header's '5 STEPS - 20% STILL MANUAL - DSN' line, the manual/auto gauge and the Phase/Web/TODO colour key. Everything that read `title` shows the derived label instead: the page header, the Make bar's confirm text, the tutorial page's h1 and intro, the library / batch / narration listings, the list-page cards, the duplicate-coordinate message. A coordinate that matches no cookbook step shows the humanized slug plus a visible 'no cookbook step for this' note - never a silent blank. Rewrite the tests that assert on titles; don't just delete them.
#4484Remove the manual-vs-automated layer from tutorials (TODO kind, mark for automation, todo type, ratios)ChoreB-19Workflow G: Tutorial namingWorkflow FeatureWorkflowsclaude
·App · walkthroughs·Alchemy·a day ago
Tutorials exist only for what is automated (Tom: 'i'm not creating tutorial videos for things i'm not automating'), so remove the layer that tracked manual work: the TODO step kind; 'Mark for automation' + its notes and the Kanban WorkItem backlink created by `update_step`; the step `todo_type` and `alc/config/todo_types.py` usage in the tutorial editor; `Walkthrough.manual_ratio` / the manual/auto counts and anywhere they render. Keep step kinds web and phase. Existing data: a stored TODO step must NOT be silently dropped - on load convert it to a web step with no page linked (it then shows as 'not linked' and the run lists it as skipped, loudly) so the author sees and fixes or removes it. The 3 steps currently flagged for automation already have real Kanban items: leave those items on the board as ordinary items (clear the flag only; never delete or close items). LEAVE the Walkthroughs usage pages (`walkthroughs_usage*.html`, the /usage routes) alone - they read telemetry/notes, not tutorials - but check they do not break; if they depend on the removed pieces keep the minimum shim and report it. Update/rewrite the tests that covered the removed behaviour.
#4485Delete the old process walkthroughs that have no cookbook coordinateChoreB-19Workflow G: Tutorial namingWorkflow FeatureWorkflowsclaude
·App · walkthroughs·Alchemy·a day ago
The cookbook's `walkthroughs/` folder holds ~80 files; most are the old capability / process maps (manual steps, no cookbook button behind them). Once everything is a Tutorial, a file with no tutorial coordinate tied to a real cookbook step is not a tutorial, and Tom decided they are more work to review than to remake: DELETE them (no archive, no review pass). Rule: keep a file only if it has a tutorial coordinate that resolves to a real cookbook step (e.g. the Clay Image tutorial, and any tutorial a Workflow tab created for a real button); delete every other file in `cookbook/walkthroughs/`. In the cookbook repo (E:/Alchemy/cgl/config/cgl-cookbook/cookbook, which has unrelated uncommitted changes: pathspec commits only, never `git add -A`): `git rm` the tracked ones (recoverable from history) and delete the untracked ones, in ONE pathspec commit whose message lists every removed file as id + title. Safety net for the untracked ones (not recoverable from git): copy them into a folder OUTSIDE the repos first and say where. Afterwards the Tutorials list, library and batch plan must show only the kept tutorials and nothing may still reference a removed file (check the Kanban work-item origin links and the usage pages' references; clear dangling ones, never delete work items).
#4486Remove the Walkthroughs usage pagesChoreB-19Workflow G: Tutorial namingWorkflow FeatureWorkflowsclaude
·App · walkthroughs·Alchemy·a day ago
Remove the Walkthroughs usage / analysis pages and everything only they use: the templates `walkthroughs_usage.html` and `walkthroughs_usage_analysis.html`, the /usage routes and their helpers (`_usage_data`, the promote-to-backlog proposals, the inductive todo mining / AI analysis), and the navigation links to them. They mined artist notes and button-click hotspots to propose automation work for walkthrough steps - a different purpose from tutorials, which exist only for what is already automated. Before deleting, grep for anything else that imports or links them (the cookbook, Kanban, telemetry) and fix or remove those references; if telemetry/analytics elsewhere still needs one helper, keep only that helper and say so. Remove or rewrite the tests that covered them.
#4506Cost of Automation: live sales-call assessment page (Pipeline nav)New featureStudio Cost Assessmentclaude
·a day ago
A sales-call page under Pipeline > Cost of Automation that walks a prospect from current state to Alchemy: Current State (pipelines needed, cost of technical chores, pipeline overhead, TD time, pain points), Traditional Costs (AAA vs AI method headcount and yearly cost), Alchemy (price anchor, phase timeline, supported software/tasks), Technical Chores (per-day chore sliders seeded from the self-estimate, detailed estimate, Art remainder, Jim Henson example with jobs covered) and Studio Flow (task/software folder hand-off flow). Tasks and software come from the Production Tasks and Software registries. Assessments save to and open from E:/Alchemy/assessments as dated JSON.
#4507Storyboard Pro panel spike 1: non-modal Alchemy window + dockabilityNew featureB-20Storyboard Pro Panel SpikeStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
From the AlchemyMenu toolbar package, open a NON-modal Uiloader/Dialog window that stays open while the user works in Storyboard Pro. Record whether it can be docked or only floats, and whether it survives panel/project switches. Write findings to dev/storyboard_pro_panel_feasibility.md (VERIFIED vs UNVERIFIED, like dev/harmony_feasibility.md). Needs a Storyboard Pro project open.
#4508Storyboard Pro panel spike 2: QWebEngineView reachable from script runtimeNew featureB-20Storyboard Pro Panel SpikeStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
Probe whether the QtScript runtime can construct Qt6WebEngineWidgets (QWebEngineView) or any web-hosting widget, and whether it can render a local/http page inside the window from spike 1. Record the exact constructor/binding names or the failure. Append findings to dev/storyboard_pro_panel_feasibility.md. Result decides whether the panel is a web view of the Alchemy UI or a native Uiloader form.
#4509Storyboard Pro panel spike 3: script-side socket to Alchemy app (:8080)New featureB-20Storyboard Pro Panel SpikeStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
From a Storyboard Pro script, open a QTcpSocket / QNetworkAccessManager request to the running Alchemy app on localhost:8080 and round-trip a JSON ping, driven by a QTimer so the UI does not block. Record what works and timer/event-loop behaviour. Append findings to dev/storyboard_pro_panel_feasibility.md.
#4510Storyboard Pro panel: live transport (script-side QTimer poller + Alchemy endpoint)New featureB-21Storyboard Pro Panel BuildStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
Replace the headless -batch op runner as the only path with a live link: a script-side QTimer(mainWindow) poller in the Alchemy package that talks to the Alchemy app over QTcpSocket / QNetworkAccessManager (see dev/storyboard_pro_panel_feasibility.md: socket to :8080 and QTimer ticking both verified). Alchemy side exposes a small endpoint the panel polls/posts to (model on the Premiere/AE bridge). Errors surface (no silent fallback); the panel shows connected/disconnected. Notes: new QTimer() needs a parent, QTimer.singleShot absent, QTcpSocket.write needs new QByteArray(...).
#4511Storyboard Pro panel: Alchemy dock in the real package (toolbar button, native Qt UI)New featureB-21Storyboard Pro Panel BuildStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
Promote the spike (dev/storyboard_pro_panel_spike/AlchemyPanelSpike_configure.js) into the AlchemyMenu package: an 'Alchemy Panel' toolbar button that lazily builds a QDockWidget on the main window (main window does not exist at configure time). Native Qt only (no web view available); no QVBoxLayout.addWidget (binding throws) - parent widgets and setGeometry, or find a working layout call. Shows connection state and the current shot context from the live transport (#4510). Installed by package/install.py; panel survives toggling visibility.
#4512Storyboard Pro panel: link current panel to Alchemy shot ids via panel metadataNew featureB-21Storyboard Pro Panel BuildStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
The panel reads the selected Storyboard Pro panel (StoryboardManager panel metadata: get/setPanelMetadata) and shows/sets its Alchemy ids (panel_id, shot id) per docs/specs/storyboard_pro_plugin_spec.md. Selecting a panel updates the Alchemy side and vice versa through the live transport.
#4513Storyboard Pro panel spike: register as a native view in the tab group / + menuNew featureB-21Storyboard Pro Panel BuildStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
Panel currently docks as a QMainWindow peer dock but cannot join Storyboard Pro's native tab group (Panel / Storyboard / Tool Properties / Library, with the '+' add-view control). Investigate whether a script can register a native view: the 'ScriptWidgets' mentioned in the scripting docs, 2700-layouts-xml workspace files, sb_menus.xml / layouts in resources. Append findings to dev/storyboard_pro_panel_feasibility.md; VERIFIED vs UNVERIFIED. Low priority: the peer dock already works.
#4514Storyboard Pro panel: move the Alchemy panel into a native scripted view (tab group)New featureB-21Storyboard Pro Panel BuildStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
ScriptManager.addView registered at package load shows as a native tab in Storyboard Pro's tab group (#4513, dev/storyboard_pro_panel_feasibility.md) but onCreate never ran (blank content) and the tab is titled with the id, not the text option. Find the right addView options/callback (inspect ScriptManager, try onCreate vs other names, loadViewUI with a .ui form), get content to render, then build the real Alchemy panel (status, project, selected panel, Tag/Untag, from panel.js) as the scripted view with a 3-argument layout.addWidget(w, 0, 0). Keep the QDockWidget as a fallback. Fix the dark-blue link colour (document stylesheet had no effect).
#4515Storyboard Pro panel: cookbook-driven actions inside the panelNew featureB-22Storyboard Pro Panel ParityStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
Render the storyboard_pro cookbook recipes (shelves + menus: Build/Render/Review/Publish, Import from TVPaint) as links in the panel and run their button modules server-side through the bridge (model: premiere bridge GET /menus + POST /menu/run), grounded to the open project, with errors shown in the panel. The toolbar buttons keep working; the panel is the second surface over the same recipes (one pipeline, no drift).
#4516Storyboard Pro panel: Request field files a board card from the live sessionNew featureB-22Storyboard Pro Panel ParityStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
A request field in the panel (model: premiere bridge POST /request): a sentence becomes a board card via the Alchemy app, filled in from the live Storyboard Pro session (project, selected panel, Alchemy identity), with the Storyboard Pro window as evidence image. Reuses the app request pipeline; no new card plumbing.
#4517Storyboard Pro panel: task todos for the current panel/shotNew featureB-22Storyboard Pro Panel ParityStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
Show the current task todo manifest in the panel (model: premiere bridge GET /todos), tick items off, add a note pinned to the selected panel (stores the panel id/frame like the Premiere playhead note). Reads/writes through the existing todos service via the bridge.
#4518Storyboard Pro panel: verify and harden (Untag, workspace reset, bridge restart, project switch)New featureB-22Storyboard Pro Panel ParityStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
Live-verify what the #4507-#4514 batch left untested: Untag by click, the Alchemy tab and dock surviving Windows > Workspace reset and an app restart, the poller recovering after the bridge restarts and after switching/opening another project, two Storyboard Pro instances against one bridge. Fix what breaks; record results in dev/storyboard_pro_panel_feasibility.md. Add slow-tick/backoff behaviour when the bridge is down.
#4519Extract the shared Toon Boom panel framework (hub, transport, panel scaffold, launcher)ChoreB-22Storyboard Pro Panel ParityStoryboard Pro Panelclaude
·Storyboard Pro·a day ago
Once the Storyboard Pro panel has its full feature set (cookbook actions, request, todos, hardening), lift what is not Storyboard-specific into a shared package so Harmony reuses it instead of copying: the bridge Hub (poll/exec queue), the launcher, transport.js, the panel scaffold (QTextBrowser host, scripted-view + dock mounting via require, link handling, step-logged mount) and the install helper. Storyboard Pro keeps only its context (panel selection, metadata, identity). Respect alc/plugins/CLAUDE.md: dependency direction {dcc} -> shared, never the reverse; no DCC API imported at module load. Tests move with the code.
#4520Harmony panel spike: what differs from Storyboard Pro (probed live through the bridge)New featureB-23Harmony PanelHarmony Panelclaude
·Toon Boom Harmony·a day ago
Harmony and Storyboard Pro share the Toon Boom QtScript engine, and the Storyboard Pro panel is built and merged (dev/storyboard_pro_panel_feasibility.md has every finding). So this spike is NOT "can a panel work in Harmony": it is "what DIFFERS in Harmony", and it should take a fraction of the Storyboard Pro one. Do it after #4521 (the bridge) so everything is probed LIVE through POST /exec on :20266, not by restarting Harmony per guess.
Bring-up: a minimal adapter stub (name, findMainWindow, context, jump) + the package from #4522 mounting the shared panel, then probe through /exec. Launch Harmony from a shell (Start-Process with the scene), never with computer-use open_application (it started a second instance). Harmony's headless mode (-batch -compile) is for ops only: widgets crash there and a startup -compile script runs once then exits.
Already settled, do not re-derive: Harmony's own production packages (SceneMarkers, MasterController, PortNames, Skinning, BreakdownAssistant) register views with ScriptManager.addView({id, text, action: "fn in ./configure.js"}), the same form that works in Storyboard Pro. (`onCreate`, as in help/extended/ScriptAPIDemos, silently never ran in Storyboard Pro.)
To verify in Harmony (VERIFIED vs UNVERIFIED in dev/harmony_panel_feasibility.md):
- the same traps hold: QTimer needs a parent, layout.addWidget(w, 0, 0) needs three args, QUrl.toString() returns debug text (use host()), a scripted view must never be string-concatenated, createView runs outside configure()'s scope (share through require()), registration only works at package load, QtScript files must be ASCII;
- which window findMainWindow() returns and its exact title (the screengrab needs it: a short substring can match several windows);
- the scene APIs the adapter needs: scene.currentProjectPath(), frame.current() / frame.setCurrent(n), selection.numberOfNodesSelected() / selectedNode(i) / selecting a node, and WHERE an Alchemy tag can live (a scene/node metadata API if one exists; otherwise a JSON sidecar next to the .xstage, not a binary one);
- File.Append (the existing shelf's logger uses it) vs the write-once logger that Storyboard Pro needed;
- the existing toolbar shelf (alc/plugins/harmony/shelf) keeps working beside the panel (its menu insertion limits are already noted in configure.js).
Output: dev/harmony_panel_feasibility.md and a mounted, connected (empty) panel; the adapter stub grows into #4523.
#4521Harmony panel: bridge, launcher and auto-start on the shared framework (do FIRST, with #4524)New featureB-23Harmony PanelHarmony Panelclaude
·Toon Boom Harmony·a day ago
Do this FIRST in the Harmony batch (and in the same session as #4524, one commit `Feat #4521 #4524: ...`): stand up the Harmony bridge on the shared framework, so every later ticket can probe and verify a live Harmony through POST /exec instead of restart-per-guess.
It is a copy of the Storyboard Pro host with the names changed. The shared PanelBridge / Launcher / menus loader already exist (alc/plugins/alchemy/tools/panel; checklist in that package's __init__ docstring).
- alc/plugins/harmony/bridge/config.py: BridgeConfig(software="harmony", display="Harmony", port=20266, server_module / launcher_module under alc.plugins.harmony.bridge, window_env="ALC_HARMONY_WINDOW", window_default="Toon Boom Harmony", panel_task="common" - harmony already has tasks/common).
- bridge/server.py: PanelBridge(CONFIG) and nothing else yet (host routes come with #4523). bridge/launch.py: the Launcher and its re-exports.
- Do NOT touch alc/plugins/harmony/batch: the headless runner stays for renders.
- Tests: copy the storyboard_pro bridge tests that exercise PanelBridge with the Harmony config (hub, request payload names Harmony, restart module, menus loaded from the harmony cookbook folder: harmony/shelves/alchemy exists).
- #4524 (same commit): the `.xstage` branch in alc/path/path.py alc_start, beside the `.sboard` one, plus a test.
Done = `venv/Scripts/python.exe -m alc.plugins.harmony.bridge.server` serves /status, /menus returns the harmony shelf (Version Up, Build, ...), tests pass. Nothing needs Harmony open yet.
#4522Harmony panel: mount the shared Alchemy panel as a native tab (AlchemyShelf package + adapter)New featureB-23Harmony PanelHarmony Panelclaude
·Toon Boom Harmony·a day ago
Mount the shared Alchemy panel in Harmony. Follow the Storyboard Pro recipe exactly (alc/plugins/storyboard_pro/package/AlchemyMenu/configure.js is the reference); only the names change.
- alc/plugins/harmony/shelf/package/AlchemyShelf: keep the existing toolbar shelf, and add (a) `ScriptManager.addView({ id: "Alchemy", text: "Alchemy", action: "createView in ./configure.js" })` at package load, (b) `function createView()` that calls `sharedPanel().mount(ScriptManager.getView("Alchemy"))`, (c) the dock fallback behind a toolbar button, (d) `sharedPanel()` = `require("./panel.js").getPanel({createTransport: require("./transport.js").createTransport, createUi: require("./ui.js").createUi, style: require("./style.js").style, iconDir: require("./style.js").iconDir, log: alcLog})`. A global set in configure() is INVISIBLE to createView (separate scope); require() is the shared handle.
- panel.js: the Harmony ADAPTER over panel_core.js (name, findMainWindow, context, jump; contextHtml/onLink/todoFrame come in #4523). A test (tests/test_alchemy_panel_shared.py) fails if the adapter lacks a hook the core calls.
- harmony/shelf/install.py: call `ship_qtscript(dest)` (alc.plugins.alchemy.tools.panel) after copying the package, as the Storyboard Pro installer does. Harmony loads packages at launch: restart after installing.
- The logger must be self-contained (no module-level variable): a timer callback calling a logger that reads one fails silently.
- Write JS with the Edit/Write tools, not shell heredocs (they mangle backslash escapes); keep every QtScript file ASCII.
Done = Harmony shows an "Alchemy" tab in its own tab group (and the dock fallback) with the live panel connected to :20266, the log (%TEMP%) shows `tab mounted, connected=true`.
#4523Harmony panel: scene / frame / node context, identity and tag (the Harmony adapter's own half)New featureB-23Harmony PanelHarmony Panelclaude
·Toon Boom Harmony·a day ago
The Harmony adapter's own half. In Harmony the unit is the SCENE (one .xstage = one Alchemy shot, as dev/harmony_feasibility.md says), not a panel: the Storyboard Pro "selected panel" becomes the CURRENT FRAME and the SELECTED NODE.
- context(): {project: scene.currentProjectPath(), window: <main window exact title>, frame: frame.current(), node: selection.selectedNode(0) if any}. The bridge resolves the Alchemy identity from the project path (PathObject), exactly as for Storyboard Pro: nothing Harmony-specific is needed there.
- contextHtml(): the scene's identity card, the current frame and the selected node, and a Tag / Untag link. WHERE the tag lives is decided in the spike (#4520): a scene/node metadata API if Harmony has one, else a JSON sidecar next to the .xstage (JSON, not a binary sidecar).
- todoFrame(): the current frame, so an Art-phase todo is pinned to where it was written; jump(frame): frame.setCurrent(frame).
- Host routes on the Harmony bridge (the Storyboard Pro ones are /panel and /select_panel): the current frame/node as the panel last reported it, and "select this node / go to this frame" from the Alchemy side. Use bridge.js_literal() to build the command safely; unit-test the queued JS.
- The Art todos, phase cards (recipes resolve for the OPEN task: Recipe.resolve("harmony", phase, task)), Request pill, menus and tabs already come from the shared panel: verify them in Harmony, do not rebuild them. The Request screengrab needs the exact main-window title from the adapter (a short substring can match several windows).
Verified live through POST /exec before asking anyone to look; the user is only needed for what must be SEEN.
#4524Harmony: start the bridge automatically when Alchemy opens a .xstage (same commit as #4521)ChoreB-23Harmony PanelHarmony Panelclaude
·Core·Toon Boom Harmony·a day ago
Do in the SAME session and the SAME commit as #4521 (`Feat #4521 #4524: ...`): it is one small branch.
Mirror the `.sboard` branch in alc/path/path.py (alc_start) for `.xstage`: ensure the Harmony bridge is running (alc.plugins.harmony.bridge.launch.ensure_running) before Harmony loads the panel, so the poller finds it listening on first try. Idempotent, detached, restarts a stale bridge: the shared Launcher already does all three. Wrap it in the same try/except-and-log as the other branches; test it with the path hook (a monkeypatched ensure_running is enough).
#4525I should be able to add software to the softwafre menu in the cookbook right from the dropdown menu and have it register in the tasks / software configsNew featureAdd Softwareuser
·App · cookbook·Alchemy·a day ago
#4529[cookbook] [storyboard_pro] [srb] Implement the Rename Layers buttonNew featurestoryboard_proRename Layersstub
·Cookbook · storyboard_pro · shelves · srb·Storyboard Pro·11 hours ago
Open a Dialog that allows me to rename layers on the current scene in storyboard pro. the field for renaming should be connected to our assets list so that a user can type and filter by asset name in order to rename the current layer with that of an asset in the show. If the artist types the name of an asset not currently in the show we should display a "create asset" button to allow them to create the asset. It is not necessary for them to create an asset, but it is an option.
#4530when i drag more than one file into Source Files or Render Files nothing happens - we need to support this - and robably have some kind of progress barat the bottom of the corresponding widget telling us the files are copying.Bug fixuser
·Cookbook · alchemy · context-menus · files·Alchemy·12 hours ago
#4531I want to be able to drag drop files from alchemy's file widgets into other applications like i can with windows explorerNew featureFile Drag-Outuser
·Cookbook · alchemy·Alchemy·12 hours ago
#4532Panel phase bar: only the canonical phases are chips; other alchemy-shelf buttons (Version Up) list under the Alchemy tabNew featureB-23Harmony PanelStoryboard Pro Panelclaude
·Alchemy·a day ago
The permanent phase bar is built from EVERY step of the host's `alchemy` shelf. That was exactly Build/Render/Review/Publish for Storyboard Pro, but Harmony's alchemy shelf (cookbook harmony/shelves/alchemy) also has "Version Up", which would become a fifth "phase" chip. A phase is one of the canonical production phases, not any shelf button.
- Shared UI (alc/plugins/alchemy/tools/panel/qtscript/ui.js, setMenus): the phase bar shows only steps whose name is a canonical phase (Build, Art, Render, Review, Publish; the built-in Art chip stays right after Build). Every OTHER step of the alchemy shelf (Version Up) is a plain button listed under the ALCHEMY tab, where every shelf's buttons already live.
- Keep the canonical list in one place the bridge payload can carry (e.g. a `phases` array on the phase recipe in menus.load_menus) so panel_core / ui.js do not duplicate the list; add a test with a shelf that has a non-phase step.
- Storyboard Pro must look exactly as before (its alchemy shelf has only phases): verify live.
Launches the transscritpt editor tool, a tool that allows us to create multiple translation tracks and edit them / preview them on the movie.
#4534Subtitle track set: German spine + English Automated + English Final, JSON storeNew featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Ontology · models·Alchemy·a day ago
A per-version TrackSet of SubtitleTracks (language, label, origin: aws|ollama|manual, cues). German .srt is the SPINE (1317 cards on the first film); English (Automated) is imported from the existing .en.srt; English (Final) starts BLANK and is the track the writer edits. IMPORTANT: de and en cue counts differ (1317 vs 1264) and timings differ slightly, so tracks are NOT index-aligned - each non-spine cue is linked to the spine card by max time overlap (store the link, let it be overridden). Stored as JSON beside the movie (data-in-json); .srt stays the export format (UTF-8 BOM, Resolve-safe via existing write_srt). Importer reads <cut>.<lang>.srt via read_srt. Tests with the real files: E:/Alchemy/gsm/str/VERSIONS/source/shots/FLM/SEQ/edt/default/tom/000.000/high/rough_cut_3_10_26_26_for_subs.{de,en}.srt
#4535Subtitles editor: movie + horizontally scrolling track lanes synced to playbackNew featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·App · transcript·Alchemy·a day ago
New /subtitles tool opened from the shelf button (?path=<version>). Layout = a video editor's timeline: the movie on top, and below it stacked LANES - German, English (Automated), English (Final) - each card drawn as a clip (width = duration, text inside). A fixed playhead sits at a set position; as the movie plays the lanes scroll horizontally under it (and scrubbing/dragging the lanes seeks the movie), so you always see the text around what you are watching. Jump to any point in the film by seeking or clicking; zoom in/out on the time axis; the card under the playhead is highlighted in all lanes, and the active English (Final) card is shown as the subtitle overlay on the video. This ticket = playback-synced READ view (lanes + player + zoom + seek); editing is #4536. Virtualize: only render clips in/near the visible window (1317 cards). Source mp4 is 2.7GB - drive from the version's .proxy/.preview mp4 or range serving, never the master.
#4536Subtitles editor: edit cards on the lanes (English Final) + card listNew featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·App · transcript·Alchemy·a day ago
Editing on top of the #4535 lanes. English (Final) starts blank: click/select a card in the Final lane (aligned under the German card at the playhead) and type the exact translation inline; autosave to the TrackSet JSON; 'copy Automated into Final' as a starting point; drag clip edges to retime, split/merge, add a card where none exists. German and Automated lanes are read-only references. A collapsible card list (German | Automated | Final, virtualized, filter 'not yet final') is a secondary jump-to/overview aid, click = seek. Per-card status (blank/edited). Export English Final to .srt (Resolve-safe) from the tool.
#4537Subtitles: add a track in another language (translate from a chosen track, local or AWS)New featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Alchemy·a day ago
Op + 'Add track' button: translate an existing track into language X on the source card timing (Transcript.translated_cards), engine chosen explicitly per run (local Ollama vs AWS - confidential cuts), re-translate a single card. Never overwrites a Final track. Not needed for the German/English Automated/English Final first use - that one imports existing srt files.
#4538Subtitles: import and export the writer's translation spreadsheet; one Final track per language (English, French)New featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Alchemy·a day ago
Import: the writer's trilingual .xlsx (German | English (approved) | French) is matched to the cut's German by word stream (not by position or time - it is a different cut), filling English (Final) and a new French (Final) track only where the match is easy; split rows join, unmatched/partial/spanning rows are reported not guessed, existing Final text is never overwritten. Export: one-click .xlsx (# | Start | End | German | each Final track) in the same shape so it round-trips. Real run: 1184/1325 rows matched, 1162 English + 1172 French cards filled.
#4539Subtitles editor: audio waveform lane (reusable waveform peaks service)New featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Alchemy·a day ago
Waveform lane above the tracks, scrolling under the playhead, with the German card start/end lines drawn over it so audio-vs-transcript drift is visible. alc/services/media/waveform.py computes min/max peaks (100/s) from the cut's audio (the .wav beside the movie, else extracted with ffmpeg), cached as <stem>.waveform.json; any tool can reuse it. No waveform existed before.
#4540Subtitles editor: move cards in time - select (click / ctrl / drag-box) on a track and the linked cards on every other track followNew featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Alchemy·a day ago
A move icon at the far right of each lane label (radio, like the eye). In move mode: click / ctrl-click / drag-box to select cards on that track, drag them (or arrow keys: 1 frame, shift = 1 s) to shift them in time; the cards linked to the same German card on every other track move by the same amount (German is the spine - move it and everything follows). Live preview while dragging, frame snapping, undo (Ctrl+Z). Why: AWS mistimed a stretch where background voices confused it; the translation is right but the cards sit in the wrong place against the audio.
#4541Subtitles editor: lock/unlock per track - edit the in/out points of the transcription (German, Automated) cardsNew featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Alchemy·a day ago
A lock icon on every lane label. Unlocked tracks show the edge handles on their cards and can be retimed by dragging the in/out points; locked tracks cannot be retimed or put in move mode. German and English (Automated) start LOCKED (transcription output), the Final tracks start unlocked; the choice is remembered per cut. The server accepts time edits on any track; TEXT edits stay Final-only. Why: the AWS timing is wrong in places and those transcription cards need correcting too.
#4542Subtitles editor: edits land on the wrong card when cards overlap, and save state is invisibleBug fixB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Alchemy·a day ago
BUG: card lookup assumed cards never overlap (binary search on END times). Once a card is retimed over its neighbour (25.09-31.58 over 30.67-31.67) the card panel/overlay/live highlight pick the wrong card, so typing lands on a different card than the one you are looking at. FIX: look cards up by START with a max-duration margin and pick the latest-started card covering the playhead. ALSO: a visible save state next to the text box (Unsaved / Saving / Saved / NOT SAVED + automatic retry, never silent), unload warning while unsaved, and a rolling server-side backup of the tracks file (at most every 2 min) so a bad operation can be undone from disk.
#4543Subtitles editor: export ALL Final tracks (English and French) as .srt in one clickNew featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Alchemy·a day ago
The Export button wrote only the track being edited. German timing is corrected by hand, so every Final track (English Final, French Final...) needs exporting with its corrected timing: one click writes <movie>.<lang>.final.srt for each Final track (UTF-8 BOM, Resolve-safe, blank cards omitted) and reports what it wrote.
#4544Subtitles editor: Split at the text cursor (only enabled with the cursor inside the sentence)New featureB-24subtitle-translation-editorTranslation TracksclaudeEditing
·Alchemy·a day ago
Split used the playhead time and ignored where you are in the sentence. Now: Split is enabled ONLY when the cursor sits inside the card's text (not at either end, text not empty). It splits there: everything left of the cursor stays on the original card, everything right becomes the new card. The split time follows the cursor (proportional to its position in the sentence, frame-snapped) and can then be retimed by dragging the edges. Alt+S as a shortcut.
#4545Subtitles editor: Shift+drag moves a card and its linked cards; Shift+drag an edge retimes in/out on all tracks; no more padlocksNew featureB-24subtitle-translation-editorTranslation Tracksclaude
·16 hours ago
Shift+drag a card moves it and every linked card on every track. Shift+drag a card's in/out edge moves that edge on every linked card. German and Automated are always editable for timing (padlocks removed) because the transcript's cut points are often wrong.
#4549Subtitles editor: Shift+drag an in/out edge puts that edge at the SAME time on every linked card (snap to one audio location)New featureB-24subtitle-translation-editorTranslation Tracksclaude
·16 hours ago
Shift-adjusting an in or out point sets the same edge of every linked card to one absolute time, so cuts line up on the same waveform peak. Previously they moved by the same delta and kept their offsets.
#4550Subtitles editor: Shift+Merge joins the linked cards on every track; hover highlights what will merge; Ctrl+Z undoes mergesNew featureB-24subtitle-translation-editorTranslation Tracksclaude
·16 hours ago
Merge: hover highlights the next card; Shift+hover highlights the next linked card on every track; Shift+click merges German, Automated and every Final with their next cards. Ctrl+Z undoes any merge.
#4559Subtitles editor: click a card to edit its text on any track (no jump); drag the waveform's heightNew featureB-24subtitle-translation-editorTranslation Tracksclaude
·16 hours ago
Clicking a card selects it for editing on ANY track (German, Automated, Finals) without moving the playhead; text saves to that track. Split/merge/delete stay on the Final tracks. The waveform lane's height is draggable (grip under it, remembered).
#4560Subtitles editor: marks on the timeline with snapping, and a Split dialog that splits every ticked track at the playhead (pick the word per track)New featureB-24subtitle-translation-editorTranslation Tracksclaude
·16 hours ago
Mark (M) flags the playhead; marks show through the lanes and waveform, cards snap to them when dragged, [ and ] jump between them. Split... opens a dialog with the card under the playhead on every track, a checkbox each (all ticked) and clickable words to pick where the next card starts; OK splits all ticked tracks at the playhead. Ctrl+Z undoes it.
#4561Subtitles editor: Merge works on every track; Premiere-style marker icon above the timelineNew featureB-24subtitle-translation-editorTranslation Tracksclaude
·16 hours ago
Merge (and Shift+Merge) works on the selected card of any track, including German and Automated. Marks show a Premiere-style marker shield in the ruler; the line on the waveform stays, in the same green.
#4562Subtitles editor - Delete works on German and English Automated cards tooAdjust existingB-24subtitle-translation-editorTranslation Tracksclaude
·13 hours ago
Delete removes the selected card on any track (German, English Automated, Final). Deleting a German card re-links the cards that sat under it to the nearest German card. Ctrl+Z restores.
#4563Ingest: link an existing folder as an ingest source (no copy, no writes to it)New featureB-25Linked Folder IngestLinked Folder Ingestclaude
·App · ingest·9 hours ago
Next to '+ Ingest Folder' in the Ingest batches rail, add 'Link Folder': pick an existing absolute path (e.g. E:\SketchToAnimate\MEGA\incoming) and register an IngestLink (id slug, label, root path, created, last_scan, scan_state) persisted in the project IO/ingest tree (e.g. _links.json). A link shows in the rail with a link icon alongside dated batches. The linked source folder is NEVER modified, renamed or written into (no .thumbs, no .ingest.json inside it) - all derived data (folder indexes, thumbnails, tag manifest) lives in a per-link cache dir under the project ingest tree keyed by link id. 'Unlink' removes the registration + cache only, never the source. Reject paths that do not exist / are not directories. Do NOT read files via AlcFile (it renames on load).
#4564Ingest: extract thumbnail generation into a shared media service (add video, collision-proof cache names)New featureB-25Linked Folder IngestLinked Folder Ingestclaude
·Service · media·9 hours ago
Move _make_thumb/_make_exr_thumb/_ensure_thumb out of app/tools/ingest/ingest.py into alc/services/media/thumbs.py: make_thumb(src, dst) -> ThumbResult(ok, reason). Keep existing support (raster via PIL w/ exif transpose, PSD via psd_tools, PDF page 1 via alc.file.core.render_pdf_page, EXR via oiiotool) and ADD video (mp4/mov/mkv: first usable frame via ffmpeg). Unsupported/failed returns a reason string instead of a bare False so callers can record it. Cache filename = stable hash of (source relative path) rather than bare stem - the old '.thumbs/{stem}.png' collides when foo.psd and foo.mp4 share a stem. Staleness check by source mtime stays. Existing Ingest batch routes (/thumb, workspace) switch to the service with no behavior change.
#4565Thumbnail job: generate thumbnails for the units of one folder, as listed in the manifestNew featureB-25Linked Folder IngestLinked Folder Ingestclaude
·Service · media·9 hours ago
Replace the earlier 'scan one folder' idea (the manifest ticket now owns scanning). A worker-runnable entrypoint (python -m alc.services.ingest.folder_thumbs --link <id> --folder <rel>) reads that folder's units from the manifest and generates ONE thumbnail per unit via the shared thumbs service: still image/psd/pdf/exr from the file, image_sequence from its representative frame (middle frame, falls back to neighbors), video from a frame via ffmpeg, mesh via a simple render if feasible else type icon. Writes thumbs into the link cache and stamps each unit in a per-folder thumbs status file (thumb path / thumb_error / source mtime). Idempotent: skips units whose source mtime is unchanged. One bad unit never aborts the folder. Headless on a Deadline Worker, no app running.
#4566Ingest: plan and submit thumbnail jobs from the manifest - one Deadline job per folderNew featureB-25Linked Folder IngestLinked Folder Ingestclaude
·Service · deadline·9 hours ago
From the manifest, a planner decides what needs thumbnails (units without a current thumb, only thumbable kinds) and shows the count + cost up front. Submit to Deadline using DeadlineService.submit_python_job: ONE JOB PER FOLDER that has pending units, each running the folder thumbnail entrypoint for that folder. Deadline is the queue manager - no custom queue/worker code. Empty/up-to-date folders get no job. Record job ids on the link (per folder). Include a dependent completion job (or poll) that stamps the link thumbnailed. Planner is a small swappable function so a different strategy (e.g. split huge folders into several jobs by unit chunk, batch tiny folders together) can be chosen after looking at real manifests.
#4567Ingest: scan status and progress on a linked folder (queued / scanning / done, folders N of M, Rescan)New featureB-25Linked Folder IngestLinked Folder Ingestclaude
·App · ingest·9 hours ago
Linked folder row in the rail shows scan_state (never scanned / queued / scanning / done / partial / failed), 'folders done of total' progress derived from the folder indexes present vs folders.json, last-scan time, and the Deadline job id with a way to open it. A 'Scan' / 'Rescan' button submits the fan-out. Failed folders are listed with their error and can be retried individually. Updates without a page reload while a scan runs (htmx poll).
--- AMENDED 2026-10-27 ---
Progress is derived from the manifest: units with a current thumb vs units needing one, per folder; plus the manifest scan itself (not scanned / scanning / done). Per-folder Deadline job state shown for folders still pending.
#4568Ingest: visual browser for a linked folder - 'All files' gallery across the whole treeNew featureB-25Linked Folder IngestLinked Folder Ingestclaude
·App · ingest·9 hours ago
Opening a link shows what is in the tree at a glance, reading only the cached folder indexes + thumbs: (1) 'All files' flat gallery of every indexed file as thumbnail tiles with name and relative folder path, (2) folder tree rail with per-folder counts, click to scope, (3) filters by kind (image / video / psd / pdf / project-file / other) and name search, (4) sort by mtime / size / name, (5) reuses the existing thumbnail-size slider, non-thumbable files get the type icon fallback. Click selects; double-click opens; right-click 'View in Explorer'. Must stay responsive at 10k+ entries (paged/virtualized, thumbs lazy-loaded).
--- AMENDED 2026-10-27 ---
Tiles are UNITS from the manifest: an image sequence is one tile (name with hashes, frame range, frame count), a video one tile, a mesh one tile. Kind filter includes image_sequence, video, mesh. The gallery is browsable from the manifest alone the moment the manifest scan finishes - units without a thumb yet show the type icon and fill in as thumbnail jobs complete.
#4569Ingest: incremental rescan of a linked folder (only changed folders)New featureB-25Linked Folder IngestLinked Folder Ingestclaude
·App · ingest·9 hours ago
Rescan walks directories, compares each folder's mtime and child count/size/mtime against its stored index, and submits tasks ONLY for new or changed folders. Entries gone from disk are dropped from the index and their cached thumbs deleted; new subfolders get picked up; removed folders disappear from the tree. 'Force full rescan' option ignores the comparison. Shared folders like MEGA/incoming change under us, so this must be cheap enough to run routinely.
--- AMENDED 2026-10-27 ---
Rescan = re-run the fast manifest scan (cheap), diff against the previous manifest per folder, and mark only changed/new units as needing thumbnails; the planner then submits jobs only for folders with pending units. Vanished units are dropped along with their thumbs. The fast manifest scan may be run on a schedule/on open.
#4570Ingest: tag and commit files straight from a linked folderNew featureB-25Linked Folder IngestLinked Folder Ingestclaude
·App · ingest·9 hours ago
Selecting files in the linked-folder gallery and tagging them (entity/parent/child/task/tag) uses the existing tagging model (IngestFile tokens -> resolved PathObject destination). The tag manifest is stored in the link cache, never in the source folder. Source path is the file's real location in place (no pre-copy). Commit copies the tagged files into the pipeline via the existing IngestManifest.commit() (create_empty_version + copy2); the linked source is untouched. Already-committed files show a 'copied' badge so repeat visits do not re-tag them.
#4571Ingest: fast recursive manifest scan of a linked folder (files grouped into units: image sequences, videos, meshes, images)New featureB-25Linked Folder IngestLinked Folder Ingestclaude
·Service · media·9 hours ago
A very fast, thumbnail-free recursive scan (os.scandir only, no file reads, no per-file stat beyond what scandir gives free, run locally - no Deadline needed) of the linked tree that writes ONE manifest JSON to the link cache. The manifest is organized by folder, and inside each folder by UNIT, not by file: image_sequence (head/tail/padding/frame range/frame count/ext, reusing alc/file/sequences.py detection - a 3000-frame render is one unit), video (ext, size, mtime), mesh (obj/fbx/glb/gltf/abc/usd*/stl/blend, size), image (single still), psd/pdf, project-file (e.g. .tvpp, .aep, .kra), audio, archive, other. Each unit records kind, name, rel path, size (sum for sequences), newest mtime, ext(s), and a thumbable flag + the representative file a thumbnail would be made from (e.g. middle frame of a sequence). Folder entries record subfolder list, unit counts per kind, total bytes. Top-of-manifest summary: totals per kind and the count of thumbnails that WOULD be generated, so we see '100 sequences, not 10000 stills' before spending anything. Groupings are decided by name/extension heuristics only. Re-running overwrites atomically.
#4581[storyboard_pro] [srb] Apply the rename.json layer plan on export: drop ignored layers, merge grouped layersNew featureB-26Storyboard Pro exportsLayer Grouping Exportclaude
·5 hours ago
One shared helper in alc/plugins/storyboard_pro that applies a scene's rename.json plan to an export, so every exporter behaves the same and the source .sboard is never changed. Ignored layers: excluded. Grouped layers: composited into ONE output layer named after the group, in stack order, with each layer's opacity (for PNG exports composite with PIL/layer_doc.compose per panel; for Harmony exportToHarmony spike whether hiding a layer excludes it from an original-scene export, and how to merge a group - e.g. post-process the exported scene or export the group as one rendered layer). Unit-test the plan -> per-layer instructions mapping; verify on shot_04 (BG group).
--- AMENDED 2026-10-27 ---
PLAN (2026-10-27, Tom): add an 'Export hidden layers' flag, true/false, an artist preference. Stored per scene in rename.json as scenes.<scene>.settings.export_hidden_layers (default false), shown as a checkbox in the Rename Layers dialog and autosaved like everything else. 'Hidden' = the layer's visibility is off on a panel; evaluated per panel. false = hidden layers are left out like ignored ones; true = they are exported. Ignore always wins over the flag. The shared plan helper resolves flag + ignored + groups into one per-panel list of output layers that every exporter (Harmony, MagiCam, bitmap) consumes, so the three cannot disagree. Spike: does exportToHarmony / setExportOneImagePerLayer already skip hidden layers? If so false is free; if not, filter after export.
#4582[storyboard_pro] [srb] MagiCam export: write a TVPaint clip_export.json-compatible folder from a sceneNew featureB-26Storyboard Pro exportsMagiCam Exportclaude
·5 hours ago
New edt op + srb shelf step that exports the open scene as a MagiCam-readable folder: <out>/<clip>/clip_export.json (version 5.1 / TVPaint 12 schema: project.camera width/height/framerate; clip name, image-count, markin/markout, camera points+positions per frame, layers[] with name/position/opacity/start/end/blending-mode/visible and link[] = {instance-index: first frame (0-based), instance-name, file '<Clip>_<nnn>.<Layer>/<Layer>.<nnnn>.png', images: frames the drawing is exposed on}) and one transparent PNG per layer per panel (solo the layer, setUseCurrentPanel + setTransparentBG - the technique the Rename Layers preview used; skip links where the layer is empty on that panel). Layer names/groups/ignored come from rename.json via the shared plan helper. Done = magicam.tvpaint.load_clip() validates the output and mb.import_multicam_rig() builds the rig from a shot_04 export.
#4583[storyboard_pro] Spike: read scene camera keyframes through the scripting API (for the MagiCam export)ChoreB-26Storyboard Pro exportsMagiCam Exportclaude
·5 hours ago
Prove we can read the animated scene camera per panel/frame from the host (MotionManager.linkedCameraFunction -> FunctionManager keyframes: x, y, angle, scale, size) and map it to clip_export.json camera.points/positions (frame, x, y, angle, scale, sizeX, sizeY; see magicam/tvpaint/models.py CameraKeyframe). Use a shot with camera moves; record the coordinate-system mapping (SBPro camera units/origin vs TVPaint pixels) and any gaps in a short note. Output: a working JS snippet + mapping notes the MagiCam export ticket builds on.
#4584Access model: super admin / admin / manager / artist permission matrix + can() helperNew featureB-27accounts-roles-2026-10-27Role Permissionsclaude
·Service · identity·5 hours ago
New alc/services/access.py: ROLES = super_admin > admin > manager > artist. can(identity, action, company_id, project_id=None), visible_project_ids(identity, company_id), visible_task filter. Admin = every project in their company. Manager = projects they are granted (people.json) at manager level. Artist = granted projects, and only tasks assigned to them. Super admin = platform flag, sees every company. Single source of truth every route/template calls; unit-tested matrix.
#4585WorkOS roles: add manager + artist, super-admin flag from control planeNew featureB-27accounts-roles-2026-10-27Manager Rolesclaude
·Service · identity·5 hours ago
Control plane validates role slugs (admin/manager/artist; legacy 'member' reads as artist), org_invite/set_role accept them. /me returns is_super_admin from an allowlist (env or DynamoDB), NOT a WorkOS org role, so it spans companies. Tom creates the manager/artist roles in the WorkOS dashboard (Roles tab) - ticket includes the exact steps. Redeploy via deploy.ps1.
#4586Invite with scope: whole company or specific project(s)New featureB-27accounts-roles-2026-10-27Scoped Invitesclaude
·App · routes·5 hours ago
Invite form gains Role (admin/manager/artist) and Scope (all my company's projects | selected projects). Invite writes a PENDING project grant keyed by email; applied to people.json when the invitee first signs in / syncs. Admins invite anywhere; managers may invite only to projects they manage. Accepting the email invite then lands the person on only their projects.
Auth gate and project switcher/list use visible_project_ids so manager/artist only see and can open granted projects; direct URLs to other projects bounce to their first allowed project (not '/'). Admin sees all in company. People levels are currently stored but unenforced.
#4588Enforce task visibility for artists (assigned tasks only) + my tasks landingNew featureB-27accounts-roles-2026-10-27Assigned Task Viewclaude
·App · routes·5 hours ago
Artists see only tasks where they are in TaskManifest.assignees (resolved via Person.aliases) across tracker, shots, and assets views; direct task URLs to unassigned tasks are refused; artists land on My Tasks. Managers see every task in their projects.
#4593[storyboard_pro] [srb] Open in Blender: build the MagiCam rig headlessly from the exported clip_export.json, then open the .blendNew featureMagiCam Exportclaude
·2 hours ago
Add an srb shelf step (python, cookbook storyboard_pro/shelves/srb/OpenInBlender.py) that takes the latest <render>/magicam/<clip>/clip_export.json written by Export to MagiCam, builds the timeline/rig headlessly in Blender 5.1 with magicam-blender (magicam.blender.import_multicam_rig), saves <clip>_magicam.blend beside it and opens it in Blender. This is how we prove the MagiCam export imports. Errors surface (no clip export yet, magicam-blender missing, build failed with Blender's output).
#4594[magicam] Variable image-plane count: build the rig with one plane per layer (clone plane + control bone beyond the bundled 10, capped)New featureB-28MagiCam variable planesMagiCam Variable Planesclaude
·2 hours ago
import_multicam_rig counts the clip's layers and, past the rig's bundled 10 planes, creates image_plane_NN by cloning the last plane (object + material, see image_plane.py copy helper) AND its control bone image_plane_NN_ctrl (shape, constraints, parenting to the dolly), then keys visibility like the first 10. Hard cap (about 24) with a clear error above it. Idempotent on re-import. Done = importing the shot_04 export builds 11 planes with the BG plane working and its control bone moving it; tests cover 10, 11 and over-cap.
#4596[magicam] Unreal parity: variable plane count in magicam_rig_spec.json so the UE rig builds N planes like BlenderNew featureB-28MagiCam variable planesMagiCam Variable Planesclaude
·2 hours ago
Make the plane count a variable in magicam_rig_spec.json (a base plane + control definition repeated N times, with the cap) instead of 10 hard-coded entries; the Blender builder, the UE rig builder (#3984) and the parity validator (#3987) all read it. Done = the same N-layer clip builds N planes and N controls in Blender and Unreal and the parity validator is green.
#4598Move accounts control plane to the cglumberjack AWS account (old personal account suspended)ChoreB-27accounts-roles-2026-10-27claude
·Service · identity·2 hours ago
The alchemy-accounts Lambda, DynamoDB table alchemy_accounts and function URL live in the old personal AWS account (651341987681), which now answers 'This account is suspended', so nobody can sign in. Recreate in the cglumberjack account: DynamoDB table (pk string, TTL on expires), IAM role, Lambda + public function URL (needs BOTH lambda:InvokeFunctionUrl and plain lambda:InvokeFunction policies), env WORKOS_API_KEY / WORKOS_CLIENT_ID / SUPER_ADMINS, deploy via deploy.ps1. Then: register the new URL's /auth/callback in WorkOS Applications > Redirects, change the ACCOUNTS_URL default in alc/services/identity.py (or set ALC_ACCOUNTS_URL), and re-create the org#<id> -> company_code rows (cgl, zci, gossman, sleeptight) via POST /orgs/{id}/meta. Users, orgs and roles live in WorkOS and are unaffected.
#4603Sync manifest schema: delta + cumulative manifests with lineage and deletionsDesignB-31Sync A: FoundationsCloud Publishclaude
·37 minutes ago
Define the versioned (schema_version) manifest format the sync system is built on. Two tiers, learned from the JHCS sync engine: (1) an immutable per-event DELTA manifest {event_id, task, user, from_user_version, from_published_version, extensions, timestamp, files{rel_path: content_hash}, deleted[]} and (2) a CUMULATIVE manifest merged from every delta, with a history snapshot written per event so any past state can be reconstructed. Must add what JHCS lacked: explicit deletions, frame sequences stored as one range entry (pattern, first, last, tree hash) not thousands of rows, paths relative to the project root with the root named in the manifest (no machine-specific prefix), size in bytes per file, and the producing site id. Event ids are sortable UTC timestamps plus a short hash. Deliverable: a doc in dev/ with the schema and worked examples, plus pydantic models in alc/services/sync/; no transport code. Reference: cglumberjack/cgl/plugins/sync/create.py and sync_manager.py.
#98Show a live filmstrip while sliding a clip in the Visual WidgetNew featureApp · transcriptclaudeScript
·App · transcript·Alchemy·3 days ago
Stretch goal split off from #96. While a clip is being slid on the Visual Widget card (the AlcSlide grab-to-slide drag), show an approximation of the frames the clip will cover at its new position — a small filmstrip / frame thumbnails updating live as the delta changes, so the artist can eyeball WHERE the clip lands, not just how many frames it moved. Today the slide shows green edge bars + a +/- frame delta badge (AlcSlide, app/static/js/slide.js); this adds the visual frame preview on top.
#831i'd like to be able to edit the duration (in frames) directly on the beat row.New featureTom
·App · transcript·Alchemy·3 months ago
#832something is causing repeating "Apply Framework" buttons on the transcript editor.Bug fixTomTranscript
·App · transcript·Alchemy·3 months ago
#833Headless farm jobs read the WRONG project's registry — every mdl build on loj crashes in CreateScaleCubeBug fixclaudeModel
·Blender·3 months ago
A Deadline mdl job for an asset in project 'loj' hydrates and reads project 'jlt'. Every asset lookup then misses, asset_details comes back None, and the build dies on the first step that asks the PathObject anything.
FROM THE FARM LOG (asset is E:/Alchemy/cgl/loj/VERSIONS/source/assets/env/homestead/mdl/...):
ProjectRegistry: Auto-hydrating default store instance 'jlt' from config.
ProjectRegistry: Cache miss. Hydrating master registries for 'cgl:jlt'...
Master Scene Registry missing on disk. Attempting script.json recovery pass...
Crash in step 'CreateScaleCube': 'NoneType' object has no attribute 'height'
CreateScaleCube.py:42 -> height = po.height
alc/path/path_object.py:1026 -> return self.asset_details.height or 600
AttributeError: 'NoneType' object has no attribute 'height'
Then: Skipping (prior failure) x6, build failed for blender/mdl, Deadline task failed.
ROOT CAUSE: ProjectRegistry.model_post_init (alc/services/project.py:459) warms the store from CFG.company.current_project at import time. That default is whatever the workstation last selected. Nothing in the headless cookbook path switches it to the project the JOB was handed, so PROJECT_REGISTRY.current is simply the wrong project for the whole run. It is not specific to env or to height — anything reading asset_details on the farm is reading another project's registry.
FIX AT THE ROOT, NOT THE CALLSITE: the run should bind the registry to the PathObject it was given (the job knows its company and project — they are in the asset path), so 'current' is the job's project for the life of the process. Do NOT patch PathObject.height to tolerate a None asset_details: a missing asset must stay a hard error, and defaulting to 600 would ship silently wrong scale cubes on every job that hits this instead of failing loudly.
SECONDARY, worth deciding while in here: PathObject.height (path_object.py:1016-1028) assumes asset_details exists on all three branches. Once routing is fixed, a genuinely missing asset should raise something that names the asset and project, not an AttributeError on NoneType.
#4302Grounding setup: design camera and 3 human-scale anchorsNew featureB-3Concept-to-Layout A: Prove itConcept to Environmentclaude
·Alchemy·3 days ago
Artist places a design camera in Unreal matching the concept perspective; add human silhouette at 3 depths to concept if no scale reference; matching mannequins/markers in 3D. The 3 feet positions define the ground plane, figure height gives scale per depth, head alignment sanity-checks horizon/camera height. Save as structured data.
#847Image editor: link an EXISTING layer to an asset — right-click the layer, not just the add-linked buttonNew featureclaudeScript
·App · image_editor·Alchemy·3 months ago
Binding a layer to a character/prop is only reachable one way today: the chain-link button in the LAYERS header (toggleEntityPicker, 'Add linked layer'), which CREATES a new layer already bound. There is no way to bind a layer that already exists.
That is the common case. The artist scribbles a shape, names the layer 'jack', and now wants it to BE Jack — but the name is not the binding, so the region never reaches the prompt as an asset and never gets a legend colour. It silently looks done and isn't.
WHAT TO BUILD
1. RIGHT-CLICK A LAYER ROW -> 'Link to asset...' (and 'Unlink' when already bound). The layer row is where the artist is already looking; the header button is for a layer that does not exist yet, which is a different intent.
2. It offers the same picker the header button does — the beat's own cast and props first (one click for Jack and the Coffee Cup), everything else in the project behind a filter. Reuse toggleEntityPicker's list rather than building a second one.
3. It sets entity_type / entity_id on the EXISTING layer's manifest entry (layer_doc already carries both) and back-fills the block's cast the same way add-entity-layer does — binding a layer to Jack on a beat that never listed Jack should tag the beat, exactly as creating a bound layer does.
4. It does NOT touch pixels. Unlike add-entity-layer's non-blank path, this never renders or SAM-cuts the asset's design — the artist already drew the region; linking only says what it is.
5. The row shows its binding: the derived legend colour as a swatch plus the asset name, so a bound layer is distinguishable from an unbound one at a glance. Colour comes from the same region_colors() derivation the legend and prompt use, so it cannot drift.
6. Unlink clears both fields and drops the swatch. Whether it should also untag the beat's cast: decide explicitly and say so — a beat may legitimately still feature Jack after one layer stops being him.
DONE WHEN: right-clicking any layer offers Link to asset; picking a character or prop binds THAT layer without altering its pixels; the row shows the asset and its colour; the generate names that region as the bound asset; and unlink reverses it.
#914PathObject.asset_details should resolve from its OWN project, not the ambient current oneBug fixclaude
·Core·Alchemy·3 months ago
#833 fixed the SYMPTOM properly - a headless run now binds alc.context.scope from the asset path, so PROJECT_REGISTRY.current answers for the job. This is the root fix underneath it: a path-derived lookup should not need an ambient scope AT ALL.
PathObject.asset_details (alc/path/path_object.py:996) reads PROJECT_REGISTRY.current five times - character/prop/dressing/environment/sequence registries. But self already carries company and project. Asking a global which project we are in, while holding a coordinate that says so, is the whole bug class: it can only ever be right by luck, or by someone remembering to bind a scope first.
THE CHANGE IS SMALL. All five reads are inside that one property, and asset_details has NO other callers in the repo - the only references are the three inside PathObject.height. store_for(po) already exists in alc/services/project.py doing exactly this resolution, with a docstring saying that falling back to the active project is the silent cross-project read it exists to prevent. So: asset_details resolves via store_for(self).
DECISION 1 - what happens when the coordinate has no project, or the asset is missing. store_for raises ValueError rather than guessing. asset_details currently returns None for an unrecognised parent, and height then explodes with AttributeError on NoneType - that IS the #833 crash. Recommend: raise an error that NAMES the asset and the project it was looked for in, rather than returning None for callers to trip over. A missing asset should be loud, and in-repo nothing depends on the None.
DECISION 2 - a real inconsistency found while measuring; decide it here rather than leave it. PathObject.height (path_object.py:1016) reads:
chr -> self.asset_details.physical_traits.height_cm or 170
env -> self.asset_details.height or 600
else -> self.asset_details.height or None
The cookbook's CreateScaleCube does: height = po.height, then "if height: build the cube, else: warn Scale not set for asset". For chr and env that else branch is UNREACHABLE - the "or 170" / "or 600" guarantees a truthy value, so an asset with no scale set silently gets a default-sized cube instead of the warning the step was written to give. Either the defaults are right and the cookbook's warning is dead code, or the defaults are wrong and are hiding unset scales. Pick one and say which.
OUT OF SCOPE: the other ~130 PROJECT_REGISTRY.current call sites. Those are legitimately ambient (a request, a DCC session) and #833's scope already grounds them. This ticket covers only lookups derivable from a coordinate the caller already holds.
DONE WHEN: asset_details resolves from self.company/self.project via store_for; a farm job gets the right asset with NO scope bound at all; a missing asset or a projectless coordinate raises something that names what it was looking for; and decision 2 is implemented rather than left ambiguous.
#993mdl turntables render the high-res design GLB instead of the published modelBug fixclaudeModel
·Plugin · blender·Blender·3 months ago
The mdl turntable renders the high-res design GLB, not the cleaned model - on every asset, not just the ones where you can see it.
WHAT HAPPENS
ImportDesignGlb lands the published design mesh in a DSN collection as the hero "hi" asset. CreateProxyModel + TransferUVsToLo build the lo from it. PlaceGeometry then calls blender.edit.hide_collection("DSN") to get the hero out of the way.
But hide_collection (alc/plugins/blender/edit.py:1256) does exactly one thing:
layer_col.hide_viewport = True
That is the outliner EYE icon - viewport visibility. Its own docstring says so. It does not set the collection's hide_render, and it does not exclude the collection from the view layer.
The turntable is a real render, not a viewport playblast: render/mdl/Playblast.py calls blender.create.render_camera(cam, start, end, seq_path). Blender renders honour hide_render and exclude; they ignore hide_viewport. So DSN is in frame for every mdl turntable.
WHY IT LOOKS INTERMITTENT
It is not intermittent. The hero sits coincident with the proxy, so on assets where the two are close the turntable looks correct while actually showing the hero. It only becomes visible where they diverge - the coffeecup, whose sculpted celtic knot and bear-head handles read as obviously dense. Every mdl review to date has been of the high-res.
Consequences worth stating: model reviews have not been reviewing the published model; turntable frame times are paying for the full sculpt; and a proxy that is wrong (bad UV transfer, missing detail, wrong placement) cannot be spotted in review, because the thing hiding it is drawn on top of it.
THE FIX
Keep the eye-icon behaviour - hiding it in the viewport with a way for the artist to toggle it back is deliberate - and ALSO stop it rendering. Set the collection's hide_render alongside layer_col.hide_viewport, or exclude the layer collection outright if the artist never needs it back during a farm run. Prefer fixing hide_collection itself rather than PlaceGeometry's call site: any other step that hides a collection expecting it to be gone has the same latent bug.
Decide and state whether hide_collection should mean "hidden from the artist" or "hidden from everything" - if both are wanted, that is two functions with two names, not one that half-does each.
DONE WHEN: an mdl turntable shows the published proxy only; the DSN hero is absent from rendered frames; the artist can still reveal DSN in the viewport; and re-running the render on the coffeecup produces visibly different geometry from v001.001.
#1007Beat board: a START PREVIS button, with Blender as its first destinationNew featureScript Start PrevisclaudeScript
·App · transcript·Alchemy·3 months ago
The beat board's prep band now reports "3/3 designed - 3/3 modelled - 1/1 rigged - ready for previs". Nothing acts on that. Give it the button it is asking for: START PREVIS, in the board header beside the band.
IT IS A MENU, NOT A BUTTON. There are two destinations already and they are not the same job: Blender (this ticket) and Unreal (its own ticket). Build it as one control with options from the start rather than a Blender button that later grows a sibling, because "which previs" is a real choice the artist makes per board, not a default with an exception.
THE PART THAT IS NOT A BUTTON MOVE - READ THIS FIRST
There is already an Open in Blender on the SCENE page (script_reader.html), and it will not work here. It POSTs to /project/{pid}/scenes/{scene_id}/open-in-blender - scene-scoped - and the template deliberately excludes it for videos: "{% if not is_video %} Scene-only 3D actions - they POST to /scenes/{id}/... and don't apply to a self-homed video (whose work is script -> previs -> cut)". A beat board IS a video. So this needs a BOARD-scoped assemble-and-launch, not a relocated button.
What that means concretely: the scene version assembles from a scene's blocks. The board version must assemble from the video's own declared locations plus the characters and props tagged on its beats - the same registry context the prep band already resolves its slots from (see _prep_band_ctx / build_prep_slots). Reuse the assemble-published-rigs-and-environments machinery and the launch itself; what changes is who is asked for the cast.
GATING - SAY WHY, DO NOT JUST DISABLE
The band already computes the whole chain and states what is outstanding. The button reads that, not its own copy of it. When previs is not possible, the control says which link is missing (a location with no model, a character with no rig) rather than greying out silently. "Ready for previs" is already a computed state - this is its consumer.
Known wrinkle to handle deliberately: the band builds in _page_ctx, not _rows_response, so tagging a new character on a beat does not refresh it until reload. The button will inherit that staleness. Either refresh both together or make the button re-check on click rather than trusting the render - do not let it launch a previs that is missing a rig the page simply had not noticed yet.
OUT OF SCOPE: the Unreal option (its own ticket - leave the menu entry visible-but-unavailable or absent, your call, but say which); changing the scene page's existing button; anything about what happens INSIDE Blender once it opens.
DONE WHEN: a board that reads "ready for previs" offers Start Previs -> Blender; clicking it assembles the board's own locations, characters and props and opens Blender exactly as the scene page does for a scene; a board that is not ready says which link is missing; and the scene page's button is untouched.
#1008Start Previs: Unreal — build the previs (3 of 4 prv steps are scaffold; cameras already built)New featureMagiCam Native UE PluginPrevis Build in UnrealclaudePrevis
·Plugin · unreal·Alchemy·2 months ago
The second destination for Start Previs: build the previs in Unreal instead of Blender.
STATE OF THE GROUND, RE-CHECKED PROPERLY - READ BEFORE ESTIMATING
cookbook/unreal/build/prv/prv.json declares four steps in order:
0 EnsureSequenceLevels - SCAFFOLD (run() only reads po/source_files/render_files, then pass_check)
1 ImportConnectedRigs - SCAFFOLD (same shape)
2 ImportEnvironmentModel - SCAFFOLD (same shape)
3 CreateCamerasFromAmcShots - IMPLEMENTED (~20 statements: resolves base_dir + shots, selects, builds cameras)
So THREE of the four need writing; the camera step - arguably the hardest, and the one that needed the beat-to-shot decision - already exists. Estimate accordingly.
(An earlier version of this ticket said all four were scaffold. That was measured by counting lines between the "Script Logic Start/End" markers, which is not a reliable test: real step logic often lives outside those markers - blender/publish/rig/SaveBlendFileToRender.py is fully implemented and would also read as empty by that method. Re-measured by parsing run() and counting executable statements.)
WHAT TO IMPORT, AND IN WHICH FORMAT
Prefer the GLB. Unreal does not care about the geo count, and the glb is the design model already being published - the reason Blender builds a proxy at all (viewport weight, clean topology for lookdev) simply does not apply here. Note the deliberate asymmetry with #993, where the Blender turntable was fixed to STOP showing the high-res: same asset, opposite correct answer, because the destination is different. Do not "fix" one to match the other.
FBX by default is acceptable for a first pass - ImportEnvironmentModel's docstring already says fbx, and nothing acts on it either way. Ship whichever gets a previs standing up, and STATE which one landed so the next person is not guessing.
SCOPE OF THE FIRST PASS - DECIDE AND SAY
Environment plus connected rigs is the obvious minimum (two of the four steps). Dressing and props are not implemented anywhere and may not be needed to judge cameras and timing, which is what previs is for. Decide explicitly whether the first pass covers them or stops at environment + cast, and say so - "we didn't get that far" should be a stated scope line, not something discovered later by someone wondering where the props went.
DECIDE AND STATE: local or headless, or both. The build can run headless, which makes "start previs" a farm submission rather than a wait with Unreal open on the artist's machine. Those are different products - one returns a job id and a notification, the other a running editor. Pick the default deliberately and say what the other costs.
IF IT RUNS HEADLESS, BIND THE PROJECT. A headless run has no ambient context, so it must bind alc.context.scope from the coordinate it was handed (see #833 / #914) or every registry read answers for whatever project the workstation last selected. That is the exact failure that took out the mdl builds - do not rediscover it.
CAMERAS: a beat board is not an amc edit. Establish what a beat maps to - one beat, one shot is the obvious reading given the board is a strict 1:1 lane - rather than letting the step find nothing and produce a previs with no cameras.
DEPENDS ON: #1009 (create the location's UE project if missing) for any location that has never been to Unreal - there is no project to build into otherwise. And on the Start Previs menu shell from the Blender ticket.
OUT OF SCOPE: the Blender option; MagiCam / camera work beyond placing a camera per beat.
DONE WHEN: Start Previs offers Unreal; choosing it stands up a previs for the board with its environment, rigs and cameras; the run states whether it is local or on the farm and how to watch it; a headless run reads the BOARD's project rather than the config's; and the ticket's own scope line says what the first pass did and did not import.
--- AMENDED 2026-08-13 ---
THIS IS THE SPRINT'S ACCEPTANCE TEST (Tom, 2026-08-13): 'the big test for when this is working is when we can run a Build in Unreal button from the previs menu and assemble the timeline with magiCams.'
Concrete surface he named:
http://127.0.0.1:8080/c/cgl/project/loj/project/loj/transcript/at/videos/theaxe/SEQ/scr?u=tom&v=000.000
- the beat board for video 'theaxe' (project loj). The Previs menu on that page is the entry point (the menu itself shipped: #1007 Start Previs button, #2897 Previs menu with Open Versions + Export Timeline). Blender is the destination that works; Unreal is this ticket.
Moved into the MagiCam Native UE Plugin sprint on that basis - it is the end-to-end proof that the rig, the caddy and the build path are one working thing rather than three parts that each demo alone.
BOUNDARY NOTE, which is a real input to #3982's architecture decision and not a caveat: this ticket is PIPELINE-SIDE, not part of the Fab store plugin. #3982's whole job is to draw that line, and it already says the store build ships with ZERO Alchemy/Python dependency while 'Alchemy Timeline import stays pipeline-side'. Assembling a timeline from a beat board's amc shots is exactly that pipeline-side half. So this exercises the native plugin from the outside - it must drive the rig through whatever public surface the plugin exposes, and if it can't, the plugin's boundary is wrong. That makes it a better acceptance test than an internal one, but it also means the work lands in the cookbook + alc/plugins/unreal, NOT in the MagiCam plugin's Source tree.
Relationship to the rest of the sprint: this can be built against the CURRENT bridge-driven rig (the verbs exist and #345/#346/#3996/#3998/#4004 made them work), so it does NOT have to wait on #3982-#3984. Sequencing it early is arguably better - it proves the assembly path while the native plugin is still being designed, and whatever it needs from the rig becomes a requirement ON #3982's public surface rather than a discovery after the plugin is built.
Also see #1176/#1177 (One camera per beat) - on the SAME theaxe board, previs currently exports 14 MagiCams where ~6 boards are one continuous setup. That directly degrades what this ticket assembles, so the two should be judged together on the same board.
#1024Get the timeline for a given TASK — stop hardcoding svz in the previs buildNew featureScript Start PrevisclaudePrevis
·App · transcript·Blender·3 months ago
The Blender previs build cannot find a beat board's timeline, so a board gets environments and rigs but no MagiCams. The fix is NOT to make boards produce an svz timeline. It is to stop asking for one by name.
WHAT IS HARDCODED TODAY
cookbook/blender/build/prv/ImportStoryvisTimeline.py builds a path by hand:
svz_obj = po.copy(tree="source", task="svz").latest_version_object
filename = f"scene_{po.parent}_timeline.json"
Two assumptions baked into one step: that the timeline lives on the svz task, and that its filename is scene-shaped. Neither is true of a self-homed video, and neither will be true of the next thing that owns a timeline.
THE SCHEMAS HAVE NOT DRIFTED - THIS IS NOT A PORT
Both ends are the same models. The board's TranscriptEdit.to_script_model() builds a real ScriptScene (transcript_edit.py ~2625) and svz.refresh derives its tracks from ScriptScene.get_timeline(). One normalized timeline model, several possible owners - which is exactly the case for resolving by TASK rather than by a hardcoded one.
THE RESOLVER ALREADY HALF EXISTS
ScriptScene.timeline reads the persisted manifest or compiles a fallback (see app/tools/script/timeline.py, "This automatically reads scene_XXX_timeline.json or compiles a fallback"). That IS the behaviour wanted - it is simply welded to a scene's coordinate through ScriptScene.path_object (hardcoded entity_type "shots", task "svz") and timeline_manifest_path.
WHAT TO BUILD
One function: given a COORDINATE, return its timeline. Something like timeline_for(po) that resolves through the coordinate's own task rather than a constant - svz for a scene, scr for a beat board, whatever owns one next - reading the persisted manifest when the task has one and deriving from that task's model when it does not. ImportStoryvisTimeline then asks for "the timeline for this coordinate" and stops constructing paths.
Note ScriptScene already carries _po_override (script.py ~301/350), which is the hook that lets a synthesized scene point at a non-scene coordinate without pretending to be registered in the project script.
DECIDE AND STATE:
* Where the resolver lives. It is model-layer, not step-layer - putting it in the cookbook step means the next consumer writes its own copy, which is how this got hardcoded in the first place.
* What it does when a task owns no timeline: raise naming the task, or return None. Prefer raising - a previs that silently comes up with no cameras is the failure this ticket exists to stop.
* Whether the step keeps the name ImportStoryvisTimeline once it is task-agnostic. "Storyvis" is the svz assumption in the label.
* Staleness: re-running previs after editing beats must reflect the new timing, not a cached manifest. Board durations are the most likely thing to have changed (see #831).
WHY NOT "just write an svz timeline for boards": it would work, and it would put the same assumption one layer deeper - every future timeline owner would have to masquerade as svz to be seen. Resolve by task once and no one has to.
OUT OF SCOPE: the timeline SCHEMA (already shared); MagiCam itself; the Unreal camera path (#1008).
DONE WHEN: the previs build asks for the timeline of the coordinate it was handed and gets it, for a scene AND for a board; a board's previs comes up with MagiCams, image planes and audio; a task with no timeline fails by name rather than producing an empty previs; and a scene's existing previs path is unchanged.
#1178Capture commit SHAs and changed files on the work item when a ticket is marked doneNew featureprovenanceclaude
·App · kanban·3 months ago
ticket_cli.py 'done' and sprint_cli.py 'done' set status/lane/worker via _set_state() but never populate the WorkItem's existing commit_shas and changed_files fields - they are empty on every item on the board. Populate them at done time so each ticket carries a machine-verifiable link to the code that closed it. Default: auto-discover by running 'git log --all --grep=#<n>' to find commits whose subject references the ticket (the alc_do_ticket convention is 'Feat #<n>: ...'), then 'git show --name-only' on each to collect touched paths; write SHAs to commit_shas and the deduped paths to changed_files. Add optional --commit <sha> (repeatable) and --no-git-scan overrides for when the convention was not followed. Never fail the done transition if discovery finds nothing - record what it found, print what it wrote, still mark done.
#1179Record the pull request URL on the work item when a ticket closesNew featureprovenanceclaude
·App · kanban·3 months ago
alc_do_ticket step 4 opens a PR ('gh pr create') and alc_sprint merges chain branches, but neither writes the PR URL back to the WorkItem - so the board knows a ticket is done but not where the review lives. Add a pr_url field to WorkItem and accept --pr <url> on ticket_cli done / sprint_cli done. Where the CLI can discover it cheaply (gh pr view --json url on the current branch), default to that and let --pr override. Surface it on the ticket modal as a link next to the status history.
#1180Provenance report - export the full ticket-to-artifact chain for a date range or sprintNew featureprovenanceclaude
·App · kanban·3 months ago
Given a sprint name, a ticket number, or a date range, emit a report of the complete development chain: ticket number and title, created_at, every status_history transition (with lane, sprint, worker and actor), the commit SHAs and changed files (from #1178), the PR URL (from #1179), and any rendered artifacts produced under the affected PathObject versions. Output as markdown for humans and JSON for machines. This is the artefact that answers 'how long would it take you to build X' with a record instead of an assertion.
#1181Add a 'sprint' command to ticket_cli for assigning existing tickets to a named sprintNew featureprovenanceclaude
·App · kanban·3 months ago
ticket_cli submit accepts --sprint at filing time, but there is no way to set or change the sprint on a ticket that already exists - you currently have to open the board or write a throwaway script against WorkBoard.editing(). Add: 'ticket_cli.py sprint <n> [<n> ...] --name <sprint>' to assign one or more tickets to a named sprint in a single call; '--clear' to unassign; and 'ticket_cli.py sprint --list' to show the sprints currently on the board with their item counts. Use the same WorkBoard.editing() lock as _set_state so it cannot clobber a concurrent write from the live web app, call item.touch() so updated_at moves, and write an architect ledger entry (via: alc_submit_ticket) like the other transitions do. Print one RESULT line per ticket showing number, title and the sprint it landed in.
#1182Add a depends_on field so tickets can declare hard ordering, and have sprint chaining honour itNew featureprovenanceclaude
·App · kanban·3 months ago
Add 'depends_on: list[int]' (ticket numbers) to WorkItem. Set it with --depends-on <n> (repeatable) on ticket_cli submit, and with a 'ticket_cli.py link <n> --depends-on <m> [--remove]' command for existing tickets. Validate on write: the referenced ticket must exist, a ticket cannot depend on itself, and reject a cycle rather than storing it. Surface it in ticket_cli show and on the ticket modal as links to the blocking tickets. Then make sprint_cli list honour it: order items topologically, and mark an item BLOCKED (not ready to hand off) while any ticket it depends on is not yet done - same gate as the existing readiness check. Finally teach alc_sprint that a declared dependency is a HARD constraint: dependent items go in the SAME chain, in dependency order, never split across parallel worktree agents.
#1183Backfill commit SHAs and changed files onto every completed work item already on the boardChoreprovenanceclaude
·App · kanban·3 months ago
One-shot, re-runnable backfill that retro-populates commit_shas and changed_files on existing work items, so the whole board history becomes machine-verifiable rather than only work done from #1178 onward. Reuse #1178's discovery function - do not duplicate the logic. For each item with a number: run 'git log --all --grep=#<n>' across BOTH repos (alchemy and the cookbook at CFG.company.cookbook_dir), collect matching SHAs, then 'git show --name-only' for the touched paths. Rules: (1) --dry-run by default, print what WOULD be written and require an explicit --apply; (2) never overwrite fields that are already populated unless --force; (3) idempotent - re-running produces the same result; (4) match strictly on the '#<n>' token with a word boundary so #24 does not match #241; (5) where a ticket resolves to zero commits, record nothing and count it, do not guess. Print a coverage summary at the end: items scanned, items linked, items with no match, and items skipped as already populated.
#1234Previs imports stack duplicates on re-run, and the accumulated scene lies about asset sizeBug fixAsset Scale Integrityclaude
·Plugin · blender·Blender·3 months ago
Re-running a previs import step in an already-built scene STACKS another copy of everything instead of replacing it. Nothing in the import path checks whether the asset is already there.
MEASURED, on a real session on 2026-08-02: a Blender session that had been worked for a few hours held 224 `jack_cs_*` rig-widget meshes. A clean headless build of the same coordinate holds 28. That is exactly 8x — eight accumulated copies of one character's control set. The same session held two cup-sized meshes from two different mdl publish generations; a clean build holds one.
WHY THIS IS WORSE THAN CLUTTER — IT MAKES THE SCENE LIE ABOUT ITSELF
Measuring the accumulated jack gave a body height of 4.3763 m against a declared 185.4 cm, which reads exactly like a model that ignores its own height. It is not: every individual copy is correct at 1.8540 m, and the 4.3763 is a union bounding box across copies sitting at different Z offsets (note 4.3763 / 1.854 = 2.36, not a clean multiple — the signature of a union, not a mis-scale).
That false reading cost real time and nearly triggered two unnecessary republishes of correctly-built assets, plus a hand-applied 0.8 scale on the homestead to "fit" a character who was never the wrong size. An artist hitting this has no way to tell an accumulated scene from a broken asset.
THE CONTRACT ALREADY EXISTS IN THE CODEBASE
DefaultBoneRig (cookbook/blender/build/rig) is explicitly idempotent and says so in its result: "already has a rig — left untouched". So "re-running a build step must be harmless" is already an established convention here. The prv import steps simply do not honour it.
WHAT TO BUILD
Make the previs import steps idempotent, the same way the rig step is:
* ImportCharacterRigs, ImportProps, ImportEnvironments, ImportDesignModels.
* Re-running against a scene that already contains an asset should REPLACE it or leave it alone — and say which in the step result — never add a second copy.
DECIDE AND STATE:
* Replace or leave-alone. Leave-alone is cheaper and matches DefaultBoneRig; replace is what an artist re-running after a republish actually wants (that is how the two cup generations got in — the cup was republished at a corrected size and the old one stayed). Consider: leave-alone unless the published version differs, then replace and SAY so.
* How an asset is identified as already-present. Collection name is the obvious handle (ENV/{asset}, CHR/{asset}, PRP/{asset}) but a stale copy may have been renamed by Blender's .001 suffixing, which is itself the symptom.
* Whether a scene that is ALREADY accumulated can be repaired in place, or only fixed by rebuilding. A repair pass would be worth having, since sessions like this exist right now.
DONE WHEN: running a previs import step twice in one scene produces the same scene as running it once; a re-run after a republish brings in the NEW version and does not leave the old one behind; and the step result says which of those happened.
#1235Heights entered in inches are stored as feet - the parser reads the first number as feet alwaysBug fixAsset Scale Integrityclaude
·Ontology · models·3 months ago
Entering a height in inches gives you feet. `PhysicalTraits.assign_height_from_imperial` (alc/models/script/models/manifests.py:515-532) parses with:
matches = re.findall(r"(\d+)\s*(?:ft|'|feet)?\s*(\d+)?\s*(?:in|\"|inches)?", imp_str.lower())
feet = int(matches[0][0]); inches = int(matches[0][1]) if matches[0][1] else 0
self.height_cm = round(((feet * 12) + inches) * 2.54, 1)
Every unit suffix is OPTIONAL, so the first number always lands in the FEET group no matter what the user typed after it. Reproduced live:
"4" -> 121.9 (4 feet)
"4in" -> 121.9 the EXPLICIT inches form is also misread
"4\"" -> 121.9
"0ft 4in" -> 10.2 only the two-part form works
So the field silently multiplies any single-value entry by 12, and ignores an explicit "in" / double-quote that says otherwise.
FOUND BY: a coffee cup entered as 4 inches was built at 121.9 cm — a 1.22 m mug in a 1.85 m character's hand. It was only caught because it was visibly absurd in a previs. A prop entered as "8in" would become 8 feet and might not look obviously wrong in a wide shot.
WHAT TO BUILD
Parse the unit the user actually typed:
* a bare number: decide and state what it means, and SHOW that unit in the UI rather than guessing.
* an explicit inches form ("4in", 4", "4 inches") must mean inches.
* the two-part form ("6ft 1in") keeps working — it is what the character dashboard's widget emits and it is correct today.
* reject what cannot be understood rather than silently choosing feet. A height that cannot be parsed should fail loudly; this ticket exists because it failed quietly.
THE ENTRY POINT, for whoever picks this up: scene_prep_band.html:92 (the height input, placeholder "height e.g. 6ft 1in") -> savePrepMeta() posts only kind/slug/height and never height_mode -> POST /scenes/prep/asset/meta (app/tools/script/script_viewer.py:1497) whose signature defaults `height_mode: str = Form("imperial")` -> height_to_cm(raw, "imperial") -> the parser above -> asset.height (script_viewer.py:1541).
Note the JS never sends height_mode at all, so the server-side default decides the unit for every prep-band entry.
RELATED, AND WORTH FIXING TOGETHER OR AT LEAST KNOWING ABOUT: the same `height` field means three different units in three places (separate ticket). Fixing this regex alone still leaves a cup stored correctly in cm and displayed as metres.
DONE WHEN: "4in" stores 10.16 cm; "6ft 1in" still stores 185.4; a bare number stores the unit the UI says it will; and an unparseable height is refused rather than silently read as feet.
#1236The height field means centimetres, metres or feet depending on which code touches itBug fixAsset Scale Integrityclaude
·App · routes·3 months ago
One field, `height`, means three different units depending on which code touches it. All three are live right now.
1. THE PREP BAND writes CENTIMETRES, parsed from an imperial string.
scene_prep_band.html -> POST /scenes/prep/asset/meta (script_viewer.py:1497)
-> height_to_cm(raw, "imperial") -> asset.height = <cm>
2. THE VERSION-SHELF HEIGHT WIDGET declares METRES and writes the raw float verbatim.
get_shelf_height (app/routes/version.py:161) returns unit="m" for env/prp/drs —
its docstring says "env/prp/drs keep metres on the asset itself" —
and POST /assets/{parent}/{child}/height writes float(raw) with no conversion.
3. THE COOKBOOK READS CENTIMETRES.
CreateScaleCube calls blender.create.scale_cube(cm=height);
PlaceGeometry calls place(proxy_obj, cm=height) and logs "height=10.16 cm".
CONSEQUENCE, OBSERVED TODAY: a coffee cup was corrected to 10.16 (cm) so the cookbook builds it at 4 inches — correct. The version shelf now displays that same asset as "10.16 m". The number is right and the label is wrong by a factor of 100, on the same asset, in the same app.
It also means the two write paths disagree with each other: typing 1.2 into the shelf widget (intending 1.2 m) stores 1.2, which the cookbook reads as 1.2 cm.
AND A FOURTH INCONSISTENCY IN THE INPUTS THEMSELVES:
* character_dashboard.html — a proper unit-aware widget with an IMPERIAL/METRIC toggle, height_mode tracker and "6ft 1in" parsing. Correct.
* environment_dashboard.html — uses the shared height_field macro, declared unit='m'.
* prop_dashboard.html:95 and dressing_dashboard.html:80 — a BARE <input type="number" step="0.1"> with no unit label, no macro, no imperial support at all.
So a prop's height box cannot tell the artist what unit it wants, and cannot accept 4in the way a character's can.
WHAT TO BUILD
Pick ONE canonical storage unit for `height` and make every reader and writer agree. Centimetres is the obvious choice — the cookbook already consumes cm, and it is the only one of the three that is load-bearing for geometry.
Then:
* make the version-shelf widget convert instead of declaring a different unit,
* give prop and dressing the same unit-aware widget characters have,
* and SHOW the unit next to every height input, since the absence of a label is what made this survivable.
DECIDE AND STATE: whether display units stay per-asset-kind (metres reads better for a 40ft building, inches for a mug) while STORAGE is always cm — that is the normal answer, and it is compatible with everything above.
DONE WHEN: a height entered in any surface round-trips to the same physical size in every other surface and in the built geometry; no input box exists without a visible unit; and the shelf stops showing a 10 cm cup as 10 m.
#1237Publish the design GLB at production scale as a model artifact, instead of re-deriving scale at every importNew featureAsset Scale Integrityclaude
·Plugin · blender·Blender·3 months ago
Publish the design GLB at PRODUCTION SCALE as a model publish artifact, so consumers stop re-deriving the scale at import time.
TODAY: the dsn publish ships the raw mesh-gen .glb — Y-up, arbitrary scale, as the generator emitted it. Every consumer that wants it has to work out how big it should be. ImportDesignModels (cookbook/blender/build/prv) does exactly that at runtime: it imports the raw glb and uniformly scales it to match the placed low-res model's bounding box. In a built previs that leaves empties carrying scales like 16.716 (homestead_dsn) and 0.0755 (coffeeCup_dsn).
Those numbers are CORRECT and by design — but they are indistinguishable at a glance from a scale problem, which matters because this project has just spent a session scale-hunting. More importantly the derivation is repeated by every consumer that ever wants the high-detail mesh, and each one has to get it right independently.
THE OBSERVATION THIS COMES FROM (Tom): "we have the asset inside the model scene at publish time." The mdl build already imports the design geometry and already scales it to the declared height — CreateScaleCube and PlaceGeometry do exactly that. So the correctly-scaled high-detail mesh EXISTS, in a scene, at publish time. It is simply not exported.
WHAT TO BUILD
Export the design geometry at production scale as an mdl publish artifact, alongside the .blend / .fbx the mdl publish already produces. Then ImportDesignModels asks for the published scaled glb and imports it at 1.0 with no runtime bbox-matching.
WHY IT BELONGS ON mdl, NOT dsn — THIS IS THE LOAD-BEARING PART
Scale is a property of the MODEL, not of the design. The declared height is consumed at model build. So if the scaled glb is an mdl artifact it is republished whenever the model is — which is exactly what must happen when a height is corrected. Today's case is the proof: the coffee cup's height was wrong (121.9 cm), it was corrected to 10.16, and the model and rig were rebuilt. A scaled glb owned by dsn would have silently kept the old scale, because dsn was not republished and should not have to be.
DECIDE AND STATE:
* Format and axis convention. The raw glb arrives Y-up; state whether the published artifact is Z-up in production units, and make the convention explicit rather than implied.
* Whether the raw dsn glb remains published as well. It should — it is the design's own output and the input to this — so this is an ADDITIONAL derived artifact, not a replacement.
* What happens for an asset with no design glb. Skip and report, per the existing convention for missing publishes.
* Whether ImportDesignModels keeps its bbox-matching as a fallback for assets published before this exists, or fails naming the missing artifact. Prefer failing by name — a silently unscaled design mesh is the failure this removes.
KNOCK-ON WORTH TAKING: ImportDesignModels currently hardcodes its destination collection per group (ENV_DSN / PRP_DSN / DRS_DSN). If the import stops doing scale work it becomes a thin importer, which is a good moment to tidy that.
DONE WHEN: the mdl publish emits a design mesh already at production scale; a previs imports it at scale 1.0 with no runtime bbox matching and no non-unit empties; correcting an asset's height and republishing the model updates that artifact too; and an asset with no design glb is named rather than silently absent.
#1566Prop rigs need real posable controls, not a bare deform boneNew featurePrevis · shot finishingclaude
·Blender·3 months ago
DefaultBoneRig (cookbook blender/build/rig/) gives a prop rig a single 'root' deform bone with NO custom shape and NO bone collections. Verified live on coffeeCup: bones=['root'], bone_collections=[], custom_shape=[None]. So a published prop has nothing to click in the viewport, nothing visible to indicate it is riggable, and no control to key its own animation on. Every prop workflow ends up going through the OBJECT instead: RigCaddy selects the prop object, and rig.edit.attach_prop constrains the object. Upgrade the prop rig to a real (minimal) control rig: (1) add a CONTROL bone with a custom shape sized to the model bbox, in a 'Main' bone collection, so it is visible and clickable in Pose mode; (2) parent the existing deform 'root' under that control, keeping the mesh skinned to the deform bone and object-parented to the armature (both required - see #note); (3) keep the deform bone in a 'Deform' collection. Then rig.query.list_rigs / rig.edit.select_root can treat a prop like any other rig, select_prop can select the CONTROL in Pose mode the way select_root does for characters, and attach_prop can target the control. Needs a republish of existing props and verification against a second prop besides coffeeCup.
#1567Baking motion destroys hand-keyframed animation - need bake and keyframe layers to coexistNew featurePrevis · shot finishingclaude
·Plugin · blender · Rig Caddy·Blender·3 months ago
Today a character is EITHER hand-keyed OR driven by baked library motion, never both, and you cannot move from one to the other across a cut. Root cause is concrete: mtn.create._prune_nla_window always does 'ad.action = None' before a bake, and the active action is exactly where hand-keyed work lives - so Bake Timeline silently discards it. (The NLA STRIPS are already handled well: a windowed bake only replaces strips intersecting the shot window, so other shots' bakes survive. It is the loose action that gets wiped.) NEEDED: keyframed animation in one shot and baked motion in the next, on the same character, without either destroying the other. Options to evaluate, no decision made: (a) LAYERS - push hand-keyed work down onto its own named NLA track (e.g. 'alc_hand') that _prune_nla_window explicitly preserves, and stop nulling the action without first stashing it; the windowed bake already proves per-shot coexistence is close. (b) TWO RIGS - one keyframe rig and one mocap rig for the same character, with visibility/influence switching per shot; heavier, but clean separation and familiar to animators. (c) BAKE TO ACTION - bake into a new action and blend against the hand-keyed one. Prefer whichever keeps ONE rig in the outliner - the two-rig option should be judged against the confusion of duplicate characters in a previs scene.
#1594on the versions view - the file areas - the right click menu only works on files, it needs to work on folders as well.Bug fixTom
·Cookbook · alchemy · context-menus · files·Alchemy·3 months ago
_resolve_env_front() (alc/plugins/blender/create.py) returns None for a videos-entity previs scene, so render_style_flythrough skips the Seedance submit with 'no env style PNG resolved'. Diagnosed live on cgl/loj videos/theaxe. THREE defects stacked, all needed: (1) WRONG PROJECT - it reads PROJECT_REGISTRY.current, which resolves from config and was ProjectStore(id='jlt') while the open scene is project 'loj'. The project must come from the scene's PathObject (po.project / po.company), not from config, or the lookup queries a different show's script entirely. That script also had 0 scenes loaded ('Master Scene Registry missing on disk. Attempting script.json recovery pass'), so nothing could ever match. (2) VIDEOS ENTITY - it does scene_id = po.parent and calls script.get_scene(scene_id). For a script scene po.parent is a scene id; for entity_type 'videos' po.parent is the VIDEO name ('theaxe'), so get_scene always returns None. Needs a videos-aware path: resolve the location from the video's own manifest/shots, or resolve the env asset directly from what is actually linked into the scene (the ENV/ENV_DSN collection's source asset), which does not depend on the script at all. (3) SILENT EXCEPT - the whole body is wrapped in 'except Exception: print(...); return None', so all three failures produce one bland message and the caller cannot tell which lookup failed. Should name the failing step and the project/scene it looked in, per the no-silent-fallback convention. VERIFY WITH: the PNG exists and is correct - the published dsn MSD's data.primary_view is E:/Alchemy/cgl/loj/PUBLISHES/render/assets/env/homestead/dsn/default/publish/latest/high/env_homestead_dsn.png - so a fixed resolver must return exactly that for this scene.
#1597Ticket workflow: codify branch-from-main, PR, and restore-the-live-tree into alc_do_ticket and alc_sprintAdjust existingclaude
·Alchemy·3 months ago
Amend .claude/commands/alc_do_ticket.md (step 5, 'Wrap up on approval') and the matching wrap-up in .claude/commands/alc_sprint.md so the close-out sequence is explicit and repeatable instead of depending on whoever is driving remembering it. Four changes:
1. BRANCH FROM origin/main, NOT FROM HEAD. The skill currently says 'git checkout -b fix/<n>-<slug>', which branches from whatever is checked out. Because the farm runs the live working tree, that is routinely an unrelated feature branch carrying its own unmerged commits, which then ride along in the ticket PR. Correct sequence: (a) 'git fetch origin main'; (b) confirm the ticket's files are identical between origin/main and HEAD with 'git diff --stat origin/main HEAD -- <files>' -- empty means the branch point is safe; (c) 'git checkout -b fix/<n>-<slug> origin/main'. Uncommitted edits carry across cleanly when the diff is empty. If it is NOT empty, say so and ask rather than guessing -- the ticket may genuinely depend on the current branch.
2. ORDER done AFTER THE BRANCH EXISTS. ticket_cli's PR discovery resolves the PR from the CHECKED-OUT branch, so calling 'done' while still standing on an unrelated feature branch records that branch's PR (this happened on #1594: it recorded PR #86 instead of #87). Worse, a later 'done --pr <correct>' will NOT displace it -- it reports 'pr: kept existing' -- so it needs '--force-provenance'. Either move the board update to after the branch is cut, or add '--no-pr-scan' to the pre-branch 'done' call so discovery cannot guess wrong. Document '--force-provenance' as the repair.
3. RESTORE THE LIVE TREE WHEN THE TICKET CLOSES. Record which branch was checked out when the session started, and return to it after the merge. Leaving the ticket branch (or main) checked out silently changes what the render farm produces, because the farm renders the checked-out working tree. Cleanup should end with an explicit 'you are back on <branch>' statement and a note of anything now missing from the live tree.
4. MERGE COMMIT, NOT SQUASH -- already in the skill, but state the reason at the merge step rather than only in the provenance step: a squash rewrites the SHA just recorded on the ticket and orphans it.
Also worth a line in the preview section: ports 8091/8092 are commonly held by other concurrent sessions; fall back to the numbered alchemy-preview-N entries in .claude/launch.json rather than trying to stop another session's server.
#1598Style Fly Through writes 31GB of unused multilayer EXRs into the SOURCE treeBug fixPrevis · shot finishingclaude
·Plugin · blender·Blender·3 months ago
render_camera() repoints the multilayer FileOutput node ('render_layers_out') at the camera's render-tree exr/ dir - but the styleFlyThrough branch returns BEFORE that block: 'if camera.get(alc_role) == STYLE_FLYTHROUGH_ROLE: _render_fly_clay(start, end); return'. So the node keeps the placeholder set in edit.setup_render_layers ('//element/exr/'), which Blender resolves relative to the .blend - i.e. into VERSIONS/SOURCE. Measured live on cgl/loj videos/theaxe 000.014: 293 files, 31 GB, at VERSIONS/source/videos/theaxe/SEQ/prv/default/tom/000.014/high/element/exr/. TWO defects: (1) WRONG TREE - renders must never land in VERSIONS/source. This is worse than untidy: the publish/version-up step copies the source dir recursively (robocopy ... /E, seen in the run_task publish log), so every version-up duplicates those gigabytes. (2) THE EXRS ARE PURE WASTE - _render_fly_clay needs use_compositing=True to drive the Composite node that produces the clay PNG plate, which leaves the FileOutput node live, so it writes a multilayer beauty/clay/matte EXR for EVERY fly frame (600+). Nothing consumes them: the matte/AOV extraction runs per SHOT camera, not for the fly. FIX: mute the render_layers_out FileOutput node for the duration of _render_fly_clay (restore in its existing finally block alongside the other saved state) so no EXRs are written at all; AND repoint or mute the node defensively so a render that happens before any repoint can never resolve '//' into the source tree. Consider making the placeholder an absolute render-tree path at setup time rather than a blend-relative one.
·Cookbook · alchemy · context-menus · prv·Alchemy·3 months ago
the button should only work on a path that resolves in a MagicCam folder. (for now) like this: E:\Alchemy\cgl\loj\VERSIONS\render\videos\theaxe\SEQ\prv\default\tom\000.014\high\010_010_0010_MagiCam but this button allows us to generate a style frame for the selected previs shot.
#1601when i create a cookbook item give me the "alc_do_ticket #N" copied into the clipboard like on a normal feature request.New featureTom
·App · cookbook·Alchemy·3 months ago
#1626ticket_cli --area-path cannot route cookbook work: repo_label is hardcoded to alchemyBug fixCookbook · repo rigorclaude
·3 months ago
dev/ticket_cli.py:488 calls classify_path(path, repo_label='alchemy') with the label HARDCODED, so --area-path can only ever resolve alchemy-repo areas. Any cookbook path files with area_id=(none) and lands unrouted, which makes every cookbook ticket triage homework - exactly the rigor gap we are trying to close now that the cookbook is its own git repo on main (github.com/cg-lumberjack/cgl_rnd_pipe). The cookbook side is ALREADY BUILT and unused: alc.services.features.classify_path takes repo_label; cookbook areas are minted with a 'cb-' id prefix; alc.models.work_item has is_cookbook_area() and root_for_area() resolving cb- prefixes against CFG.cookbook_dir instead of the repo root. Only the CLI's entry point is missing. WANTED: (1) let the caller say which repo a path belongs to - either an explicit --repo alchemy|cookbook, or infer it (a path under CFG.cookbook_dir, or one that fails alchemy classification but matches a cb- area, is cookbook); (2) cookbook area prefixes are SOFTWARE-FIRST ('blender/build/rig/'), not cookbook-rooted ('cookbook/blender/build/rig/') - whichever the caller passes should work, or the error should say which form is expected, because both forms silently returned (none) when probed; (3) the dry-run NEEDS-DETAIL line should say WHY an area did not resolve rather than only that it is missing. VERIFY: filing with a cookbook path (e.g. blender/build/rig/DefaultBoneRig.py) resolves to the matching cb- area instead of (none).
#1627alc_do_ticket and alc_sprint must work the cookbook repo with the same branch, commit and PR rigorChoreCookbook · repo rigorclaude
·3 months ago
The ticket workflow skills assume ONE repo. alc_do_ticket step 5 says 'git checkout -b fix/<n>-<slug>', 'git push -u origin <branch>', 'gh pr create --base main' - all run in the alchemy working directory. Now that the cookbook is its own git repo on main (github.com/cg-lumberjack/cgl_rnd_pipe, working copy at CFG.cookbook_dir), a ticket whose work lands in cookbook files gets no branch, no PR, and no provenance. Tonight's session is the proof: the DefaultBoneRig parenting fix and the CleanScene re-parent fix were made and shipped by hand, outside any ticket, because the skill had nowhere to put them. WANTED, following the patterns already in alc_do_ticket: (1) DECIDE THE REPO from the ticket's area - is_cookbook_area(area_id) already answers this - and run the branch/stage/commit/push/PR steps in that repo's working copy, not always the CWD. (2) A ticket may touch BOTH repos (a cookbook recipe step plus the alc op it calls). Say plainly what happens then: two branches and two PRs, cross-referenced in both bodies, with the same #<n> in both commit subjects. (3) PROVENANCE - step 5.6 / 'done' searches git history for a commit carrying #<n>; it must search the repo the work actually landed in, or a cookbook ticket will forever report 0 commit(s). (4) The 'stage only what this ticket touched' rule matters MORE in the cookbook: that tree routinely carries large in-flight refactors (96 pending files earlier today), so whole-file git add is dangerous there. (5) alc_sprint should batch cookbook tickets the same way. Also worth stating the cookbook's own conventions in its CLAUDE.md so an agent working there knows them.
#1628Cookbook repo has no tests and no CI - a broken recipe step only surfaces in a live DCCChoreCookbook · repo rigorclaude
·3 months ago
The cookbook (github.com/cg-lumberjack/cgl_rnd_pipe, working copy at CFG.cookbook_dir) has a .pre-commit-config.yaml, a CLAUDE.md and a README, but NO tests/ directory, NO .github/workflows, and no pyproject.toml. Alchemy runs ruff + black + pytest (943 tests) before anything merges; the cookbook merges on inspection alone. That asymmetry is expensive because cookbook steps are PRODUCTION code - the farm and every DCC session import them from the checked-out branch, so a broken step is not caught until a render or publish fails in a live DCC, usually mid-shot. WANTED, mirroring alchemy rather than inventing something new: (1) a tests/ dir with at least import-and-signature smoke tests for every recipe step (each step module exposes run(); a step that cannot be imported, or whose run() signature drifted, should fail in CI, not in Blender). Alchemy already stubs bpy for exactly this - see tests/test_blender_hide_collection.py - so DCC-importing steps are testable headless. (2) a recipe-JSON validation test: every steps[] entry's module + import_string must resolve to a real importable module (a typo there is silent until the step runs). (3) GitHub Actions running the same ruff/black/pytest gate on PR. (4) a pyproject.toml so tooling config lives in the repo rather than being inherited. START SMALL: (1) and (2) alone would catch the common breakages.
#1642Wire cookbook context-menus into the file/folder right-clickNew featureclaude
·App · routes·Alchemy·3 months ago
The versions view's right-click menu (app/templates/partials/version/file_zones.html, showFileContextMenu) is a hard-coded list: copy path, reveal, rename, delete. The cookbook's 'context-menus' recipe type is authored and scaffolded (cookbook/alchemy/context-menus/{task}/*.py, alc/cookbook/constants.py ARTIST_RECIPES) but nothing in the app loads or renders it, so an authored context-menu button is unreachable. Surface the task's context-menu steps as extra items on the file/folder menu, the way shelves already work: a loader (mirroring _load_shelf_module in app/routes/version.py, which imports cookbook.alchemy.shelves.{task}.{button}) plus confirm/run routes, passing the RIGHT-CLICKED path -- not just the active version PathObject -- so a shot-scoped button can act on the row that was clicked.
#1748Add a style-frame column to the transcript editor, reusing the visual widgetNew featurePrevis · shot finishingclaude
·App · transcript·3 months ago
Style frames can only be made two ways today: the scene-timeline STYLE FRM cell (app/tools/script) and the version view - both per-take and both requiring a finished previs render. Neither gives the per-beat control the transcript editor already has for visuals. WANTED: a second column in the beat rows (app/tools/transcript/templates/partials/transcript_rows.html) holding a STYLE FRAME tile beside the existing visual tile, reusing partials/visual_tile.html rather than inventing a second widget - it already provides everything this needs: Generate / RE-GENERATE, the reference chips (location, + cast / prop), shot size / angle / move, SCRIBBLE, and the self-polling running state that swaps the finished tile in (gen_state running -> hx-get poll on itself). The background machinery is there too: _visual_gen_worker + _gen_key(scope, visual_id) already run a nano pass per beat and hand back a tile, so a 'style_frame' scope should slot in beside the existing one. TWO GENERATION MODES, and the second is the actually-new capability: (a) PREVIS RENDER EXISTS -> style the clay plate, the proven path (alc.services.ai.style_frame_nano.style_frame_for_shot(render_dir / cam), nano mode B on the manifest's marked anchor frame). (b) NO PREVIS RENDER YET -> generate the style frame from the beat's own storyboard visual plus its reference chips, so a style frame can exist BEFORE previs is rendered. That is what opens the workflow up: style can be explored while the script is still being written, and previs later conforms to it rather than the other way round. Mode (b) needs a new call - style_frame_for_shot is render-only - so treat it as the design work of this ticket, not a given. THE INDICATOR: each style-frame tile needs a checkmark showing whether a previs render exists for that beat's shot, because it tells the artist which of the two modes they are about to get. Resolve it the way create_take_style_frame does (app/tools/script/timeline.py): look for {cam}.*.png under the latest prv render dir for that take. Beat -> take mapping has to be resolved for this; the beat board knows blocks, the render dir knows {block}_{take}_MagiCam.
#1816Style page shows the hard-coded default spine as if it were saved, so a project with no stl.json looks configuredBug fixclaude
·App · routes·Alchemy·3 months ago
StyleManifest.load() returns an instance carrying DEFAULT_STYLE_SPINE ('painterly illustrated animation style, no photorealism') when msd/stl/stl.json does not exist, and the Style page renders that straight into the STYLE SPINE textarea as a VALUE. The page is then indistinguishable from one where somebody set that phrase on purpose: the field is populated, there is no 'not set / inheriting the default' state, and nothing says the file has never been written. On 2026-08-03 no project in cgl had an stl.json at all - loj included - yet every Style page showed that spine and every generated image prompt ended with it. WANTED: an unsaved manifest renders the default as a PLACEHOLDER, not a value, with a line saying the project has no Style manifest yet and what will be injected until it does. The same distinction matters wherever the spine is echoed (the beat board's style-frame modal now shows a 'Project style spine' line, added in #1748, and would inherit the same lie).
#1885Previs pre-pass treats every rig as a character, so a rigged prop crashes the shot's metadataBug fixclaudePrevis
·Blender·3 months ago
discover_characters() returned every {slug}_rig armature in the scene, so a rigged PROP (coffeeCup_rig, one 'root' bone, parented into Jack's hand) was measured as a character. _char_frame then did rig.pose.bones['head.x'] and raised KeyError. That lookup sits inside 'if visible', so it only fired on shots where the prop was ON CAMERA - which is what made it look random. create.py catches the pre-pass exception, prints, and renders hero framing, so each affected shot rendered at 1280x720 instead of the 1434x1434 overscan and wrote no shot_manifest.json, with nothing marking it. FIXED in this ticket: rig_kind() classifies each rig (explicit alc_entity_type stamp wins, else structural - ARP humanoid = arp_rig_type or a head.x bone); discover_rigs() returns every rig with its kind; discover_characters() narrows to characters only; ShotManifest gains props: dict[str, CharacterInShot] and the pre-pass records prop presence + depth there. Props stay OUT of layer_order/per_frame, which exist to paint characters from mattes - a prop has no matte yet (#1857). style_frame_for_shot now defaults its prop references from the manifest's props, so the scene timeline gets them too, not just the beat board.
#2006Overscan resolution has no upper bound - a close-up turns a 2MP previs frame into 24MPBug fixPrevis · shot finishingclaude
·Blender·3 months ago
prepare_overscan_render sizes the OS frame to contain every visible character, and nothing bounds the result. alc/plugins/blender/tasks/prv/metadata.py ~line 290: wf = 1.0 + margins.left + margins.right; side = round(max(hw*wf, hh*hf)/2)*2. The margins come from how far a character extends past the hero frame, so a close-up drives wf arbitrarily high - and because the frame is square, COST SCALES WITH THE SQUARE of that factor. MEASURED on cgl/loj videos/theaxe 000.014 tonight: shot 0050 rendered 1434x1434 (2.1 MP) at ~2.5 s/frame; shot 0100 rendered 4856x4856 (23.6 MP) at 45-140 s/frame - wf about 3.39, so 11.5x the pixels and roughly 50x the wall-clock. EXRs went 142 MB -> 527 MB per frame, PNGs 1.1 MB -> 12 MB. A previs render that should take seconds took 26 minutes for 15 frames and read as a hung farm job (no progress in the Monitor, and the Deadline task log looked idle because frames were 140 s apart). WANTED: (1) clamp the OS side to a sane multiple of the hero resolution (or an absolute megapixel ceiling) - previs is a fast, disposable pass and 24 MP is never the right answer for it; (2) when the clamp bites, RECORD it in the ShotManifest (e.g. os_clamped plus the requested-vs-actual factor) so downstream knows the hero framing is not fully contained rather than silently assuming it is; (3) print the chosen OS resolution and factor at render time - the number that explains a 50x slowdown should not have to be reverse-engineered from PNG dimensions. Consider whether the clamp belongs per-shot or sequence-wide, since the square aspect is deliberately fixed sequence-wide so the Seedance fly-through plates register.
#2007Previs farm jobs should submit one task per frame, not one task per shotNew featurePrevis · shot finishingclaude
·Service · deadline·3 months ago
The previs submitter makes ONE Deadline task per SHOT: the job script (generated per submit) takes an idx, picks shots[idx], and calls render_camera(cam, start, end, ...) for that shot's whole frame range. So a 43-frame shot is a single task, and any frame that stalls or dies takes the entire shot with it - and on a retry the whole shot re-renders, including every frame that already succeeded. Tonight one shot sat on a worker for 26 minutes having produced 15 of 43 frames, and from the Monitor it was indistinguishable from a hang: one task, 0 percent, no per-frame progress. WANTED: task-per-frame. Each task renders ONE frame, so (1) a bad frame fails alone and Deadline retries just that frame, (2) the Monitor's progress bar becomes real per-frame progress instead of a single bar that sits at 0 for half an hour, (3) frames spread across workers as the farm grows - the previs pass is embarrassingly parallel and currently pinned to one machine per shot. WATCH OUT FOR: the per-frame cost is not just the render - each task currently pays a full Blender startup plus the scene load (about 45 s for theaxe: reads the .blend, the linked chr/prp rig libraries, builds menus). At 2.5 s/frame that startup would dominate completely. So this needs either a persistent/managed Blender per task chunk, or a frame-CHUNK size (e.g. 5-10 frames per task) rather than literally one - measure before choosing. The existing fan-out pattern in alc/services/deadline is the place to start. Also note render_camera does a per-SHOT overscan pre-pass (prepare_overscan_render) before rendering; splitting by frame must not re-run that per frame - compute once and pass it in, or the pre-pass becomes the new bottleneck.
#2008Overscan envelope should not chase feet - close-ups need a body-part scope or per-shot overrideBug fixPrevis · shot finishingclaude
·Blender·3 months ago
The overscan envelope sizes the OS frame to contain every visible character IN FULL. That is right for a mid or wide, and wrong for a close-up: a tight shot on a face still has to overscan far enough DOWN to include the feet, because the feet belong to a visible character. The frame is square, so cost scales with the square of that factor - measured on theaxe shot 010_010_0100 tonight, a 1434x1434 hero became 4856x4856 (11.5x the pixels, ~50x the wall clock, 527 MB EXRs). Slicing a character at the ankles in a face close-up is not a defect - it is the shot. WANTED, and the design question is which of these is right: (1) BODY-PART SCOPE - build the envelope from the parts the shot is actually ABOUT rather than the whole rig bound. A head-and-shoulders framing should sample head/neck/shoulders/hands, not feet. The manifest already records per-character depth and layer order per frame, so the sampling machinery is there. (2) PER-SHOT OVERRIDE - let a shot declare its overscan intent (full body / upper body / head, or an explicit factor) and store it on the ShotManifest so it survives a re-render, the way the user-marked style-frame anchor does. (3) BOTH - derive a sensible default from shot size, overridable per shot. NOTE the interaction with the fixed square aspect: the OS aspect is deliberately uniform sequence-wide so the Seedance fly-through plates register with the OS clay, so whatever is chosen must not make the aspect vary per shot. Related: #2006 caps the resulting resolution, which is the safety net; this ticket is the correctness fix - with a sane envelope the cap should rarely bite.
#2009Flag a shot when overscan makes the hero frame a tiny fraction of the renderNew featurePrevis · shot finishingclaude
·Blender·3 months ago
Overscan exists to give the AI pass enough of a character that it does not have to HALLUCINATE identity - it earns its cost on an OTS or any framing where a face is partly out of frame. The code implements a proxy for that intent: contain every visible character in full. The proxy breaks where the intent does not apply. theaxe 010_010_0100 is a shot of Jack's boots and a mug; there is no face to protect, but the envelope still grew UP to contain his head - top margin 5.6854 frame-heights - producing a 4856x4856 render whose HERO FRAME IS 3.9% OF THE PIXELS. Every downstream pass (mattes, style frame, Seedance) then works on an image that is 96% empty overscan, which is why the styled result came out strange. It was not a bad generate; it was handed a nearly-empty picture. WANTED - DETECTION, which is the part no ticket covers yet: compute the hero fraction (hero_rect.w * hero_rect.h, already on the ShotManifest) and FLAG the shot when it falls below a sane threshold - 44.8% is a healthy shot (0050), 3.9% is broken (0100). Surface it in three places, because right now it is invisible until a render is mysteriously slow or a styled frame is mysteriously wrong: (1) print it at render time with the OS resolution and the dominant margin, so a 50x slowdown explains itself in the log; (2) store it on the ShotManifest so the shot carries its own warning; (3) show it in the UI wherever a shot's previs state is displayed, next to the style-frame controls - an artist about to spend a Seedance pass on a 3.9% hero should see that before spending it. Pair with a per-shot overscan override (#2008) so the flag has a remedy: flag says this is wrong, override says what it should be.
#2010Shot manager needs a per-shot overscan control at render submissionNew featurePrevis · shot finishingclaude
·App · script·3 months ago
Overscan is a property of the SHOT'S INTENT, and intent lives in the shot manager - not in geometry. Deriving it from the character envelope is guesswork that is right for an OTS (give the AI enough face that it cannot invent one) and wrong for a close-up (containing the whole body is exactly what the framing excluded). The artist already knows which is which when they set the shot up, so let them say so. WANTED: a per-shot overscan control in the shot manager, read at render submission. TRI-STATE, not a tickbox - the off case is the one that matters and a checkbox cannot express the middle: AUTO (today's derived envelope, the default), OFF (no overscan, hero framing only - a shot of boots and a mug has nothing to hallucinate and should not pay for context), EXPLICIT (a factor, for a shot that needs more or less than derived). REQUIREMENTS: (1) PERSIST ON THE SHOT so a re-render does not reset it - the same lesson as the user-marked style-frame anchor, which used to be silently demoted to auto by a re-render; store it where a re-render preserves it. (2) VISIBLE AT SUBMISSION - the submitter should show what each shot will render at, because the cost difference is enormous and currently invisible until frames are slow: theaxe 0050 rendered 1434px, 0100 asked for 4856px, a 11.5x pixel difference on the same 85mm lens. (3) NOT lens-derived - both of those shots are 85mm; focal length does not discriminate, camera distance does. Shot size (already authored per beat, next to angle and move) is the honest signal for the AUTO default. (4) The square OS aspect is fixed sequence-wide so the Seedance fly-through plates register with the OS clay - a per-shot control must not vary the ASPECT, only the amount.
#2037Rows should carry the shot code - once previs exists you navigate by shot numberNew featurePrevis · shot finishingclaude
·App · script·3 months ago
Timeline and beat rows are labelled by BLOCK ('Block 0020', 'Block 0130'), which is the right identity while a script is being written. The moment previs exists that stops matching how the work is discussed: renders, camera dirs, shot manifests, farm tasks and Deadline job names are all keyed on the SHOT code (010_010_0100_MagiCam), so an artist reading a row has to translate block -> take -> shot in their head, on every row, constantly. WANTED: show the shot code on the row itself - '010_0010' or the full '010_010_0010' - somewhere immediately readable, not hidden in a tooltip or only visible on hover. Surfaces that need it: (1) the scene timeline's media rows (app/tools/script/templates/partials/timeline_playbar.html) - the STYLE RND / STYLE FRM / PREVIS lanes are already per-TAKE and resolve to {block}_{take}_MagiCam to find media, so the code is already computed there and simply is not displayed; (2) the transcript editor beat rows (app/tools/transcript/templates/partials/transcript_rows.html), which will need it more once the style-frame column lands (#1748) since that column's checkmark is about whether a previs render exists for that shot. NOT every beat maps to a shot - a beat with no take has no shot code, and the row should show nothing rather than a placeholder that reads like a real code. Keep it visually quiet: this is a wayfinding label, not a heading; it should be scannable down a column without competing with the block name.
#2117i often have to refresh when attempting to select a task pill or asset on the asset grid. the pattern seems to be - go to version view, click back button - refresh is needed to click on anything.Bug fixTom
·Cookbook · blender · refresh · srb·Alchemy·3 months ago
#2118Something is wrong with the asset prp axe mdl - versions - we have a publish, but we dont have any user versions visble - it's causing the version view to behave strangely - we likely just have to fix the task msd or something similarBug fixTom
·Cookbook · alchemy · publish·Alchemy·3 months ago
#2174Beat board: hide/show columnsNew featureBeat board · video columnclaude
·App · transcript·3 months ago
Add a column visibility control to the beat board (transcript create-mode), so the board stays readable once a third visual column lands. Copy the Kanban board's "Lanes" pattern: a <details> dropdown labelled "Columns" holding one checkbox per column; toggling a checkbox shows/hides every matching cell across all beats; the hidden set persists in localStorage keyed per video so it survives re-render.
Columns to cover: speaker, text, screen time, and each visual slot (storyboard, style frame, and the video column from the sibling ticket).
Reference implementation to copy: app/tools/kanban/templates/kanban_board.html:160-172 (the checkbox list) and :766-785 (the toggle handler + the wtHiddenLanes:{project_id} localStorage read/apply).
#2175Beat board: add a video column (third visual slot)New featureBeat board · video columnclaude
·App · transcript·3 months ago
Add a third visual slot, "video", alongside "visual" (storyboard) and "style_frame", so each beat on the board can hold a generated Seedance clip. The card reuses the tile's existing video face (<video> element, SHORT badge, source in-point handle), its own Generate button, and its own generation poll — the same shape as the style-frame card, one column over.
Touch points (the slot registry already exists — extend it rather than adding a third branch per surface):
- alc/models/transcript_edit.py:84-86 VISUAL_SLOTS, :237 slot_media(), :1228 set_visual_media()
- app/tools/transcript/transcript.py:3249 _slot() and :3257 _slot_stem() (filename prefix)
- app/tools/transcript/templates/partials/transcript_rows.html:317 — the slot tuple is hardcoded as ('visual', 'style_frame'); drive it from VISUAL_SLOTS instead
- app/tools/transcript/templates/partials/visual_tile.html — currently written two-slot around the binary _styling flag at :31-32; it needs to become n-slot
Generation routes to the existing Seedance path: alc/services/ai/shot_finish.py:213 styled_video -> alc/services/ai/seedance/create.py previs_to_video, which takes the previs clip as motion authority plus the style frame as the look reference.
#2176Seedance video prompt: project-editable, and visible in the submission payloadNew featureBeat board · video columnclaude
·Service · ai seedance·3 months ago
The Seedance video prompt is a hardcoded module constant that no caller can influence, so today's submissions still carry wording tuned during the first tests — and the style prompt has moved on since. Give it a project-editable home and show it before submit.
(a) Store a video prompt template on the Style manifest (alc/models/style.py StyleManifest, saved at {msd_root}/stl/stl.json) alongside style_spine, with a per-video override on VideoStyle (style.py:703-820) resolving video -> project, the same shape as resolve_spine() at :807-820.
(b) Thread a prompt argument through the callers that currently drop it. previs_to_video already accepts prompt= (alc/services/ai/seedance/create.py:66) — it is the callers that never pass one: alc/services/ai/shot_finish.py:213-259 styled_video (no prompt parameter at all), alc/plugins/alchemy/tasks/prv/create.py:37-55, and app/tools/script/timeline.py:889-907 _run_style_video.
(c) Show the resolved prompt in the Generate payload modal (app/tools/transcript/templates/partials/visual_payload.html — it already has a "Prompt" block at :112 for the style-frame path) so the artist can read exactly what is being submitted, and edit it for that one submission without saving it back to the project.
Keep _DEFAULT_PREVIS_PROMPT (create.py:31-49) as the fallback when the project has set nothing.
#2177when a sprint no longer has items it should be cleared from the "current sprint" columnNew featureTom
·App · kanban·Alchemy·3 months ago
#2360Ticket wrap-up should reconcile In Progress items before commit + PRChoreclaude
·App · kanban·Alchemy·3 months ago
When alc_do_ticket / alc_sprint wrap up a ticket (commit + PR), nothing checks whether the ticket's sibling items are still sitting in In Progress after their work actually merged. Result: items linger in in_progress forever, so their sprint never registers as finished and the board misreports what is actually in flight. Add a reconciliation step to the wrap-up: before (or as part of) the done/PR step, surface any item still in_progress whose provenance shows a merged commit/PR, and prompt to close it out. Related: #2177 added WorkBoard.prune_current_sprint_if_finished(), which stands a sprint down once every item is done — it can only fire if in_progress items are actually reconciled, so this closes that loop.
#2452Seedance video modes: a mode dial with a default prompt per modeNew featureSeedance · video modesclaude
·Service · ai seedance·3 months ago
Turn the Seedance video prompt from ONE editable string into a mode dial, where each mode ships its own default prompt and sends its own inputs. Picking a mode is the artist's main control; the prompt is what the mode resolves to, and stays editable.
The modes (the axis is HOW MUCH AUTHORITY THE PREVIS HOLDS):
- follow — previs exact. Sends @Video1 + @Image1. This is today's _DEFAULT_PREVIS_PROMPT, unchanged.
- guide — previs governs staging, camera and where each character is; their locomotion is schematic, so the model generates natural gait and weight. Same inputs as follow, different prompt.
- prompt — no previs at all. Sends @Image1 + prose only; @Video1 is omitted from the payload.
Mode is a PER-BEAT choice, not per-project: one shot is mocap-exact and the next is rough positional blocking. Store it on the beat (VisualDescription in alc/models/transcript_edit.py) beside the existing per-beat framing/camera fields.
Follow the STYLE_MODES precedent at alc/models/style.py:670-700 — a registry holding per-mode instruction text, selectable by the artist — rather than inventing a second pattern.
Resolution order per beat: per-video override -> project override -> the mode's shipped default. Compose direction and the lipsync suffix on top, exactly as they compose today.
MIGRATION — this reshapes what shipped in #2176. StyleManifest.video_prompt and VideoStyle.video_prompt are single strings today; they become per-mode (keyed by mode). Any existing value migrates to the "follow" mode, because that is the mode it was written for. #2176 landed hours before this ticket, so there is unlikely to be real saved data yet — but check before assuming, and do not silently drop a saved prompt.
#2453Include direction on the video columnNew featureSeedance · video modesclaude
·App · transcript·3 months ago
Give the video column the same "Include direction" affordance the style-frame column already has, so a shot can carry specific direction ("he hesitates before opening the door") into the Seedance pass.
The mechanism already exists and is simply not wired to the third column:
- the per-beat direction is read by _beat_direction(doc, visual_id) and _direction_ctx(doc, visual_id) in app/tools/transcript/transcript.py
- _previs_style_payload receives direction= and direction_ctx=; _previs_video_payload receives neither
- the direction block in app/tools/transcript/templates/partials/visual_payload.html is behind {% if _styling %}, so the video payload never draws it
So: pass the beat's direction into _previs_video_payload, draw the block for the video slot, and compose the direction onto the resolved video prompt the same way the lipsync suffix composes onto it (see compose_previs_prompt in alc/services/ai/seedance/create.py).
Same contract as the style column: the toggle is per-submission, the direction itself is the beat's own field, and turning it off must leave the beat's stored direction untouched.
#2454Spike: can Seedance take images as timed keyframes, or only as references?ChoreSeedance · video modesclaude
·Service · ai seedance·3 months ago
SPIKE — investigate and report, do not build UI yet.
Establish what Seedance 2.0 on fal.ai actually supports for images, before any UX is designed around "images as keyframes":
1. Read the endpoint schemas for bytedance/seedance-2.0/reference-to-video and bytedance/seedance-2.0/fast/reference-to-video (model ids at alc/services/ai/seedance/create.py:16-19), plus any sibling image-to-video / first-last-frame endpoints in the same family.
2. Answer specifically: does the endpoint accept MULTIPLE images (image_urls is already a list and the prompt addresses @Image1)? If so, are additional images addressable as @Image2, @Image3 …? Are they TIMED — pinned to a position in the clip — or are they references the model consults freely? Is there a separate first-frame / last-frame parameter?
3. Report the answer with the schema evidence, and a recommendation on whether "images as start/end frames" is buildable as specified, buildable in a different shape, or not supported.
The distinction that matters: a reference the model can consult is NOT a keyframe pinned at time t. A picker that implies keyframing when the endpoint only does references would promise timing control the model cannot honour.
Deliverable is a written finding on the ticket, plus a follow-up ticket if it turns out to be buildable.
#2455Multi-reference images for Seedance, bound by roleNew featureSeedance · video modesclaude
·Service · ai seedance·3 months ago
Send more than one reference image to Seedance, labelled BY ROLE, not by time.
The endpoint accepts up to 9 images (fal schema: image_urls, maxItems 9, addressable in the prompt as @Image1, @Image2, …). We currently send exactly one — the style frame — so eight slots are unused. See the spike finding on #2454 for the schema evidence.
Build a small reference picker on the video payload modal:
- Slot 1 stays the shot's committed style frame (@Image1), as today.
- Additional slots bind published designs already in the project — a character design sheet, an environment/location reference — each becoming @Image2, @Image3 in submission order.
- The prompt binds them by IDENTITY, following ByteDance's documented form <Subject_N>@<Image_N> (e.g. "the woman in the red coat @Image2"), so the model knows which reference describes which subject.
Slot labels must read "Style / Look", "Character design", "Environment" — NEVER "Start frame", "End frame", or anything implying a position in time.
Thread the extra images through alc/services/ai/seedance/create.py previs_to_video, which already passes image_urls as a list — it takes one element today and needs to take several, in a stable order that matches the @ImageN numbering in the composed prompt.
Respect the endpoint's ceilings: 9 images, 3 videos, 3 audio, and no more than 12 files total across all modalities.
━━ SOURCE THE REFERENCES FROM THE BEAT, NOT FROM A PICKER ━━━━━━━━━━━━━━━━━━━━━━
Revised after a live test (see notes). Do NOT build a manual picker as the primary
mechanism. The beat ALREADY declares what is in the shot:
VisualDescription.characters : list[str] (alc/models/transcript_edit.py:154)
VisualDescription.props : list[str] (alc/models/transcript_edit.py:155)
`_beat_props(doc, visual_id)` (app/tools/transcript/transcript.py:1129) already feeds the
STYLE column at three call sites. Its own docstring says it exists so there is "one answer
to 'what's in this shot' across both columns" — written when there were two columns. There
are now three, and the video column is the one that gets neither cast nor props.
So the mechanism is the same one direction needed (#2453): wire what the beat already knows
into the video payload.
- Resolve the beat's `characters` to their published design references -> @Image2, @Image3…
- Resolve the beat's `props` likewise, and/or name them in the composed prompt.
- Bind them by IDENTITY in the prompt, ByteDance's documented form <Subject_N>@<Image_N>,
so the model knows which reference is which.
A manual picker may still be worth having as an OVERRIDE for references the beat cannot
know about (an environment plate, a lighting reference). But the default must be zero
configuration — the artist already tagged the cast and props, and being asked to tag them
again in a second place is the failure this ticket exists to remove.
#2493Tween mode: inbetween this beat's style frame and the next beat'sNew featureSeedance · video modesclaude
·Service · ai seedance·3 months ago
A fourth video mode, "tween" — inbetween this beat's style frame and the NEXT beat's style
frame.
The specific case it serves: on the beat board a beat is often a KEY, not a shot. No camera
change, hardly any character change — the beat exists to carry timing. In that scenario the
beat's style frame is exactly what is wanted as the opening image, and the next beat's
style frame is exactly what is wanted as the closing image. Animators read this as
pose-to-pose; the board is already a key-pose sequence, so this mode is inbetweening.
There is NO second image picker. The end frame is always the next beat's style frame, in
board order. That is the whole point — zero configuration, and it is why this belongs on
the beat board and not in a standalone tool.
Routes to a DIFFERENT endpoint from the other three modes:
bytedance/seedance-2.0/image-to-video, which takes image_url (start) + end_image_url (end).
See the spike finding on #2454 for the schema. previs_to_video cannot serve this — it
targets reference-to-video, which has no end-frame parameter.
Duration comes from the beat's own screen time (the board's existing time column), clamped
to Seedance's 2-15s window the way the current pass already clamps it
(_ensure_min_duration in alc/services/ai/shot_finish.py).
Set generate_audio=False. These clips have no audio and are not meant to — a tween coming
back with invented sound effects is noise to strip in the edit.
REFUSE BY NAME, in the shape the payload modal already renders refusals, when:
- this is the LAST beat — there is no next beat to tween to;
- the next beat has no committed style frame.
That is the entire refusal list.
#2507GUARD: do not change the Seedance dialogue/lipsync path (generate_audio must stay ON)Adjust existingclaude
·Service · ai seedance·3 months ago
DO NOT ACT ON THIS AS WORK. This card exists to STOP a change, not to request one.
The Seedance dialogue/lipsync path is working in production with a very small error rate
and represents weeks of work. It must not be "improved". Anyone who arrives at the idea
below has already been here — read this before touching it.
THE TEMPTING WRONG IDEA: "generate_audio is True on dialogue shots and we don't use the
returned audio, so send @Audio1 as lip-sync conditioning only and set generate_audio=False."
WHY IT IS WRONG (fal OpenAPI, Seedance2R2VInput, read 2026-08-04):
generate_audio (boolean, default true): "Whether to generate synchronized audio for the
video, including sound effects, ambient sounds, AND LIP-SYNCED SPEECH. The cost of video
generation is the same regardless of whether audio is generated or not."
Lip-synced speech is one of the things that flag GENERATES. Setting it False is the
documented way to lose the mouth match. There is also no cost saving — the schema says so
explicitly. "Conditioning only" is not a mode this endpoint has.
ALSO ESTABLISHED, and the reason the misreading was tempting:
audio_urls: "REFERENCE audio to guide video generation."
@Audio1 is a reference like @Image1 and @Video1 — the model regenerates synchronized audio
guided by it rather than muxing the file through. That is why returned audio differs from
what was sent. It is documented behaviour, NOT a defect, and not something to fix.
CONSEQUENCE: leave create.py's lipsync suffix, its "Use @Audio1 as the video's audio track."
sentence, generate_audio, and audio_urls handling ALONE — including when they look
redundant against the schema. Working beats theoretically-correct on a path with weeks of
tuning behind it.
·Cookbook · alchemy · shelves · dsn·Alchemy·3 months ago
This button will: Ensure a design publish (run one if it hasn't yet been published) Ensure a model publish, Ensure an "Assets" location for unreal in the current project, Run a UE rig build in that location, Run a UE render in that location.
#2588[unreal] [rig] UE rig render recipe — turntable mp4 out of the rig buildNew featureUE · rig turntableclaudeRig
·Unreal Engine·3 months ago
Add the missing RENDER phase for the unreal rig task: a new unreal/render/rig/rig.json plus its steps, mirroring the existing unreal/render/mdl recipe (SaveScene, CheckForSequencer, SetRenderSettings, RenderTurntable, ProcessRenders) adjusted for rig context. The mdl steps are already task-agnostic — they resolve everything from po.unreal_sequence_path(), po.unreal_level_path(), po.get_render_path() and po.unreal_asset_name — so this is close to a copy with task=rig. The rig BUILD phase already authors what the render needs: BuildRigTurntable writes chr_<child>_rig_TT_Level.umap and chr_<child>_rig_TT_Sequence.uasset at the rig's /Game coordinates. Pass: the rig turntable LevelSequence renders through MRQ to the rig version's render dir and ProcessRenders encodes a review mp4 at the version's preview path. Verify against tani, whose rig turntable assets already exist on disk in the 'assets' UE location.
#2589[unreal] ensure_location() — idempotent resolve-or-create for a UE locationNew featureUE · rig turntableclaude
·Alchemy·3 months ago
Add an ensure_location() to alc/services/unreal.py that resolves a UE location by name and returns it, creating it only when it is absent. Today create_location() deliberately raises ValueError when the world directory already exists ('never silently overwrites an existing world'), so callers that just want 'make sure this location is here' have to catch the exception or pre-check with list_locations(). ensure_location() should: return the existing Location when the world is already on disk (and optionally connect the session to it, matching create_location's connect_session default); otherwise delegate to create_location(). Keep create_location's raise-on-exists behaviour unchanged — this is a new front door, not a change to the existing one.
#2603Default Seedance video to 480p, and put resolution/tier on the payload modalAdjust existingSeedance · video modesclaude
·Service · ai seedance·3 months ago
Default Seedance video renders to 480p, and expose resolution/tier as controls on the video
payload modal.
CURRENT STATE (verified): the board's Animate calls styled_video with neither argument, so
it takes the defaults — resolution="720p", tier="fast" (alc/services/ai/shot_finish.py:220-221,
mirrored at alc/services/ai/seedance/create.py:236,242). The tier is already the cheap one;
resolution is not at its floor.
What it should do:
1. Change the default resolution to 480p for the previs/board video path. 720p is the
CEILING for reference-to-video (it rejects 1080p with a 422), not a considered choice —
nothing about judging staging, motion or character identity needs it.
2. Put resolution and tier on the video payload modal, beside the mode dial, as
per-submission controls with 480p/fast as the default. Same contract as the prompt and
the mode: visible before the generate is spent.
3. Keep the accepted sets honest — SeedanceResolution already documents that
reference-to-video caps at 720p while image-/text-to-video allow 1080p. The control must
offer what the CURRENT MODE's endpoint actually accepts rather than a fixed list, or it
will offer 1080p on a path that 422s.
Cost note worth surfacing in the UI if cheap to do: billing is per second, and there is a
~5s output floor, so a 2s beat does not cost 2s. Resolution is the lever the artist
controls; duration is mostly set by the beat.
#2604The video pass ignores the project style spine, and hardcodes 'painterly' against itBug fixSeedance · video modesclaude
·Service · ai seedance·3 months ago
The video pass is blind to the project's style spine, and ships a look adjective that
contradicts it. Two halves, one root cause.
HALF 1 — the modal states something false.
_previs_video_payload hardcodes "style_spine": "" and "spine_source": ""
(app/tools/transcript/transcript.py:1590-1593, and the sibling return around :1381-1383).
So the payload never resolves the spine, and the modal renders "no style written for this
video or the project" even when one IS set. Verified against cgl/loj, whose
StyleManifest.style_spine is "Stop Motion Animation".
Fix: resolve it the way _visual_gen_payload already does — _spine_with_source(doc)
(transcript.py:1689) — and display it with its source, exactly like the style column.
HALF 2 — the shipped prompts hardcode a contradicting look word.
alc/services/ai/seedance/create.py:42 (follow) and :89 (guide) both contain:
"Use @Image1 ONLY as a look reference — match its painterly art style, color ..."
"painterly" is baked in. On a project whose declared spine is "Stop Motion Animation", every
submission has been instructing the model toward the wrong medium. This is the same class of
defect the style-frame path already solved by injecting the spine (see
alc/services/ai/style_frame_nano.py, which pulls StyleManifest.effective_spine).
Fix: the medium comes from the resolved spine, not from a constant. Either inject the spine
where "painterly" currently sits, or drop the adjective and let @Image1 plus the spine carry
the look. Do NOT leave a hardcoded medium in a prompt that a project can set.
Decide and state which of these the video pass should do, because they are different:
(a) SEND the spine as part of the composed video prompt (parity with the style pass), or
(b) not send it, because @Image1 already embodies the look — in which case the modal must
say THAT, rather than "no style written".
Silently doing neither, which is today's behaviour, is the one option that is wrong.
━━ DECIDED (Tom, 2026-08-04): OPTION (b) — DO NOT SEND STYLE INFORMATION ━━━━━━━
"i think we should not send style information because we've already baked that into the
image at this point."
The style frame was generated WITH the project spine, so the look is already carried by
@Image1. Restating it in prose only creates a second, competing description of the same
thing — which is precisely how a project set to "Stop Motion Animation" ended up being told
"painterly".
This resolves the open question in HALF 2. Concretely:
1. Strip the medium adjective from the mode prompts. create.py:42 and :89 currently read
"match its painterly art style, color palette, lighting…". Remove the adjective rather
than replacing it with the spine — name NO medium. "Match @Image1's art style, color
palette and lighting" says everything needed and cannot contradict the image.
2. Do NOT inject style_spine into the composed video prompt. The video pass takes its look
from @Image1, full stop.
3. The modal must SAY that. Today _previs_video_payload hardcodes an empty spine and the
template renders "no style written for this video or the project" — a false statement AND
now the wrong frame entirely. Replace it with a line stating the look comes from the
style frame. The artist should read "the image is the look authority", not "you forgot to
set something".
The design this lands on is symmetrical with what the prompt already does for motion:
@Video1 is the SOLE authority on motion, timing, camera and position.
@Image1 is the SOLE authority on look.
Neither is re-described in prose, because prose competes with the reference.
SCOPE GUARD: this applies to the VIDEO pass only. The style-frame pass must keep injecting
the spine (alc/services/ai/style_frame_nano.py) — that is where the look gets baked into
@Image1 in the first place. Removing it there would remove the thing this decision relies on.
#2618[unreal] Headless recipe wrapper quits the editor mid-MRQ, then reports successBug fixUE · rig turntableclaude
·Plugin · unreal·Alchemy·3 months ago
Two compounding defects make a UE render recipe report success while producing zero frames, whenever the recipe is trampolined into a headless editor by run_cookbook(software='unreal'). PROVEN live on 2026-08-04 against tani rig tom/002.002: UnrealEditor-Cmd crashed and no frames were written, yet run_cookbook returned True. (1) TEARDOWN RACE: the generated wrapper in alc/plugins/unreal/setup/headless.py (~line 40-46) runs the recipe then hits 'finally: unreal.SystemLibrary.quit_editor()'. RenderTurntable's in_editor probe is hasattr(unreal, 'EditorAssetLibrary'), which is TRUE inside UnrealEditor-Cmd, so the step takes its ASYNC in-editor PIE branch, calls pass_check immediately and returns. quit_editor() then tears the engine down while MRQ's PIE world is still spinning up. Log timeline: 12:29:42.416 MoviePipelineLinearExecutorBase starting job [1/1]; 12:29:42.676 PIE world UEDPIE_0_chr_tani_rig_TT_Level created; 12:29:42 'ALC HEADLESS: recipe complete'; 12:29:44.636 'PCGSubsystem destroyed while still initialized'; 12:29:46.610 'Assertion failed: Index >= 0' -> crash. (2) SUCCESS BY LOG STRING: at headless.py ~line 241, passed = 'ALC HEADLESS: recipe complete' in out. The wrapper prints that line whenever the python block finishes, regardless of whether any frame landed, so a crashed/empty render still reports passed. Fix the wrapper seam so it waits for MRQ completion before quit_editor(), and gate 'passed' on actual output (frames on disk / MRQ job completion) rather than on a log substring.
#2790Style frame prompts hardcode 'painterly' against the project's own spineBug fixSeedance · video modesclaude
·3 months ago
The STYLE FRAME prompts hardcode "painterly" and "brushwork" in the same breath as they
inject the project's style spine, so a project that declares a different medium gets two
contradicting instructions in one prompt.
This is #2604's defect, one column to the left — and it matters MORE, because #2604's whole
decision ("@Image1 is the sole authority on look; the video pass sends no style text") rests
on the style frame actually carrying the project's declared medium. If the style frame is
itself generated against hardcoded "painterly", the authority it is supposed to hold is
wrong at the source.
HARDCODED SITES in alc/services/ai/nano_banana/create.py (not overridable by any project):
:41 "use ONLY its art style, color palette, brushwork, and …"
:626 "brushwork, and painterly treatment. "
:636 "match its color palette, brushwork, and painterly …"
:666 "{playblast} is the finished painterly scene, …"
:736 "Image 1 is a finished painterly frame EXCEPT for ONE gray, untextured clay …"
:742 "the background. Illustrated animation style, painterly, no photorealism. "
Also check alc/services/ai/style_frame_nano.py FINAL_LOOK_DIRECTIVE for the same pattern.
NOT a bug, leave it: _DEFAULT_STYLE_SPINE at :22 ("painterly illustrated animation style,
no photorealism"). That is a fallback for projects that have set nothing, which is exactly
what a default is for. The defect is the medium words that a project CANNOT override.
What it should do:
Remove the hardcoded medium words. The spine is already injected on these paths — it should
be the only place a medium is named. Where the wording needs to say "match its look", say
that generically: "match its art style, color palette and treatment" names no medium and
cannot contradict the spine.
Verify against a project whose spine is NOT painterly (cgl/loj is "Stop Motion Animation"):
compose a style-frame prompt and assert no medium word appears except the ones the spine
itself contributes.
#2817Exclude references on a style frame pass, and have the prompt followNew featureSeedance · video modesclaude
·App · transcript·3 months ago
On the STYLE FRAME payload modal, let the artist exclude individual references before
generating — and have the exclusion actually change the prompt, not just the file list.
Today the modal lists IMAGES SENT grouped by role (plate / characters / props) with no way
to drop one. On a shot where the artist wants only the background, every character matte,
character design and prop reference still goes.
REQUIRED BEHAVIOUR
- A per-reference include/exclude control on each image in the payload modal, and a way to
clear a whole group (all characters, all props) in one action — "everything but the
background" is the motivating case and should not take six clicks.
- Per-submission only, never saved back to the beat. Same contract as the prompt edit and
the direction toggle: this pass, not the next one.
THE TRAP — RECOMPOSE, DO NOT POST-EDIT THE STRING
The prompt embeds image indices at composition time:
alc/services/ai/nano_banana/create.py:900 f"Image {n} — {label} mask (white pixels …"
:908 f"Image {n} — {label} design reference "
:1069 f"Image {n} — {label} matte (white pixels …"
:1120 design_lbl = f"Image {n} — {label} design reference:"
and the modal advertises "image numbers already resolved". So excluding image 3 of 5 must
RENUMBER the remaining images and regenerate every reference to them. Filter the reference
list and recompose the prompt from the filtered set — never string-patch the composed
prompt. A version that removes the image but leaves the numbering produces a prompt whose
sentences point at the wrong pictures, and it fails SILENTLY: a plausible frame comes back
with the wrong character painted in the wrong place.
DROPPING A WHOLE CLASS MUST DROP ITS CLAUSES
This is more than image bookkeeping. The FINAL LOOK prompt is built around characters:
"each character appears ONLY as a gray, untextured clay figure … repaint ONLY the SURFACE
of each clay figure in place …". With every character excluded, those sentences describe
nothing that is in the payload, and instruct the model about figures it cannot see. So an
empty character set must compose a background/plate-only prompt, not the composite-pass
prompt with the character language left dangling.
Verify by composing with each combination — all refs, no characters, no props, plate only —
and asserting that (a) the numbering is contiguous from 1, (b) every "Image N" in the prompt
resolves to an image actually sent, and (c) no clause survives that refers to a class with
no members.
#2831The image editor opens a hidden layer, not the linked compositeBug fixclaude
·App · image_editor·3 months ago
The image editor opens a HIDDEN layer instead of the composite that is actually linked, so
the artist is shown an image that is not what the beat points at and not what any pass will
send.
REPRODUCED WITH REAL DATA — cgl/loj, beat v0009, shot 010_010_0080_MagiCam:
style_media (what the beat links)
…/010_010_0080_MagiCam/style_frame/style_frame.0253.png md5 464956424f03
style_frame.0253.layers.json declares three layers:
"Pasted 1" full canvas visible: TRUE <- the no-mug frame
"Layer 1" full canvas visible: FALSE <- a DIFFERENT costume variant
"Pasted 2" 153x202 at (446,449) visible: TRUE <- the mug, pasted back in
The linked composite = Pasted 1 + Pasted 2 — Jack with the mug at his mouth, green
knit hat, red plaid. That is what the board tile shows, correctly.
What the image editor displayed is layers/style_frame.0253_01_Layer_1.png — the
layer marked visible:FALSE. It is a visibly different costume: olive ribbed hat,
brown/black buffalo check. It is neither the composite nor its base.
WHERE IT IS NOT WRONG (checked, so the fix is not aimed here):
- alc/services/media/layer_doc.py:104 — flatten() DOES skip layers with visible False.
- The layer dicts carry "visible" through the API surfaces (image_editor.py:836, 917, 994).
So the model and the flatten are right; the defect is in what the editor SELECTS or RENDERS
when it opens a board that has a layers.json.
WHAT IT SHOULD DO
Opening the editor on a slot must show the composite that slot links — the same pixels the
tile shows and the same pixels a pass would send — with the layer stack presented as it was
saved, hidden layers hidden.
DATA-LOSS RISK, treat as the reason this is p1: the editor's save flattens the visible
layers and overwrites the linked composite (image_editor.py:735-737). If the editor opens in
a state that misrepresents which layers are visible, a save from that state can overwrite a
correct composite with the wrong one. Check whether the open-state defect can propagate into
save before deciding the fix is cosmetic.
Verify on the data above: open beat v0009's style-frame slot, assert the canvas equals
style_frame.0253.png, assert "Layer 1" is present in the stack but hidden, and assert a
no-op save leaves md5 464956424f03 unchanged.
━━ PROVEN ON DISK: THE SAVE IS CORRECT, THE OPEN IS NOT ━━━━━━━━━━━━━━━━━━━━━━
Tom's hypothesis was that save wrote the composite but failed to write the layers. The data
says otherwise, and the test is cheap enough to keep as a regression:
Flattening style_frame.0253.layers.json by hand (PIL alpha_composite, layers in manifest
order, at their x/y):
honouring "visible" -> pixel-IDENTICAL to style_frame.0253.png (ImageChops diff
bbox is None — not "close", exactly equal)
ignoring "visible" -> differs across the whole canvas (bbox 0,0,1024,1024), and
the result is the olive-hat / brown-check image the editor
displayed
So the saved artifacts are self-consistent: layers.json + layers/*.png + the composite all
agree, and flatten() reconstructs the linked image exactly. Nothing about the save is wrong.
THE DEFECT IS ON OPEN: the editor composites the stack WITHOUT honouring visible:false, so
it draws the hidden "Layer 1" over the base and shows an image that is not the linked
composite and never was.
That relocates the fix — do not go looking in the save path. Compare against
alc/services/media/layer_doc.py:104, which skips hidden layers correctly; whatever the
editor uses to build its opening canvas is not applying that rule.
THE DATA-LOSS ROUTE IS STILL REAL, and this makes it sharper: if the editor opens with
"Layer 1" effectively visible in its client state, the next save flattens the VISIBLE layers
(image_editor.py:735-737) and would write that hidden layer INTO the composite — silently
replacing a correct linked image with the wrong costume. The bug is currently cosmetic only
until someone saves.
REGRESSION TEST TO KEEP: for any board with a layers.json, flatten(manifest, honouring
visible) must equal the composite the slot links, byte-for-byte. That invariant is what
proves open and save agree, and it is checkable without a browser.
#2897Start Previs becomes a Previs menu, with Open Versions and Export TimelineNew featureclaude
·App · transcript·3 months ago
The beat board's "Start Previs" control should stop saying "Start" once previs exists, and
become a menu.
BUTTON STATE
- No previs versions yet -> "Start Previs" exactly as today (the current one-shot action).
- One or more versions -> "Previs", with a caret, opening a menu. Starting is no longer
the primary action once it has been started.
MENU ITEMS
1. "Open Previs Versions" — opens the Previs version view for this video.
2. "Export Timeline" — writes the board's timeline into the previs RENDER folder, and then
opens the version view in another tab so the export is visible where it landed.
THE TIMELINE'S TRACKS, in this order:
V3 videos from Seedance (the beat's video slot)
V2 style frames (the beat's style_frame slot)
V1 storyboards (the beat's "visual" slot — the board calls it Visual)
A1 VO (the beat's voice take)
Each beat contributes a clip to each track for its own screen time, so the four tracks line
up beat for beat. A beat with nothing in a slot leaves a gap on that track rather than
shifting the ones after it — the tracks must stay in sync with the board.
USE WHAT EXISTS. There is already OTIO export and FCP XML generation from the previous
iteration of this work, and this should follow it rather than invent a second path:
alc/models/edit/otio.py — OtioAdapter (:89), FcpXmlHelper (:515),
OtioSceneTimeline (:917), export_fcp_xml (:973),
read_fcp_xml (:908), normalize_otio_rates_for_fcpxml (:568)
alc/models/edit/markers.py — find_version_fcpxml (:69), review_to_fcpxml (:103)
Read those first and say in the PR which of them you reused and which you had to extend. If
the existing writer cannot express a 4-track V3/V2/V1/A1 layout, extend it rather than
writing a parallel exporter.
#2924Show the style-frame and Seedance video lanes on the beat board timelineNew featureclaude
·App · transcript·3 months ago
The beat board's NLE shows the storyboard lane and the VO lane. Add the two that exist on
every beat and are currently invisible in time:
V3 videos from Seedance (the beat's video slot)
V2 style frames (the beat's style_frame slot)
V1 storyboards (today's lane — the board's "Visual")
A1 VO (today's lane)
V1 AND V2 STAY LOCKED TO THE BEAT. They are one still per beat, so each occupies exactly its
beat's screen time and the two lanes line up with the audio, beat for beat.
V3 IS NOT LOCKED TO THE AUDIO, and this is the point of the ticket. A generated clip's
length is a fact about the RENDER, not about the beat:
- It can be SHORTER than the beat — the tile already flags this with its SHORT badge.
- It can be LONGER, and now routinely is: fal enumerates output durations as 'auto' or
4-15s, so a 2s beat comes back as a 4s clip (see SEEDANCE_MIN_OUTPUT_SEC in
alc/services/ai/shot_finish.py).
The lane must show that mismatch HONESTLY: draw the clip at its real duration, overhanging
its beat where it does, rather than trimming it invisibly to fit or pushing the following
clips along. The board's beat timing is authored; the clip's length is measured; a lane that
hides the difference is a lane that lies about what will be cut.
USE THE EXISTING MACHINERY, do not hardcode:
- Codes come from assign_track_codes (alc/models/edit/models.py:1210) — video roles count UP
from the bottom, so adding lanes ABOVE storyboard numbers them V2/V3 on its own. Never
write "V2" as a literal; a lane's number is a fact about its neighbours.
- Per-lane state (lock / enable / mute / solo, height, colour) already persists by code
through POST /set_track_state and doc.track_state, so the new lanes inherit all of it.
- alc/models/edit/planning.py builds the stack; app/tools/transcript/timeline_view.py is
already generic over "any list of Tracks" (see its docstring) and should not need
per-track special-casing.
SHARE THE TRACK DEFINITION WITH #2897. That ticket exports the same four tracks to the previs
render folder. The displayed stack and the exported stack must be one definition — if they
drift, the timeline an editor sees is not the timeline that gets handed off.
#3030[cookbook] [unreal] [rig] Implement the Ensure Review Media buttonNew featureunrealstub
·Unreal Engine·3 months ago
Ensure there's a registered review for the current version.
#3031[cookbook] [unreal] [rig] Implement the Ensure .glb file buttonNew featureunrealstub
·Unreal Engine·3 months ago
ensure a .glb file is in the render directory of the pathObject for this rig. (if there is one in the source directory, copy it over.)
#3032[cookbook] [unreal] [rig] Implement the publish buttonNew featureunrealstub
·Unreal Engine·3 months ago
typical pathObject.publish()
(ensure we are updateing the version msd on this step as well, but it should be automated.
#3059Expression slots on the character design, filled by a Seedance harvest passNew featureclaude
·Service · ai seedance·3 months ago
Give a character a set of EXPRESSION references, declared by a template, filled from a Seedance
harvest, and composed back into a design sheet.
== THE PROBLEM ==============================================================
A character resolves to ONE design reference (#2455 binds it as @ImageN; for Jack that is
chr_jack_tpose.png). A neutral sheet says what the character looks like, not what they look
like SURPRISED. On a close-up where emotion carries the shot, the beat's direction says
"jack's facial expression is surprised" while the reference asserts a resting face -- the words
and the picture disagree and the model splits the difference.
Characters with minimal features make this acute: Jack has black dots for eyes, so the entire
expressive range lives in brow, mouth and beard shape, and there is nothing to infer from.
Consistency is the gate. Identity holds across shots today; expression does not.
== 1. WHERE IT LIVES: SLOTS ON THE CHARACTER DESIGN VIEW =====================
An expression set is a fact about the CHARACTER, not about a shot, so it belongs on the
character's dsn version view as a row of slots beside the existing MAIN / ANCHOR / HEADSHOT /
T-POSE. That mechanism already exists, already drag-to-publishes, and already shows amber for
an unfilled slot. (Poses were done as a shelf button; this is the durable version.)
The set is STANDARD, not derived per script. A fixed set is what makes "what is missing"
answerable, lets the resolver bind BY NAME per beat, and survives across productions --
characters outlive scripts.
RECOMMENDED SET -- Ekman's six basic emotions plus neutral, the defensible standard and what
FACS was built to describe:
neutral, happy, sad, angry, surprised, scared, disgusted
Use "scared" rather than "afraid" to match the emotion vocabulary already in
alc/models/motion.py:265-280 (scared / surprised / angry / happy / sad / laughing / crying) --
two words for one concept across motion and design is a future bug.
Tom's starting list was neutral, smile, worried, shocked, content, eyes closed, winking. That
maps as: smile->happy, shocked->surprised, worried->scared (low), content->happy (low). It has
no anger, sadness or disgust, which a script will almost certainly need. "content" and
"worried" are INTENSITY variants rather than distinct emotions -- worth supporting as a
slight/full pair on the emotions that carry a show, but as a second tier.
Keep MECHANICAL EYE STATES (eyes closed, wink) distinguishable in the model from emotional
states. They render in the same row but serve blinks and beats, not emotion.
Practical note for minimal-feature characters: Ekman's six are distinguished mostly by brow +
mouth combinations, so they map unusually well onto dot-eyed designs -- EXCEPT disgust, which
is nose-wrinkle dominant and will be the hard one to read.
== 2. THE TEMPLATE DECLARES THE PANELS ======================================
Do NOT invent a layout format. TemplateFamily (alc/models/motion_template.py:95) is already
this shape -- its docstring reads "One design, N formats: shared slots + per-variant layout
comps":
slots: List[SlotSpec] -> the expression names
layouts: List[TemplateLayout] -> sheet formats (16:9 review, portrait, contact sheet)
and it already validates unique slot names, which is what expression names need.
ONE DEFINITION, TWO SURFACES: the same slot list drives the fill UI on the design view AND the
composed sheet, so the sheet can never declare a panel the design view does not offer, and a
slot can never exist with nowhere to appear.
The title block belongs in the template too -- character, version, project, date -- so the
output reads as an authored deliverable. Jack's existing turnaround (character_sheet.png,
"TURN MODEL SHEET / LUMBERJACK V01") shows the house convention to match.
== 3. FILLING THE SLOTS: HARVEST, SCRUB, PAINT ===============================
a. HARVEST -- run a Seedance pass anchored on the character's existing design image so
identity holds, with the expression as the direction. A clip THROUGH an expression yields
many intermediates, so one generate produces dozens of candidates rather than one roll of
the dice.
b. SCRUB AND PICK -- the artist scrubs the clip and commits a frame to the named slot. The
gesture already exists: the beat board's video tile has a slide handle that picks which
frame represents the beat (media_in, on the shared AlcPlayback transport). Same
interaction, different destination. Server-side, extracting a still at a timestamp has
precedent in alc/services/media/video.py (frame_styled_video, extract_style_bg_frames).
c. PAINT OVER -- the harvested frame is SUBSTRATE, not output. It does not need to be
finished; it needs to be ON MODEL. A designer drawing an expression from scratch
re-establishes the character's proportions every time; here the proportions are given and
the work is expression, not construction. The image editor is the second half of this
tool: harvest -> scrub -> open -> paint on a layer -> publish.
d. PUBLISH WITH THE STACK INTACT -- keep the generated frame as the base layer under the
painted layer. The slot then carries its own provenance forever: what was machine-made vs
hand-corrected stays separable, and a later correction does not start from zero. DO NOT
flatten on publish.
Identity drift needs no separate guard. Promotion is already a human act with a brush in it --
an off-model frame gets corrected at the moment it is chosen, by the person who would notice.
== 4. THE COMPOSED SHEET IS A POST-PROCESS ==================================
Compose the design sheet FROM the filled slots, through the template. Direction matters: the
sheet is derived, never an input.
* A sheet composed from the slots CANNOT DRIFT from them -- regenerate on any slot change
and it is always current. A hand-assembled sheet is stale the first time one expression is
re-harvested.
* Each expression stays individually addressable and re-harvestable. A submission binds ONE
expression as @ImageN; it never needs the sheet, and never needs the model to isolate a
panel from prose -- @ImageN addresses a whole image, not a region of one.
* The sheet becomes a real DELIVERABLE for the production designer, the art department or a
client, published and versioned like any other dsn output.
DOUBLE DUTY: composed with its GAPS VISIBLE, the sheet IS the readiness checklist in
presentable form. An empty panel where "angry" should be says the same thing as the amber
slot, to someone who will never open the design view.
== 5. READINESS: TWO DIFFERENT DEMANDS ======================================
SYSTEM-REQUIRED -- fixed, identical for every character on every show. MAIN / ANCHOR /
HEADSHOT / T-POSE exist because rigging, the UE build and the reference resolver need them.
Always drawn, always amber when empty.
SCRIPT-REQUIRED -- varies by production. A character who never smiles in this show does not
need a smile reference, and showing it as an outstanding gap is noise.
So: a fixed slot row, with the SCRIPT marking which panels this production actually requires.
Deriving the requirement needs no new authoring -- the board's per-beat direction lines already
name the expressions this production calls for.
"Is this character ready to start" then stops being a judgement and becomes answerable: every
script-required slot filled, the rest visible but not blocking. Same job the scene-prep
checklist does for a shot, applied to a character.
== 6. RESOLUTION ============================================================
Test the mechanism at 480p. Whether the harvest produces usable expressions, whether
scrub-and-commit feels right, whether the slots resolve per beat -- none of that needs
resolution, and paying more to learn the same thing is waste.
Do NOT pin resolution as part of this build. What the build owes instead: record the source
clip's resolution on the published slot and SHOW it, so a 480p reference is visibly a 480p
reference. Re-harvesting at full quality then becomes a deliberate outstanding step, the same
job the amber warning does for a missing slot.
The trap this avoids: #2603 made 480p the default for video renders, which is right for
judging staging on a disposable test. A frame picked from a 480p clip becomes the identity
reference bound into every subsequent style frame -- a default that is correct for the
disposable path becomes a quiet defect the moment the same machinery produces something
durable.
== 7. POSES -- SAME ENGINE, DIFFERENT SURFACE ===============================
Seedance can generate full-body poses the same way. But EXPRESSIONS ARE A CLOSED SET (seven,
the same seven for every character -- so "what is missing" is a real question and an empty slot
is a real answer) while POSES ARE OPEN (T-pose, sitting, reaching, mid-stride, holding a mug --
no complete list, so no meaningful gap to display).
That cardinality difference is why one is slots and the other stays an on-demand action that
files its result as a variant. Same harvest code path underneath; do not force poses into a
slot row.
== 8. SCOPE ================================================================
This is deliberately NOT the general emotion system that was ruled out earlier: it is a fixed
set of slots on a page that already has slots, filled by a pass that already exists, producing
a handful of published variants. LoRA remains the answer only if expression range has to be
reliable ACROSS productions rather than sufficient for one -- and that tooling exists
separately.
== REUSE, DO NOT BUILD NEW =================================================
TemplateFamily / SlotSpec / TemplateLayout alc/models/motion_template.py:95
emotion vocabulary alc/models/motion.py:265-280
Seedance pass, modes, direction composition shipped 2026-08-04
reference binding by @ImageN #2455
frame extraction from a clip alc/services/media/video.py
scrub-to-frame gesture beat board video tile (media_in)
paint-over with layers image editor (#2831 unblocked this)
dsn slots + drag-to-publish character design version view
== 9. WHY VIDEO, NOT IMAGE GENERATION (the empirical case) ==================
Tom, after testing: "the seedance acting that it did on him worked with the design and didn't
drift, whereas other methods of eyes closing etc with that particular design didn't work --
they put whites around his eyes. So it's the only way I've seen to automatically get that so
far without a lot of prompting."
THE MECHANISM, because it predicts where this holds. An image generator RE-DRAWS the
character, so it re-interprets the design -- and a stylized convention like dot eyes with no
sclera is exactly what a model normalizes toward realistic anatomy. Hence the whites. A video
pass CONTINUES an existing image rather than re-imagining one: it never has to decide what an
eye looks like, only how to move the one already there. The design's rules survive by
construction rather than by prompting.
IMPLEMENTATION CONSEQUENCE: anchor the harvest as a literal START FRAME, not as a loose
reference. bytedance/seedance-2.0/image-to-video takes image_url as frame one (this is the
endpoint tween mode already uses -- see #2493), so the design image IS the first frame and
everything after it is animation of that exact artwork. Reference mode (image_urls / @ImageN)
gives the model latitude to re-interpret, which is the latitude that produces sclera. Prefer
image-to-video for the harvest and say so in the code, because the reason is not obvious and
someone will otherwise "improve" it onto the reference endpoint.
This is also the strongest argument for the whole approach: it gets a design-respecting
expression set WITHOUT prompt engineering, on a character whose stylization defeats the
obvious methods.
== 10. THE SLOT IS SOURCE-AGNOSTIC =========================================
Tom: "I can obviously supply other ways of getting facial expressions as well."
The harvest is ONE way to fill a slot, not the only way. Hand-drawn, painted, photographed,
generated by another tool, or lifted from an existing sheet must all be equally valid fills.
The dsn slots already drag-to-publish, so this costs nothing IF the harvest is built as a
filler of a general slot rather than as the slot's owner.
Concretely: do not model an expression slot as "the output of a harvest pass". Model it as a
published image with a name, which a harvest pass happens to be good at producing. The
provenance (generated / painted over / supplied) rides on the slot as metadata, which is what
the layer stack from section 3d already gives for free.
#3115The expression harvest should render every expression into ONE 'expressions' clip, not one pass per slotAdjust existingclaude
·Service · ai·3 months ago
#3059 shipped the expression slots, and the harvest works - but it renders ONE expression per pass. Tom, on testing it: "it only renders one of the expressions - it should render all of them - same price" and "it should render all of them to a 'expressions' mp4".
That was the original idea and it is much cheaper. Seedance bills per second, so one clip that travels THROUGH every expression covers the whole set for a fraction of nine separate passes (15s max vs 9 x 5s = 45s), and it is ONE wait instead of nine.
WHAT SHOULD HAPPEN
- One action on the expression row renders a single clip through the whole ordered set, landing as the `expressions`-tagged mp4 (chr_jack_dsn_expressions.mp4). That tag already exists in the dsn vocabulary and this is exactly what it is for. The composed sheet already uses the same tag as a .png, so the two read as the video and the still of one artifact.
- Duration scales with how many expressions are in the pass, clamped to Seedance's 2-15s window.
- Every covered slot records that shared clip as its source_clip, plus where it sits in the sequence, so opening any expression tile loads the same clip.
THE UX THAT MATTERS
One clip fills nine slots, so the modal must not force a close/open cycle per slot - that is the friction that would undo what already feels good ("just click and pick the frame essentially"). The scrub bar should show roughly where each expression sits, and committing a frame to one slot should leave you scrubbing for the next.
KEEP the single-expression harvest. Re-harvesting ONE expression at full quality later is exactly the deliberate outstanding step the resolution badge points at, and it should not cost a re-render of the whole set. A targeted re-harvest keeps its own per-tag clip; a slot opens whichever clip its record names.
#3185filter isn't working again on the asset gridBug fixTom
·App · web ui·Alchemy·3 months ago
#3200Asset grid: 'Started only' tick box to hide assets nobody has begunNew featureclaude
·App · web ui·Alchemy·3 months ago
A checkbox in the asset-grid header that narrows the grid to assets with at least one task whose status counts as work having STARTED. Composes with the search box and the Scenes location tags (intersection, not replacement). Shows a (started/total) count so the number is visible before ticking. Hidden on an empty grid.
#3202The image editor rebuilds a stale layer stack over a newer compositeBug fixclaude
·App · image_editor·3 months ago
Opening the image editor on a board whose composite has been REGENERATED throws the new image
away and rebuilds the old edit from a stale layer manifest.
== REPRODUCED, WITH THE PRODUCER IDENTIFIED ================================
Tom: "1) click the styled image 2) Image Editor opens (with that image loaded) 3) Image editor
replaces that with its previously saved layers."
Timestamps on cgl/loj, both shots he was working:
010_010_0080_MagiCam/style_frame/
style_frame.0253.png 08-04 17:09:35 <- TODAY, a re-style
style_frame.0253.layers.json 08-03 21:33:45 <- yesterday's hand-edit
layers/ (newest) 08-03 21:33:45
010_010_0090_MagiCam/style_frame/
style_frame.0291.png 08-04 17:09:39
style_frame.0291.layers.json 08-03 21:32:09
The composite is ~20 HOURS NEWER than the manifest that is being used to rebuild it.
THE PRODUCER IS RE-STYLE, NOT FLATTEN. Tom reached this via a flattened image and proposed
"flatten should delete the layer history", which is correct and should be done. But the case
above was produced by pressing RE-STYLE: the style pass rewrites style_frame.{fs}.png and
leaves the sidecar manifest, layers/ dir and .psd untouched. That happens on every re-style,
for every beat that was ever hand-edited, so it is the common path rather than an edge case.
app/tools/image_editor/image_editor.py:873 states the assumption in as many words: "The layer
MANIFEST ({board}.layers.json) is the source of truth". That is true while the editor is the
only writer of the composite. It stops being true the moment a pass regenerates the PNG.
RELATIONSHIP TO #2831: different bug, same symptom. #2831 was the editor DRAWING a layer
marked visible:false and is fixed. This is the editor preferring a STALE STACK over a newer
composite. Both read as "the editor opens the wrong picture", which is why the first report
was diagnosed as one thing; #2831's fix is not in question.
== WHAT IT SHOULD DO ======================================================
1. STALENESS GUARD ON OPEN — the durable fix, because it does not depend on every producer
remembering. When the manifest does not describe the composite the slot links, THE
COMPOSITE WINS: open the flat image, and do not silently rebuild from the stack.
Detect it on content, not only on mtime: flatten the manifest honouring `visible` and
compare to the composite (this is exactly the invariant #2831 already established and
tests). An mtime comparison is a cheap pre-filter, but a re-style that happens to preserve
bytes should not trip it, and a touched file should not either.
Do NOT delete the stale stack silently. Tell the artist the layers no longer match this
image, and offer to discard them — losing a hand-built stack without asking is worse than
the bug.
2. FLATTEN CLEARS THE HISTORY — Tom's ask, and correct: after a flatten the composite IS the
image and the stack describes something that no longer exists. Remove the manifest, the
layers/ dir and the .psd together, so a half-cleared board cannot exist.
3. INVALIDATE AT THE PRODUCER — any pass that rewrites a board's composite should clear its
layer sidecar in the same operation, rather than leaving it for the reader to notice. The
style-frame pass is the one that bit here; a re-generate on any slot is the same shape.
Give layer_doc a single `invalidate(board_png)` and call it from the write path, so the
rule lives in one place rather than in each pass.
4. A WAY TO CLEAR IT BY HAND — Tom: "for some of the existing images i likely just have to
delete the layer history by hand." There are already boards in this state on disk; they
need an in-app way out that is not a file manager. An explicit "discard layer history"
action in the editor covers both the backlog and any future case the guard flags.
== VERIFY =================================================================
* A board whose composite is newer than its manifest opens the COMPOSITE, and says so.
* Flatten leaves no manifest, no layers/ dir and no .psd behind.
* A board whose manifest DOES reconstruct its composite still opens as a layer stack --
this must not regress the normal editing loop.
* #2831's existing tests still pass untouched.
#3203Right-click a beat board thumbnail to show the file in ExplorerNew featureclaude
·App · transcript·3 months ago
A right-click menu on the beat board's visual thumbnails, with "Show in Explorer" as its
first item -- reveal the file on disk, selected, in the OS file manager.
WHY: debugging what a card is actually showing currently means asking someone to read a path
out of a tooltip and paste it into Explorer. Every question of the form "is this the file I
think it is" starts there -- the stale-layer bug (#3202) and the identical-looking tween
thumbnails both cost time that a reveal-in-place would have saved.
* On every visual card -- storyboard, style frame and video slots alike.
* Reveal the actual linked file (the slot's media), selected in its folder, not just the
folder opened.
* The path is already resolved and already in the tile's tooltip, so this is a new action on
known data rather than new resolution.
Worth having in the same menu while it exists, if cheap:
* Copy path
* Show the layer sidecar (.layers.json / layers/ dir) when the board has one -- the exact
artifact #3202 is about, and the thing an artist currently cannot see at all.
Windows reveals with `explorer /select,<path>`; keep the OS call behind one helper so macOS
(`open -R`) and Linux can follow without touching the caller.
CONSTRAINT: a right-click menu must not swallow the existing right-click behaviour on these
tiles if there is any, and must not interfere with the drag-and-drop the cards already
support.
#3204The columns feature is hard to use for how much i want to use it, i'd rather have toggles for the Storyboard, Style Frame and Video columns that are always visible.New featureTomStoryboard
·App · transcript·Alchemy·3 months ago
#3206Keep Seedance takes instead of overwriting, and let an earlier one be made liveNew featureclaude
·Service · ai seedance·3 months ago
Every Seedance submission overwrites the previous one. Keep them as TAKES, and let the artist
put an earlier take back as the live clip on the beat.
== TODAY: THERE IS NO HISTORY, AND RE-ANIMATE DESTROYS THE PREVIOUS RESULT ==
alc/services/ai/shot_finish.py writes to a FIXED path per shot per mode:
style_frame.{fs}_styled.mp4 (+ .hero.mp4) reference modes
style_frame.{fs}_tween.mp4 (+ .hero.mp4) tween
Verified across the whole loj/theaxe sequence: exactly one file per shot per mode, 18 files
for 9 shots. A second RE-ANIMATE replaces the first with no copy kept and no prompt to
confirm.
The only thing that has ever preserved an earlier result is switching MODE between runs --
`_styled` and `_tween` are different stems, so shot 010_010_0120 happens to hold both. That is
an accident of naming, not a feature, and nobody should come to rely on it.
Tom, hitting this: "i just had an example where i liked the first one more than the second, but
now am unsure if i can get it back." He cannot. It is gone.
== WHAT IT SHOULD DO ======================================================
1. EVERY SUBMISSION WRITES A NEW TAKE rather than replacing the last. The beat's video slot
points at whichever take is LIVE; re-animating adds one and makes it live, it does not
destroy what is there.
2. A TAKE PICKER ON THE VIDEO CARD -- see the takes for this beat, play them, and set one
live. Restoring is the whole point of keeping them.
3. EACH TAKE RECORDS WHAT PRODUCED IT. This is the half that makes the feature worth more
than an undo: mode, the composed prompt actually submitted, the references bound
(@ImageN), resolution, tier, duration, and when. "I liked the first one better" is always
followed by "what was different about it" -- and today that second question is
unanswerable even if the file survives.
Everything needed is already composed at submit time; it just is not written down.
4. THE HERO CROP TRAVELS WITH ITS TAKE. `{stem}.hero.mp4` is derived from the styled mp4, and
a take whose hero came from a different render is a subtle way to show the wrong thing.
Same for the `.offset.json` lip-sync lead, where one exists.
== NOTES FOR WHOEVER BUILDS IT ============================================
NAMING: "take" is already this app's word for a recorded voice performance. Reusing it for a
generated video is consistent -- several attempts at the same beat, one of them chosen -- but
check the existing take model before minting a parallel vocabulary.
WHERE THEY LIVE: numbered siblings or a `takes/` subfolder beside the style frame, NOT the
asset VERSIONS tree -- that is a publish-level concept and far too heavy for "I pressed the
button twice".
COST IS NOT A REASON TO SKIP THIS: these clips are 300KB-1MB each at 480p. A retention rule
(keep N, or keep-until-published) is worth having eventually, but it is not a reason to keep
overwriting now, and unbounded is still better than destructive.
SAME SHAPE AS THE REST OF THE DAY'S WORK: generate several, look at them, choose one. The
expression harvest scrubs a clip and commits a frame; this keeps clips and commits one. Both
turn a single-shot gamble into a pick from a set.
== VERIFY =================================================================
* Two consecutive re-animates leave TWO takes on disk, and the first is still playable.
* Setting an earlier take live changes the tile and what a downstream pass reads.
* Each take reports the prompt, mode and references it was made with.
* A beat with exactly one take behaves as it does today -- no picker clutter for the common
case.
#3207media_bust stats the CFG-current company's root, not the request'sBug fixclaudeScript
·Alchemy·3 months ago
app/template_helpers.py::media_bust builds its stat path from CFG.prod_root. Every other reader of a root-relative media path goes through alc.path.add_root(), which resolves against alc.context.active_prod_root() -- the REQUEST's active company. Make media_bust use add_root() too, so one helper family has one idea of where the root is.
Symptom when they disagree (a tab looking at a company other than CFG-current): the stat raises, media_bust returns '' and the img/video src carries no ?v=<mtime>. The browser then serves the cached bytes -- so a re-generate or an in-place editor save shows the OLD picture, which is exactly the 'is this the file I think it is' confusion #3203 was filed about.
#3235Export the timeline before previs exists, with gaps for what is missingNew featureclaude
·App · transcript·3 months ago
Let the beat board export its timeline BEFORE previs exists, carrying whatever is there, with
what is missing tracked as todos rather than baked into the cut.
== TODAY: THE EXPORT WAITS FOR PREVIS ======================================
The Export Timeline action (#2897) writes V3/V2/V1/A1 into the previs render folder, so it
presumes a previs render. But a board has real, exportable content long before that: authored
screen time, a voice track, storyboard frames, style frames, and a written description per
beat. An editor could be cutting to that.
Nothing about the timeline actually needs previs -- DURATIONS COME FROM THE BOARD. Screen time
is authored per beat and is already what the tracks are laid out against, so a pre-previs
export is fully defined.
== WHAT IT SHOULD DO ======================================================
1. EXPORT AT ANY TIME. No gate on a previs render existing.
2. EACH TRACK CARRIES WHAT EXISTS, and a GAP where it does not:
V3 the beat's video, when it has one
V2 its style frame
V1 its storyboard ("Visual")
A1 the VO take
A beat with nothing on a track contributes a Gap of its own duration -- NOT a shortened
track and NOT a placeholder pretending to be media. A Gap preserves timing, so the four
tracks stay aligned beat-for-beat and the audio stays in sync, while being honest that
there is nothing there yet.
For a slot holding a CLIP rather than a still, export its first frame -- the picture that
represents the beat, not the motion.
3. AS PREVIS ARRIVES, GAPS BECOME CLIPS. Nothing is restructured and nothing is replaced: the
same timeline gets progressively more real. That is the property that makes this worth
doing early, and it is why the alternatives below were rejected.
== WHAT NOT TO BUILD, AND WHY =============================================
REJECTED -- RENDERED TEXT CARDS AS PLACEHOLDERS. Tempting because they are legible in the
NLE and give a "fill in the blanks" read. But a card is a picture that has to be REPLACED
when the real image lands, where a Gap simply becomes a clip. It also puts work-tracking
information inside the deliverable.
REJECTED -- A V4 TRACK FOR TEXT CARDS. Once V1 holds real storyboards, a V4 of cards shadows
them: two sources for "what is this beat", drifting apart the moment either is edited.
REJECTED -- SUBTITLES CARRYING THE DESCRIPTIONS. OTIO has no subtitle schema and FCP 7 XML
has no clean one; more to the point the project already writes sidecar .srt through the
subtitles pipeline. Forcing description text into the timeline would duplicate a solved
thing in a worse format.
== WHERE THE "FILL IN THE BLANKS" LIVES: TODOS ============================
The one thing cards were good for -- saying "something goes here, and here is what" -- belongs
in the todo list, which already exists, is already where work is tracked, and already supports
clicking through to a position.
* A beat's written visual description becomes a TODO when its visual is missing.
* The todo's existing jump takes the artist to that beat.
* As beats are filled, todos are ticked. The gap in the timeline and the item on the list
are two views of the same fact.
DECIDED -- THE TODO HOOKS TO A FRAME AT EXPORT TIME. Tom: "i don't mind todo hooking to a
frame at export time - if we want a change we just re-export."
An earlier draft of this ticket required anchoring the todo to the BEAT and deriving the frame
at display time, so re-timing could not strand it. That is NOT wanted, and the simpler version
is right: the export is a SNAPSHOT, a frame is the only way a timeline addresses time, and
``TodoItem.frame`` already exists for exactly this. Use it.
The trade, stated so it is a known cost rather than a surprise: re-timing a beat leaves
earlier todos pointing at a frame that now holds something else, and nothing announces it.
Re-exporting refreshes them. Do NOT build staleness detection for this -- it was considered
and declined.
== VERIFY =================================================================
* A board with no previs at all exports a timeline whose total duration equals the sum of
the beats' screen time, with V1/V2/V3 all gaps and A1 carrying the takes.
* A partially-filled board exports clips where media exists and gaps where it does not, with
the tracks still aligned beat-for-beat.
* Rendering a previs for one beat turns exactly one gap into a clip; no other clip moves.
* A beat whose slot holds a video exports that clip's FIRST FRAME on the still tracks.
== OPEN, WORTH TEN MINUTES BEFORE BUILDING ================================
How gaps present in the target NLE. The writer emits FCP 7 XML (``export_fcp_xml``,
target="premiere"), and both Gap and offline handling are adapter-and-app specific. Do a
throwaway export into Resolve and Premiere and look, rather than discovering it after the
track layout is built around an assumption.
== RECONCILIATION IS THE REAL WORK ========================================
Tom, immediately after the decision above: "so it's the reconciliation of the todos that will
have to be a thing."
Correct, and it is the substance of this feature rather than a detail. A re-export meets todos
that already exist -- some ticked, some open, some edited by hand -- and blind regeneration
would duplicate the open ones, resurrect the ticked ones, and destroy anyone's edits.
THE BEAT COMES BACK, AS AN IDENTITY KEY, NOT AS THE JUMP TARGET. These are two different jobs
and only one of them was declined:
the FRAME is what the todo points at -- a snapshot, refreshed by re-exporting
the BEAT is what the todo IS about -- stable, and the only thing that can match a
todo across two exports
Frames move, so they cannot identify anything. Record the beat on the todo so reconciliation
has something to match on; keep using the frame for the click.
RULES TO SETTLE (proposed; confirm before building):
* MACHINE-OWNED vs AUTHORED. A todo created by an export is the export's to manage. A todo a
human typed is not, and must never be rewritten or removed by a re-export. This is the
rule that makes the rest safe -- decide how the two are distinguished first.
* Beat still missing its visual, todo open -> update the frame, touch nothing else.
* Beat now HAS its visual, todo open -> close it. The work is demonstrably done.
* Beat still missing, todo already ticked -> do NOT resurrect. Worth surfacing though:
either it was ticked without being done or
the visual was deleted, and both are worth
seeing.
* Beat no longer exists -> the todo is orphaned. Remove it if
machine-owned; flag it if authored.
* Never overwrite todo TEXT that differs from what the export would generate -- that is
someone having edited it.
WHAT MAKES THIS TRACTABLE: the beat's description is already the todo's text, and the beat
already knows whether its visual exists. So reconciliation is a join on beat id between "what
the board says now" and "what the last export left behind" -- not a diff of free text.
#3276I want the equivalent of this "Request" field in the Adobe Premiere Plugin - essentially on the same row with the Alchemy Header - we should make it smart enough to fill itself out properly as well, just like this tool.New featurealchemyTom
·Cookbook · alchemy·Alchemy·3 months ago
#3296find out if adobe premiere can be run headless - as in - a command line script that can render a sequence from it without opening it.New featureTomScript
·App · kanban·Alchemy·3 months ago
#3327When i switch the Action / Narration voice on a script - it should mark all audio generated by another voice as "dirty" - so i can "re-generate" them. As a side note if we are not currently marking which voice was used in generation, we need to.New featureTomScript
·App · transcript·Alchemy·3 months ago
#3473in the cookbook execution panel - like the one in premiere - we should be showing default Render, Review, and Publish recipes if no software or department sepecific recipe exists for example i'm expecting to see the alchemy * default * render recipe here if premiere doesn't have a specific one.New featureTomScript
·Plugin · premiere · edt·Adobe Premiere Pro·3 months ago
#3475Author the alchemy/render/default recipe so DCC-less tasks have a fallback renderNew featureclaudeScript
·Alchemy·3 months ago
Add a shared alchemy/render/default recipe. Recipe.resolve's chain for a render phase is {software}/render/{task} -> {software}/render/default -> alchemy/render/{task} -> alchemy/render/default; the last rung has no recipe on disk, so any task without a DCC-specific render recipe resolves to None and its Render card is empty. alchemy/review/default and alchemy/publish/default both exist and are what make Review/Publish fall through correctly; render is the only phase missing its shared default.
#3494Premiere panel clipboard silently no-ops without the manifest permissionBug fixclaude
·Plugin · premiere·Adobe Premiere Pro·3 months ago
Copying from the Alchemy panel in Premiere must either place the text on the clipboard or say why it could not. navigator.clipboard is a GETTER that throws when the UXP manifest does not request the permission ('Clipboard access not supported ... Valid manifest entry required from manifest version 5'), so it must be read inside the try block, and the panel must declare clipboard: readAndWrite in requiredPermissions in manifest.json. UXP has no document.execCommand, so the permission is the only path - a failure should log rather than fail quietly.
#3495Coordinate routes push the URL the browser is already on, breaking BackBug fixclaude
·App · routes·Alchemy·3 months ago
A coordinate route must never answer with HX-Push-Url naming the address the browser is already on. The shell-restore boot (app/templates/index.html hx-get={{ boot_path }} hx-trigger=load) re-requests the very URL the browser navigated to; answering that with a self-push makes htmx add a duplicate history entry, so pressing Back appears to do nothing. Comparison must normalize the URL - query parameter ORDER and the presence of an empty query must not make two spellings of one address look different.
#3496Creating an asset returns an empty grid instead of the populated oneBug fixclaude
·App · routes·Alchemy·3 months ago
After creating an asset (chr/env/prp/drs) the response should render the asset grid containing the new asset, the same shape the GET grid returns. Today the create handler builds its own context and comes back with an empty grid.
#3497A None optional reaches PathObject as a real token and trips UnknownTokenErrorBug fixclaude
·Plugin · alchemy·Alchemy·3 months ago
Unset optional kwargs must be absent from the dict handed to PathObject, not present as None. A None passed through alc/plugins/alchemy/create.py reached PathObject as a real token and raised UnknownTokenError; only non-None kwargs should be forwarded.
#3544Code telemetry: nothing calls sync_git — the event stream has been frozen since 2026-07-19Bug fixAnalytics & Effort Attributionclaude
·3 months ago
alc/services/code_activity.py:sync_git is idempotent and its own docstring says it is meant to
run "on a schedule or after every session" so only the delta lands. NOTHING CALLS IT. Grep across
the repo returns the definition and the docstring reference -- no caller, no scheduler entry, no
hook. It was run by hand once to backfill and never again.
CONSEQUENCE, measured 2026-08-08:
last code event in telemetry 2026-07-19
alchemy commits since that date 429 (git log --all --since=2026-07-19)
cookbook commits since that date 31
in the event stream 0
Alchemy-era git events total 178 -- so the stream is missing more than twice everything it has
ever recorded, and the gap is exactly the most recent three weeks.
WHY THIS IS P1 AND BLOCKS THE DISPLAY WORK:
The missing window is the period where the process changed -- ticket-numbered commits, a PR per
ticket, several commits a day (2026-08-08 alone: #3494/#3495/#3496/#3497 each its own ticket and
branch). Any executive receipt built today would render a dataset ending 2026-07-19 and silently
omit the strongest evidence Alchemy has. That is worse than showing nothing.
IT ALSO INVALIDATES A CONCLUSION DRAWN FROM THE STALE DATA:
Sessionizer accuracy is a direct function of commit density, and the stale tail made the
sessionizer look permanently unreliable. From the weeks that DID ingest:
week commits/day median gap chained sessionized
W21 3.0 231m 25% 2.1h <- floor, useless
W23 12.6 16m 79% 16.6h <- credible
At W23 density the sessionizer measures real spans. The recent (uningested) weeks look like W23
or denser, so derived time should be TRUSTWORTHY for them -- the opposite of what the stale data
suggested. Re-ingest before deciding how much to lean on self-reported weekly_hours vs derived
time (#3500).
WHAT TO BUILD:
1. Call sync_git for every configured repo (alchemy AND cookbook -- both are already repo-labelled
in the ledger) on a real trigger. Cheapest correct options, pick one:
- on app startup, plus a periodic tick while the app runs;
- a post-merge / post-commit hook;
- a scheduled task, since it is idempotent and cheap.
Prefer something that cannot silently stop -- the failure mode here was silence.
2. Surface freshness in the UI. Show "events current to <date>" wherever this data is displayed
(Analytics, Actuals, Features). A stale stream must be visible as stale, not indistinguishable
from a quiet week.
3. Backfill the gap now as part of the fix, and verify the counts line up with git log.
ACCEPTANCE: after the fix, code events exist for every commit through today in both repos; a new
commit appears in the event stream without anyone running anything by hand; the analytics and
features pages display the data's freshness date; sync_git's ledger still prevents duplicates.
RELATED: blocks #3500 (executive receipt) and materially changes #3498/#3499 coverage numbers --
coverage is currently computed against a commit population that is missing 460 commits.
#3605Code telemetry: merge commits re-attribute already-counted work to the merge dateBug fixAnalytics & Effort Attributionclaude
·3 months ago
A merge commit's diff contains every file from the branch it merges. `sync_git` ingests merges
like any other commit (`repo.iter_commits("--all")`, no `--no-merges`), and the feature derivation
reads their full diff — so merging a PR attributes that whole branch's files to the MERGE date,
a second time, on top of the day the work was actually done.
PROVEN on real data (2026-08-08):
cookbook ea3861a2 "Merge pull request #21 from cg-lumberjack/feat/3473-panel-default-recipes"
merge dated 2026-08-08 07:24
carries 5 files (premiere/render/default/EnsureAlchemyPath.py, RenderTimeline.py, ...)
actually authored 2026-08-07 09:13 (e29a4dcd "Feat #3473: add premiere/render/default ...")
So Aug 07's work renders as NEW FEATURES on Aug 08's day row.
Scale on that one day: 31 alchemy commits, of which 6 are merges. Every merged file is counted twice
— once on its real day, once on the merge day. The weekly activity view showed 32 NEW FEATURES /
41 UPDATES for a Saturday, which is not what a person did that day, and the inflation lands
specifically on days when PRs get merged. That is precisely the wrong signal: merge days look like
the most productive days, when a merge is the one commit that creates nothing.
FIX: a merge commit introduces no new work — the work is in its parents, which carry their own
dates and are ingested separately.
1. Skip merges at ingest: `iter_commits("--all", no_merges=True)`, OR keep the event (useful for
"when did it land") but flag it `is_merge: true` in details.
2. Exclude merges from feature/leaf derivation and from the New-Features/Updates day columns
either way. If a merge event is kept, every consumer that derives features or effort from
commit diffs must filter it out.
3. Backfill correction: existing ingested merges are already in the tree. Either re-derive with
merges excluded, or mark them so consumers can filter retroactively — do NOT silently leave
the double-count in historical days.
4. Diffing a merge against its first parent is the alternative, but it is the wrong call here:
for a standard PR merge that still yields the whole branch. Skipping is correct.
RELATED, NOT FIXED BY THIS TICKET (worth stating so nobody thinks the day rows are exact after it):
- Commit date is when work was RECORDED, not when it was done. A "WIP:" commit that bundles days
of accumulated working-tree edits is genuinely authored on the day it is committed; no date
field recovers the real span. Author date does not help — on this repo author and committer
dates are identical for the affected commits (checked).
- Agent-authored commits land under the human's git identity, so a sprint run by agents inflates
that person's day (11 of 2026-08-08's 31 commits were agent-written). Distinguishing them needs
its own mechanism.
ACCEPTANCE: merging a PR no longer adds its branch's files to the merge day's NEW FEATURES /
UPDATES columns; the 2026-08-08 cookbook premiere steps appear on 2026-08-07 where they were
authored, and not on 08-08; historical merge double-counts are either re-derived away or flagged;
a test covers a merge commit whose diff carries files authored on an earlier day.
#3606Receipt: reported-hours tile counts weeks with no data, labelling a 1-week figure as 3 weeksBug fixAnalytics & Effort Attributionclaude
·App · features·3 months ago
On the Delivery receipt, the REPORTED HOURS tile sums self-reported weekly_hours across every week
the capabilities span, treating a week with NO reported hours as 0.0, then labels the total with the
count of SPAN weeks rather than the count of weeks that actually carry data.
app/tools/features/features.py ~1007:
weeks = sorted({w for c in caps for w in c["weeks"]}) # every span week
reported = round(sum(reg.weekly_hours.get(w, 0.0) for w in weeks), 1) # absent -> 0.0
...
"reported_weeks": weeks,
partials/features_receipt.html:63:
Self-reported by the person, for the {{ reported_weeks|length }} week(s) this work spans.
OBSERVED 2026-08-08 (live):
capabilities span 2026-07-23 -> 2026-08-08 = W30, W31, W32
weekly_hours holds W25 70.0 | W28 60.0 | W29 60.0 | W32 60.0
overlap W32 ONLY
tile renders "60.0 h — Self-reported by the person, for the 3 weeks this work spans."
One week of reported time is presented as three weeks of reported time. W30 and W31 are blank and
contribute a silent zero.
WHY IT MATTERS MORE THAN IT LOOKS: the tile immediately to its left reads "~16.3 h DERIVED EFFORT"
for those same three weeks. A reader compares 60 against 16 and concludes the sessionizer undercounts
by ~4x. The true comparison is one week reported against three weeks derived, which is not a
comparison at all. This is the one misleading number on a page whose entire design principle is that
an estimate must never read like a measurement — the coverage banner and the "never summed with the
derived figure" footer both get this right; this tile does not.
FIX:
1. Count and label only the weeks that HAVE reported hours: "60.0 h across 1 of the 3 weeks this
work spans".
2. When coverage is partial, say which weeks are missing, or render the tile in the muted/uncertain
style rather than the confident alcGreen — a partial cross-check is not a cross-check.
3. Do not silently coerce a missing week to 0.0 inside a sum that is then presented as a total. If
any span week lacks data the headline number is not comparable to the derived figure and the UI
must say so.
4. Consider showing derived-vs-reported per week rather than as two lump sums, so the comparison is
like-for-like or visibly absent.
ACCEPTANCE: with hours reported for only W32 of a W30-W32 span, the tile does not claim 3 weeks;
it states how many weeks actually carry data; a fully-reported span still renders as today; a span
with zero reported weeks still renders the existing "No hours reported" empty state.
RELATED: #3500 built this view. #3605 (merge commits double-count) inflates the DERIVED number on
the same page from the other direction — both should land before the receipt is shown to anyone
outside the team.
#3607Features: name capabilities by grouping TICKETS inside an area, and learn the naming as you goNew featureAnalytics & Effort Attributionclaude
·App · features·3 months ago
#3499 tags a SEARCH RESULT (find work by file or button). This is the other direction: work down the
ticket backlog you already have and name the features in it. The unit of decision is the TICKET, not
the commit — a ticket is already a human-written, human-named unit, and the commits join to it for
free through the `#<n>` already in their subjects.
THE CHAIN THAT ALREADY EXISTS (measured 2026-08-08, W31 onward):
commit --#3473--> ticket --area--> ??? --> feature
460 W31+ git events; 189 non-merge commits carry a ticket ref; 164 distinct tickets;
159 found on the board; they fall into 47 areas.
AREA IS THE CONTAINER, NOT THE FEATURE. That distinction is the whole ticket. Two proofs from real
data:
* SPLIT: `app-transcript` holds 25 tickets and is plainly several features --
Beat board #670 #671 #687 #831 #1007 #2174 #2175 #2453 #2897 #2924 #3203 #3204 #3235 #347
Visual/framing #530 #532 #533 #2817
Media linking #528 #529
Locations rail #390 #1027
Transcript/voice #341 #832 #3327
* MERGE: one Unreal story is scattered across five areas --
plugin-unreal (14) + cb-unreal-publish-rig (3) + cb-unreal-build-rig (2) + svc-unreal (2)
+ plugin-unreal-cam (2) = 23 tickets.
So the two verbs a human needs are MERGE and SPLIT. Everything else is already derived.
WHAT TO BUILD:
1. Area list with ticket counts, each expandable to its ticket numbers + titles (titles are the
readable part -- "Beat board: hide/show columns" tells you more than any path).
2. Select any subset of tickets, across areas, and name it as a Capability. Reuse the #3499
Capability model -- do NOT introduce a second concept.
3. Merge two capabilities; split one by reassigning a subset of its tickets.
4. Commits ride along automatically via the ticket's `#<n>` -- never ask anyone to pick commits.
5. Scope to W31 (2026-07-27) onward. The pre-W31 era has no ticket refs (0% vs 72% this week) and is
deliberately OUT of scope -- do not build archaeology tooling for it.
NAME SUGGESTIONS, IN PRIORITY ORDER (deterministic first, LLM last or not at all):
a. Existing capability titles -- the vocabulary already in the registry.
b. **An explicit "Prefix:" already in the ticket title.** 33 of 159 tickets (21%) carry one, and
they are good names Tom wrote himself: Beat board (6), UE MagiCam (5), Features (3), Analytics
(2), Seedance video modes, Image editor, Asset grid, Delivery receipt, Visual beats, Tracker,
Ticket workflow, Cookbook recipe rot, SmartTask, Code telemetry, Kanban. Extract with a simple
leading-`Prefix: ` match -- free, deterministic, no model.
c. The 41 dormant `curated` day-pill names.
d. Area name as the fallback.
LEARN FROM EACH NAMING -- THIS IS THE POINT, NOT A NICE-TO-HAVE:
Record HOW each capability got its name: `accepted-suggestion` (and which tier above) vs `typed`.
That ratio is the automation readiness signal -- when accepted-suggestion dominates, the suggester is
good enough to run unattended. Each human naming also grows the vocabulary that tier (a) matches
against, so the next batch is cheaper than the last. This is a growing-vocabulary matcher, not
machine learning -- keep it deterministic and inspectable.
ALSO SURFACE THE HABIT LEVER: 136 of 325 non-merge W31+ commits (42%) carry NO ticket ref, so even a
perfect ticket->feature join covers only ~58% of recent work. Show that number somewhere in this view.
Raising commit-subject compliance is cheaper than any tooling, and it is invisible today.
ACCEPTANCE: `app-transcript` expands to its 25 tickets with titles; a subset can be named "Beat Board"
in one action and the remainder named separately; the five Unreal areas can be merged into one named
capability; commits follow via `#<n>` with nobody selecting a commit; suggestions are offered from
existing titles and from `Prefix:` matches; each capability records whether its name was typed or
accepted; the view shows what share of recent commits carry no ticket ref.
RELATED: #3499 (Capability model + tag-from-search), #3501 (capability at filing time -- the forward
mechanism this backfills toward), #3500 (the receipt this feeds), #3605 (merge commits must be
excluded or they pollute every count here).
--- AMENDED 2026-08-08 ---
CORRECTION — the name suggester in tier (b) above is BACKWARDS. Do not build it as written.
Tier (b) proposes a ticket title's "Prefix:" as the FEATURE name. Working the real data showed the
prefix is the CONTAINER, not the feature. "Beat board:" names a surface the same way "app-transcript"
names a tool — the 7 tickets prefixed "Beat board:" are ~5 distinct features:
#670 + #687 timing manipulation (drag OUT point / grab-to-slide source in-point)
#2174 + #2175 column management (hide/show / add video column)
#671 scribble layout
#1007 start previs
#685 scene-prep embed
The FEATURE is the text AFTER the colon — Tom already wrote it, per ticket.
THE NAMING TEST (Tom's, final authority on every name):
A feature name completes "you can now ___". A container name doesn't.
Features are verb phrases ("drag a beat's OUT point to set its screen time",
"Design -> UE Rig" = "turn a design into a UE rig"). Containers are nouns
("Beat board", "Features", "Analytics", "Image editor").
REVISED DESIGN:
* Hierarchy is area -> surface -> feature. The "Prefix:" extraction feeds the SURFACE level (a
container grouping, derivable, useful for navigation). It must never be offered as a feature name.
* Feature-name candidates come from the post-colon verb phrases of the grouped tickets; the human
merges/splits and names the group. Suggestions from existing capability titles remain tier (a),
filtered by the tautology check below.
* Tautology check stays: slug-compare any suggested name against its area AND surface; if it
matches or one contains the other, the suggestion is worthless — demote it and require a human
name ("Features:" inside app-features tells nobody anything).
* Tom has FINAL approval/editing on every feature name. Suggestions are offered, never auto-applied.
Record accepted-suggestion vs typed per name — that ratio is the automation-readiness signal.
The acceptance criteria change accordingly: "a subset can be named ... in one action" now means the
subset is grouped under a surface and the NAME offered comes from the verb-phrase level, with the
container name never presented as the default.
#3608Ticket workflow: an edit verb for ticket_cli — amend a card in place, with amendments datedNew featureAnalytics & Effort Attributionclaude
·App · kanban·3 months ago
ticket_cli could file, claim, link, sprint and close a ticket but not CORRECT one — the only doors
for fixing a spec were the web modal or a hand-written script against the model. Corrections
discovered mid-session (a wrong number in a brief, a superseded design) went unrecorded, and an
agent picking the card up later built the stale version. Proven the same day it was built: #3607's
name-suggester design was discovered backwards after filing, and the correction had nowhere to live
but session memory.
THE VERB:
ticket_cli edit <n> [--title ...] [--spec ...|--spec-file ...]
[--append-spec ...|--append-spec-file ...]
[--notes ...|--append-notes ...] [--kind ...] [--priority p1|p2|p3]
DESIGN RULES:
* REPLACE vs AMEND are distinct: --spec rewrites the field; --append-spec adds a dated
"--- AMENDED YYYY-MM-DD ---" block so a correction reads as a correction on the card instead of
silently rewriting what triage already read. Same pair for notes. Giving both is refused.
* -file variants for text beyond shell-argument size; file wins never — inline+file together is
rejected as an almost-certain mistake.
* Rides _edit_tickets: same WorkBoard.editing() lock as the live web app (no clobber races), one
save, and an architect-ledger entry per edit — edits leave receipts like every other mutation.
* Refuses: unknown ticket, empty edit (no flags), short title (<12 chars, same gate as submit),
unknown kind/priority, and a no-op edit ("already reads exactly like that").
VERIFIED: all guard rails exercised; #3607 amended via --append-spec-file with the corrected
area->surface->feature design; original spec intact below the amendment; ledger entry written.
#3609Labor Actuals method sentence drifted from the sessionizer constantsBug fixclaude
·App · analytics·3 months ago
The Labor Actuals panel prints its own method, and that sentence must always describe the algorithm that actually ran. It had drifted: the copy said 'sessions split on gaps over 15 min' while time_actuals.SESSION_GAP_SECONDS was 120*60 (2 h), and it never mentioned the 45 min per-session ramp at all. Because the panel's whole claim is that its numbers are honest, a reader trusting the stated method was being told a different algorithm than the one that produced the figures. Fix: time_actuals.method_note() formats the EFFECTIVE dials (it takes the same gap_seconds/tail_seconds the reductions take, so it describes the run and not just the module defaults), summarize() ships them as summary['method'], and the template renders the sentence from those values instead of restating them. Checked app/tools/features/ for the same class of restatement: it consumes the sessionizer via active_seconds_by_key but its copy only describes attribution (even-split, commit-time floor), which stays true whatever the dials are, so nothing to change there.
#3610Receipt: per-feature history timeline — a span bar with commit dots, exec-firstNew featureAnalytics & Effort Attributionclaude
·App · features·3 months ago
Tom's mockup direction for displaying the HISTORY of a feature (his words: pack information into
the dots — the commits essentially — while keeping the interface open to show the start/end of the
feature, which is what an exec is going to care about).
THE SHAPE — one horizontal band per capability on the Delivery receipt:
* A time axis across the band (the capability's own first->last span, or the page's date filter
when one is set).
* A SPAN BAR from first commit to last commit — the headline an exec reads: when it started, when
it finished, how long it stayed open. This is the layer that must stay visually quiet and
legible at a glance.
* A DOT PER COMMIT positioned at its timestamp ON the span. The dots are the drill-in layer:
tooltip (or click-out) carries sha, subject line, repo, ticket #<n>, files/lines touched.
Visual encoding is available on the dot — e.g. color by repo (alchemy vs cookbook), or accent
when the commit carries a ticket ref — but keep to at most two encodings so the band stays calm.
* Active-day clustering falls out naturally: dots bunch on working days and the gaps read as the
idle spans — the drip-vs-push pattern (6 active days of 17) becomes VISIBLE instead of a number.
WHY THIS AND NOT A CHART LIBRARY: telemetry.py already builds all charts as inline SVG matched to
the dark chrome (bar_chart_svg, timeline_svg, calendar_strip_svg from #3491 is the nearest
precedent — a per-task strip of day cells). This is the same idiom one level up: a
capability_history_svg(events/commits, span, tz) builder beside the others, consumed by
features_receipt.html. No JS chart dependency.
HONESTY REQUIREMENTS (inherit the receipt's discipline):
* Merge commits are EXCLUDED from the dots (#3605) — a merge is not work, and a burst of merge
dots on a Friday would recreate the exact distortion #3605 removes.
* The span bar derives from the same commit set as the dots — never from a different population,
or the bar and its dots contradict each other.
* An empty or single-commit capability renders honestly: one dot, no fake span.
PLACEMENT: inside each capability card on the receipt, between the schedule line and the area
chips. The collapsed card keeps its current one-line summary; the band appears when the card is
expanded (evidence trail) — OR always-on if it fits the card height budget; builder decides on
sight, states the choice.
ACCEPTANCE: Design -> UE Rig renders a band whose bar runs 2026-07-23 -> 2026-08-08 with 22 dots
(minus merges) bunched into its ~6 active days; hovering a dot names the commit and its ticket;
alchemy and cookbook dots are distinguishable; the receipt page still has no horizontal scroll.
RELATED: #3491 (calendar_strip_svg precedent), #3500 (the receipt this lives on), #3605 (merge
exclusion this must inherit), #3607 (grouping view — the same band could later preview a proposed
group before it is named, but that is NOT in this ticket's scope).
--- AMENDED 2026-08-08 ---
TOM'S MOCKUP ARRIVED after filing ("FEATURE TIMELINE VIEW") — it corrects the layout above in two
ways. This amendment wins where it conflicts with the original spec.
1. VERTICAL LANES ON ONE SHARED TIME AXIS — not a horizontal band inside each capability card.
The mockup is a PAGE-LEVEL comparative view: a date axis running down the left, one vertical
lane per feature side by side, each lane a colored line with glowing dots (commits) along it.
The exec reading is comparative: what was in flight this month, when each feature started,
when each finished, which overlapped. A per-card band cannot answer that; a shared axis can.
(The per-card band from the original spec may still exist as a secondary use of the same
builder, but the mockup's view is the deliverable.)
2. MILESTONE CALLOUTS ARE A FIRST-CLASS LAYER — the original spec missed this entirely.
The mockup annotates lanes with labeled cards at specific points:
- top card = feature name + facet ("FEATURE: UE RIG WORKFLOW (V2)")
- mid callout = "BRANCH MERGED (Branch v2)"
- end callout = "DEV END DATE (Sprint 1)" / "DELIVERED v1.0 (Live)"
So each lane reads: START (first commit) ... commit dots ... MILESTONES ... END.
RESOLUTION OF THE #3605 TENSION, and it is elegant: merge commits are EXCLUDED from the dots
(they are not work) but they are EXACTLY the "BRANCH MERGED" milestone callouts (they are
events). A merge stops polluting effort and starts marking delivery. Ticket done-stamps and
PR-merge events are the other derivable milestones. Nothing new needs capturing.
DERIVABLE MILESTONES, in priority order:
a. first commit -> lane start (under the feature card)
b. PR merge commits -> "Merged (PR #n)" callouts (the #3605-excluded merges, reused)
c. ticket done stamps -> "#n done" (board status_history already records them)
d. last commit -> lane end; label "last activity", NOT "delivered" — delivery is a
claim, and unless a real delivery event exists the label must not overstate.
LAYOUT NOTES from the mockup: dark chrome, glow on dots, color per lane; 3-4 lanes fit an exec
view — beyond that, pick by date filter or top-N by effort with an explicit "N more not shown"
line (no silent truncation). Vertical = time flows down; dates on the left gutter.
ACCEPTANCE (replaces the original): the receipt (or a "Timeline" toggle beside it) renders
Design -> UE Rig, Kanban Board and Code Telemetry as three parallel vertical lanes on one date
axis; each lane starts at its feature card, carries its non-merge commit dots with hover detail,
shows a "Merged (PR #126)"-style callout where its PR merges land, and ends at last activity;
lanes are color-distinct; no horizontal page scroll; features outside the filter are counted,
not silently dropped.
#3667Capabilities are ticket-first: born when the ticket is filed, commits join through ticketsBug fixAnalytics & Effort Attributionclaude
·App · features·3 months ago
A capability's commit membership currently comes from whatever route tagged it, and the search route
(#3499 tag-batch) produces FALSE MEMBERS. Proven on the flagship example, reading the Design -> UE Rig
timeline lane with Tom (2026-08-08):
4 of its 13 non-merge dots do not belong to the feature:
6e452e34 07-23 "Added a features page..." <- the FEATURES PAGE, tagged because the
"Build UE Rig" search matched shared files
492d1c09 08-08 Fix #3544 (code-telemetry ingest) <- this sprint's own commits, swept in the
eb0bda70 08-08 Feat #3498 (feature search) same way
Consequence: the lane "started 07-23" -- eight days before the feature existed -- and its last
activity reads today. Both endpoints of the exec headline are wrong.
The feature's REAL birth is a ticket, exactly as Tom remembered: #275, created 2026-07-31,
"[cookbook] [unreal] [rig] Implement the Create Rig from Design GLB button" (with siblings #276
Import GLB rig, #278 IK Rig rebuild, same day). The ticket-linked commits in the lane (#232-234,
#2548, #2589, #3030-32) are all correct members -- every false member is search-derived.
THE MODEL CHANGE (Capability, alc/models/feature.py):
1. Capability gains ticket_numbers as FIRST-CLASS membership. Commits derive through tickets --
subjects carrying #<n> of a member ticket (the #3607 tickets-view path already computes this) --
plus individually-confirmed extras for pre-convention work.
2. Search (#3499) demotes from direct commit-tagger to DISCOVERY aid: a search result proposes the
tickets (and orphan commits) it finds, each requiring confirmation before membership. A file
match is a lead, not evidence -- shared files (app shell, features tool) make search sweep in
neighbours, which is exactly how todays false members got in.
3. Membership curation: remove a commit or ticket from a capability, reversibly. Wrong members are
discovered by LOOKING (that is how these were found) so removal must be one click, not registry
surgery.
4. Filing-time capture (#3501) becomes the PRIMARY edge: ticket names capability at filing ->
commits inherit through the ticket at wrap-up. Search backfills history; tickets carry the future.
THE TIMELINE CHANGE (#3610, app/tools/features/history.py):
5. A lane STARTS at its earliest member ticket's filed date, rendered as a distinct marker
("Filed #275 07-31") below the first commit dot -- the gap between filed and first commit is
real information (design/wait time). Lane foot label becomes the filed date; "started <first
commit>" remains only for capabilities with no member tickets.
6. Ticket done-stamps become available as milestones alongside PR merges (board status_history
already records them).
MIGRATION: existing capabilities keep their commit_keys but the receipt/timeline flag members that
no member ticket explains ("2 commits unexplained by this capability's tickets") so Tom can prune
from the view. Design -> UE Rig is the acceptance case.
ACCEPTANCE: Design -> UE Rig rebuilt ticket-first (#275 #276 #278 + whichever Tom confirms) shows
its lane starting at "Filed #275 07-31"; the features-page commit and the two 08-08 telemetry
commits are gone from its dots; a search result can no longer add a commit to a capability without
confirmation; removing a member is a single reversible action.
RELATED: #3499 (search-tagging -- demoted by this), #3501 (filing-time capture -- promoted by this),
#3607 (tickets view -- the machinery this builds on), #3610 (timeline -- start marker changes).
--- AMENDED 2026-08-08 ---
TOM'S FINAL DIRECTION (2026-08-08 — supersedes everything above where it conflicts, including
the suggestion machinery in #3607's design):
"STOP attempting to automate the tagging of the features — just give me a very easy way to tag
them myself. It's the only way to ensure we even have the right elements in the model. Tag enough
that the right data shows up on the timeline."
WHAT THAT MEANS — BUILD EXACTLY THIS, NOTHING CLEVERER:
A. THE TICKETS VIEW IS THE AUTHORITY, AND IT IS A HAND TOOL.
1. Checkbox any set of tickets (the checkboxes exist) -> ONE action bar with two moves:
- type a NEW capability name -> create + map
- pick an EXISTING capability (type-ahead over the named list) -> add to it
That second move is the accretion case (a feature grows one ticket at a time) and it must
be as cheap as the first.
2. Every ticket row SHOWS its current capability ("in: Design → UE Rig") and un-mapping /
re-mapping is one click, reversible. No orphaned states, no registry surgery.
3. NO automated grouping, NO candidate clusters, NO name suggestions pushed at him. The
surface chips (Prefix: -> BEAT BOARD etc.) may stay as passive visual grouping for
scanning, and the verb-phrase text is fine as a PREFILL when exactly one ticket is
selected — but nothing proposes, nudges, or auto-applies. He types the name.
4. The screengrab bug confirms why: rows #670 AND #687 both showed the same suggestion chip,
which is ALREADY a named capability's title. Suggestions rendered as noise. A row that is
mapped shows its membership; a row that is not shows nothing.
B. TICKET-FIRST MODEL (unchanged from the spec above): Capability.ticket_numbers is first-class;
commits join through #<n> in subjects; search (#3499) results and any commit-level tagging
become CONFIRM-ONLY leads; filing-time capture (#3501) remains the forward path.
C. THE TIMELINE READS THE AUTHORITY:
1. All named capabilities render as lanes (drop max_lanes=4; horizontal scroll inside the
panel; page overflow-x stays forbidden). One quiet count line: "N tickets not yet mapped".
2. Lane starts at earliest member ticket's filed date ("Filed #275 07-31"); dots = commits
explained by member tickets; unexplained legacy members flagged visibly on the lane.
3. Clicking a lane card jumps to that capability in the Tickets view. The timeline never
edits; the authority edits.
D. LEGIBILITY: each page states in one plain sentence what a lane/surface/mapping is and where
membership comes from. No hidden categorization.
E. DO NOT AUTO-PRUNE. Design → UE Rig keeps its 4 false members until TOM removes them in the
new UI (one-click, reversible). An agent must not delete members from existing capabilities.
ACCEPTANCE (replaces prior): Tom can, in under a minute each: select 5 beat-board tickets and
name "Beat Board"; add #347 to it afterwards; create a capability from a single ticket; un-map a
wrong member from Design → UE Rig. The timeline then shows every named capability as a lane
starting at its first ticket's filed date, and the app-transcript pile can be worked down from
26 unmapped without leaving the page.
SEQUENCING — UI-FIRST (Tom): build the tagging UI FIRST and prove its mechanics on test fixtures;
Tom then does the real tagging himself with his own judgment; the timeline's hydration is VALIDATED
BY that tagging session, not assumed by the build. The agent must not fabricate mappings on the
live board to make the timeline look hydrated — an empty-but-correct timeline that fills as Tom
tags IS the deliverable. Ship the UI, then step back.
#3670Skills ask for the feature name: capture capability at filing and at ticket startChoreAnalytics & Effort AttributionFeature Attribution WorkflowclaudeMotion Capture
·App · kanban·Alchemy·3 months ago
Tom: "it's obvious that we should attempt to name the feature we're working on when writing the
ticket — and if it's not named, make it part of the initial ticket setup from alc_sprint and
alc_do_ticket skills to ask the operator what the feature should be called."
#3501 built the plumbing (WorkItem.capability, submit --feature, Request-pill field, wrap-up
auto-tag). This ticket adds the ASK at the two moments a human is already touching the ticket:
1. dev/ticket_cli.py: `edit <n> --capability "..."` — set/replace the capability on an EXISTING
ticket (the edit verb covers every other card facet; this is the missing one, and the skills
below need it to record the answer).
2. .claude/commands/alc_submit_ticket.md: filing rule — always attempt to name the feature via
--feature; naming guidance = 2-3 words, the thing ADDED, passes "you can now ___", never the
widget/app/tool name (those derive — #3668).
3. .claude/commands/alc_do_ticket.md: at claim time, if the ticket carries no capability, ASK the
operator what the feature should be called, then record it (ticket_cli edit --capability).
4. .claude/commands/alc_sprint.md: same rule while reading briefs (§2) — collect the unnamed
items and ask ONCE as a batch, not one interruption per item; record each answer before
dispatch so chain agents inherit named tickets.
Optional-but-refusable stays the law: the operator can decline ("skip") and the ticket proceeds
unnamed — a wrong forced name is worse than an empty one (#3501's own rule).
ACCEPTANCE: edit --capability round-trips on a real ticket; all three skill files carry the rule;
/alc_do_ticket on an unnamed ticket asks before work starts.
--- AMENDED 2026-08-08 ---
THIRD CAPTURE POINT (Tom): "confirm feature, widget, app/tool before commit/PR."
5. Wrap-up confirmation, in alc_do_ticket.md and alc_sprint.md (§5, before the commit/PR steps):
present the triple for the ticket(s) being landed —
Feature: <the ticket's capability, or the ask if still empty>
Widget: <derived from ui_keywords / touched templates — pick-list, per #3668>
App: <derived from area_id -> tool label>
— and have the operator confirm or correct in one exchange. Record the confirmed feature via
ticket_cli edit --capability. Widget/App confirmations have nowhere durable to land until
#3668 decides storage — record them in the PR body text for now (a receipts breadcrumb), and
note that #3668 inherits the storage question.
Wrap-up is the RIGHT confirmation moment because derivation finally has data: the commits are
known, so the derived widget/app can be shown as evidence rather than guessed.
#3672Kanban modal: Capability field moves directly under the Task lineAdjust existingAnalytics & Effort AttributionFeature Attribution Workflowclaude
·App · kanban·3 months ago
The Capability input (#3501) renders at the bottom of the ticket modal, after the Priority/Sprint grid — visually orphaned from the ask it describes. Tom: 'that should be directly under the task line — or even over the top of it. I'll rarely ever enter this since most tickets are created with the quick create, but it makes the most sense from a UI standpoint.' Move the block (label + input + datalist + its comment) to sit immediately below the Task title input, above the categorization grid. No behaviour change.
#3716Tickets view: inline Capability field on every row — type it where you read itAdjust existingAnalytics & Effort AttributionFeature Attribution Workflowclaude
·App · features·3 months ago
Tom, using the hand tool: the checkbox->action-bar flow works but the fast path is an editable Capability field ON EACH ROW (the empty right end of the line) — type the name where you read the ticket, Enter maps it, auto-complete over existing capability titles. Prefilled with current membership when mapped (the input becomes the membership display; the one-click unmap x stays beside it). Submits ONLY on Enter and no-ops server-side when the value equals the current holder's title (the #3687 lesson: never write an unchanged field). Empty value is a no-op — unmapping stays on the explicit x. Reuses name_tickets (create/accrete/move semantics identical to the action bar). The action bar remains for multi-select batches.
#3717Tickets view: row fields save on blur, row-local refresh, bar only for batchesBug fixAnalytics & Effort AttributionFeature Attribution Workflowclaude
·App · features·3 months ago
Tom filled SIX inline capability fields and none saved. Faults: (1) the row input commits only on Enter — moving to the next field commits nothing; (2) any action swaps the whole body, wiping every other row's un-saved text; (3) the checkbox action bar auto-opens and suggests a competing path ('the upper area is honestly just confusing — i don't know what the path is to set these values'). Fix: row field commits on change (blur) AND Enter; the response re-renders ONLY that row (unsaved text elsewhere survives); the row's unmap x also stays row-local; the action bar renders only when 2+ tickets are checked (it is the batch tool — single tickets are the row's job) and the single-select prefill goes away. Known cost, acceptable: group unmapped-counts refresh on next full render, and reversal of a row unmap is retyping the name (cheap now). Server keeps the no-op guard (#3687).
--- AMENDED 2026-08-08 ---
TOM, seeing the bar again: 'i don't see a lot of use for this section.' REMOVE the action bar and the row checkboxes outright (merge has its own dropdown; nothing else consumes selection). What survives of it: ONE slim always-visible line — add-by-number + capability + Map — because founding tickets (#275) have no row to type on. The inline row field is THE path; the top line is only for tickets the list can't show.
#3732Timeline: stretch time vertically, pack lanes into columns — page scrolls downAdjust existingAnalytics & Effort AttributionFeature Attribution Workflowclaude
·App · features·3 months ago
CORRECTED after Tom rejected the rotation ('i like time vertical — space it out so we can fit all of the tags horizontally; we're stretching horizontally because we don't have enough vertical space within time to see them all'). Time stays VERTICAL, now at top. Changes: (1) the axis height derives from the data span — px-per-day (~96, clamped) instead of a fixed 560px plot, so 17 days is ~1,700px tall and the page scrolls DOWN naturally; (2) lanes PACK into columns: a lane occupies a y-interval (card top -> foot label), and lanes whose intervals don't collide share a column — single-day capabilities from different days stack into the same column instead of each claiming 300px of width; (3) lane columns narrow (~150px) with compact cards (trimmed title, full title in tooltip) so the packed columns fit the page width; the panel keeps overflow-x only as a safety net. Newest-first placement so column 0 carries the most recent work. Tests updated: same-day lanes take different columns, disjoint-period lanes share one, height scales with span.
#3748Capability panel: commit removal stays in place — local row swap, inline undo, live countsBug fixAnalytics & Effort AttributionFeature Attribution Workflowclaude
·App · features·3 months ago
Pruning unexplained commits from a capability panel is a focus fight: each x re-renders the whole body, the details panel collapses, and scroll position is lost ('when i click x they are removed but focus changes so i can't keep working; the dropdown also closes so it's hard to find the list again'). Fix, same pattern as the row fields (#3717): the x swaps ONLY that commit line, replacing it with a slim 'removed <sha> — undo' stub; undo restores the line in place (commit/add row-local); the panel's '<N> commits · <M> unexplained' header updates out-of-band; the details element never re-renders so it stays open and scrolled. Nothing else changes — full-body path stays for non-row callers.
Too slow for a demo, and serving views we tried but don't need at this stage (Tom: 'the timeline is
what's valuable in a demo — ticket mapping is the only other thing I use').
MEASURED (2026-08-09, live main): 11,261 event files; scanner first-in-process walk 1.76s, and the
mtime cache still RE-STATS the whole tree per request = 0.26s floor on EVERY page; WorkBoard.load
0.21s per request. Warm endpoints 0.5-0.9s, cold 1.5s+; the first click after a server start pays
walk+parse — the demo killer.
FIX, three layers + one surface change:
1. Scanner TTL: EventScanner.scan(max_age=N) — within N seconds of the last pass, return the last
result WITHOUT the stat-walk. Features-tool readers pass max_age=30 (the code-sync watcher only
lands new events every 30 min anyway; a row-map save doesn't change the events tree). The
analytics panel's explicit Refresh keeps max_age=0 — a refresh IS a rescan, unchanged.
2. Board memo: cache WorkBoard.load keyed on the board file's (mtime, size) — a save invalidates
naturally, every read between saves is free.
3. Startup warmup: prime the scanner once on app lifespan (piggyback next to the code-sync
watcher), so the FIRST click in a demo hits a warm cache instead of paying the 1.76s walk.
4. SURFACE: /features LANDS ON THE TIMELINE. Header order: Timeline · Tickets first-class;
Receipt and the weekly Activity view stay reachable but secondary (experiments keep their code,
lose their prominence — nothing deleted, nothing loading eagerly).
TARGET: warm endpoint <150ms; first demo click <500ms after startup warm; landing view = timeline.
ACCEPTANCE: measured before/after numbers in the PR; analytics Refresh still rescans; a row-map
save still reflects immediately (registry reads are not TTL'd — only the events walk is).
#3768Timeline: filter lanes by anything they know, and mark bugfix lanes with a bug glyphNew featureFeature Attribution Workflowclaude
·App · features·3 months ago
Two demo needs (Tom, making a video that must show ONLY the UE rigging, facial expressions and
attach/detach-prop features):
1. FILTER the timeline. A filter box on the timeline header; terms are comma-separated and OR'd
("ue rig, facial, attach" -> every lane matching any term). A lane MATCHES on anything it
knows: capability title, member ticket numbers, member ticket TITLES, or any commit dot's
subject — because tagging is partial by design ("all of that might not be tagged yet, but I
know the Build UE Rig button is tagged so the filter should find that"). Case-insensitive
substring. The count line says how many lanes the filter hid (no silent truncation); clearing
restores all. The dynamic stretch/packing then lays out ONLY the filtered lanes — three clean
lanes for the video instead of 43.
2. BUG GLYPH on bugfix lanes. A lane whose member tickets are majority change_type=bugfix carries
a small bug marker on its card (tooltip says why) — so fixes read apart from features at a
glance. Derived from the board's change_type; nothing new captured; mixed lanes stay unmarked.
Board metadata for member tickets (filed/done, already loaded) extends to carry title +
change_type — one read, no extra requests.
#3773Tickets page: ONE spreadsheet grid — sortable columns, editable capability AND area, search as a filterNew featureFeatures · ticket gridFeature Attribution Workflowclaude
·App · features·3 months ago
Tom, using the shipped tool: "On the current editable rows we don't have a way to edit the area and
other derived details (which are sometimes wrong). My take is a spreadsheet view with sortable
columns as the ONLY viewing/editing mechanism on this page. I think it makes everything easier."
And on Find: "it returns great results, but it's the older display method without the ability to
edit the capability — we should really only be using the one that's working."
CONSOLIDATION: the Tickets page becomes ONE grid. The grouped-by-area details view, the surface
chips, the capability panels below, and the search-results block all fold into it or die.
THE GRID — rows are TICKETS (the authority unit), columns sortable by click:
# ticket number (links to the board modal once #3671 lands)
Title read-only, truncated with tooltip
Capability THE working editor — the #3716/#3717 inline field exactly as it behaves today
(type-ahead, saves on blur/Enter, row-local swap, OOB counts, x to unmap)
Area EDITABLE — a pick-list over the registry's areas (ids exist; the CLI edit --area
writes this already, the grid needs the same write via a row endpoint). Fixing area
also fixes the derived App, which is downstream of it.
App derived from area (read-only, updates when area is corrected)
Widget derived from ui_keywords; editable as a simple tag list on the ticket (ui_keywords is
a real WorkItem field — editing it here fixes the derivation at its source)
Type change_type (bug/feature/chore) — editable select; feeds the timeline's bug glyph
Filed / Done dates, sortable — sort by Filed = the backlog in age order
Commits count, with the audit expander (the per-commit explained/unexplained + remove stays,
as the row's expandable detail, not a separate panel)
SORTING server-side via a sort= param (the page is already hx-swapped); default: unmapped first,
then filed desc. A group-by-area toggle can REPLACE the old fixed grouping if grouping is missed.
SEARCH CONVERGES: the Find box stays in the header but becomes a FILTER over the same grid — the
#3498 engine (file path / UI label -> commits -> tickets) selects which ROWS show (plus an
untagged-commits stub section only for matched commits with no ticket). The old search-results
block with its own batch-tagging UI is deleted; one surface, one editing grammar.
EDITING GRAMMAR carries over from #3717/#3748 everywhere: type where you read, save on
blur/Enter, row-local swaps, OOB count updates, one-click reversible removals, no suggestions.
All row writes to the BOARD (area, widget/ui_keywords, type) go through the same lock the CLI
uses (WorkBoard.editing) and stamp the architect ledger like ticket_cli edit does — grid edits
are board edits and leave the same receipts. (#3687's stale-form rule applies: write only
changed fields.)
PRECEDENT: the tracker grid / my-tasks grid patterns already in the app for sortable columns.
#3784Unified coordinate ontology: area slugs become renderings of real axes (realm x software x stage x task)DesignOne Fact One HomeFeature Attribution Workflowclaude
·Ontology · models·2 months ago
Tom, reviewing the #3773 grid's area drift: 'plugin-unreal-cam - this is a very specific alchemy plugin architecture - app = unreal, task = cam, auto-tagging that is gold... i would like to see if there's a way to create a unified coordinate system for files found within the plugin area, services area, model area, and within the web app.'
FINDING (audited 2026-08-09, live board): the coordinate system already exists in three spaces sharing two axes - it is only code-space that flattened its coordinates into opaque slugs.
code-space: realm x software x stage x task (flattened into area_id slugs)
asset-space: PathObject company/project/entity/TASK/variant/version (structured, load-bearing)
execution-space: {dcc}.{verb}.{task}.{op} etymology + recipe_type
Shared axes: SOFTWARE and TASK. 'cam' in plugin-unreal-cam == PathObject.task 'cam' == unreal.build.cam.* - one code, three spaces. The unification is promoting those axes to first-class on the Area model instead of parsing them out of strings.
EVIDENCE - the slugs are recoverable. A 5-rule grammar parses 360/364 tickets (67/70 distinct in-use areas):
plugin-{software}-{task} plugin-unreal-cam -> realm=plugin, software=unreal, task=cam
cb-{software}-{stage}-{task} cb-unreal-publish-rig -> mirrors the cookbook's literal disk layout
svc-{domain}(-{detail}) svc-ai-seedance -> software=alchemy
app-{tool} app-features -> software=alchemy, surface=features
alc-{band}|core|misc alc-plugins -> core alchemy
Misfits (3, all fixable slugs not grammar failures): plugin-smart-task, plugin-etymology, cookbook-engine.
Cross-check: parsed software vs WorkItem.software agrees 246/263 where both exist; the 17 disagreements are the two-fields-one-fact drift (mostly stale software=alchemy on plugin-unreal tickets; #346 the reverse). Same disease as #3780 (WorkItem.capability vs registry ticket_numbers).
BUILD (staged):
1. Area model (alc/models/feature.py) gains optional axes: realm, software, stage, task, detail. area_id becomes a RENDERING of coordinates, not the truth. Root-cause at the model layer - no per-callsite parsing.
2. Backfill: parse the 388 registry + in-use ids through the grammar; hand-review the misfits; fix the 3 bad slugs.
3. The grid's register-area flow (#3773) populates axes, not just id+name.
4. Grid columns become coordinate projections. DISPLAY DECISION NEEDED: if App = software axis, ~300 rows read 'Alchemy' and the surface/tool distinction needs its own home (second column? App shows software only when != alchemy?). Decide looking at the grid.
5. WorkItem.software / production_task become DERIVED from the area's axes (one write, one truth) - resolves the drift class alongside #3780.
6. Later, the gold: path -> coordinate derivation (alc/plugins/{software}/, cookbook/{software}/{stage}/{task}/, app/tools/{tool}/) so a commit's diff carries coordinates deterministically. Feeds tagging with zero suggestions.
Vocabularies to lock (all already exist in practice): realm {plugin,cookbook,service,app,model,core}; software = the existing WorkItem.software vocabulary; stage = the cookbook semantic arc {start,build,render,review,publish}; task = the PathObject task codes (scr,dsn,mdl,rig,cam,mtn,ldv,prv,edt,amc,...).
#3785The "Tracker" view should simply be a view option for the asset grid. It reduces our navigation complexity considerably.New featureAsset View ModesTom
·Alchemy·3 months ago
#3786Add a 'Tasks' link to the Browse sidebar for task-centric trackingNew featureTelemetry TruthBrowse task viewclaude
·App · routes·Alchemy·3 months ago
Add a 'Tasks' item to the Browse workspace sidebar that opens the tracker's Task View (rows = individual tasks grouped by entity), so task-centric tracking is reachable from Browse without going to the Planning workspace. Follow-up to #3785, which folds the Tracker matrix into the asset grid as a Details view toggle.
#3787Timeline: day names on the date axis, weekends tintedAdjust existingFeature Attribution Workflowclaude
·App · features·3 months ago
Tom: 'It'd be good to have Day names above or to the left of the dates - help us connect days with days of the week - and having a slightly different color for weekends would be helpful visually.' The timeline's date gridlines (08-04, 08-03...) gain the weekday name (Mon/Tue...), and weekend gridlines/bands render in a slightly different color so the work-week rhythm is visible at a glance.
#3788Timeline renderer: ONE shared lane/axis engine — feature and creative timelines speak the same languageChoreCreative TimelineCreative Timelineclaude
·App · features·3 months ago
Tom: 'bring the artist workflows into the same visual language for a timeline — this will help a lot to see creative work the same way.'
The feature timeline's renderer (app/tools/features/history.py: Lane + timeline_svg) is the visual language: vertical date axis newest-at-top, weekday names + warm weekends (#3787), packed lanes, cards, filed diamonds, solid/hollow dots, milestone callouts, foot labels. EXTRACT it into a shared module (suggest app/tools/timeline.py) so the coming Creative Timeline renders through the SAME engine and the language cannot fork.
Parameterize only what is already generic: the lanes list, colors, link_base, and the caption vocabulary (('Filed', 'commits', 'Merged PR') become caller-supplied labels so creative lanes can say 'Created', 'versions', 'Approved'). The Lane dataclass moves; build_lanes STAYS in features/history.py (it is feature-specific derivation).
ACCEPTANCE: the feature timeline renders IDENTICALLY through the shared module — pin with the existing tests/test_feature_timeline.py suite unchanged except imports. No new visual features in this ticket.
#3789Creative timeline: what is a DOT for an artist — the event-to-mark vocabularyDesignCreative TimelineCreative Timelineclaude
·App · analytics·3 months ago
The feature timeline's honesty comes from its vocabulary: dots are commits, milestones are PR merges + done-stamps, and a dot no member ticket explains renders hollow. The creative timeline needs the equivalent decisions BEFORE the view is built, mapping the production event stream's verbs onto marks:
- DOT (the artist's commit): a VERSION PUBLISH (commit_version / publish-phase events). NOT every event — a render farm burst or an auto-save is machine time, not an artist statement.
- MILESTONE: review flags (flag_for_review), review approvals, tracker status -> done. Possibly 'sent to next dept' (the publish that hydrates downstream).
- HOLLOW vs SOLID: what marks unverified work? Candidate: a version with no review event = hollow, reviewed = solid — the same 'unexplained' honesty transposed.
- IGNORED: start/build/app/menus noise (the phase table shows 112 start events worth 10.2s — noise), dev_mode events, automation.
Decide against the THREE TIME UNITS model (machine/artist/schedule): the timeline shows SCHEDULE (calendar truth) decorated with artist moments; machine time stays in the existing phase/task bars.
Deliverable: a short mapping table (event pattern -> dot | milestone | ignore) recorded in the module docstring of the lane builder this feeds (#3790's build step), plus agreement on the hollow rule. Keep it testable against loj's real stream: the mapping must place jack/homestead/coffeeCup Design + Model work sensibly on the axis.
--- AMENDED 2026-08-09 ---
Tom, sharpening the model: 'all the telemetry data for artists hinges on TASKS - task phases, button clicks... we can see the lifecycle of a task whether it be a modeling task, texturing task, script task, or editorial task through its related telemetry data. And this would be USER-CENTRIC - right now i'm the only user contributing so everything will show what i'm doing and what alchemy's doing for me.'
Two consequences for the vocabulary:
1. THE LANE IS A TASK'S LIFECYCLE - the unit is the asset-task (the tracker's unit), and every telemetry event already carries the task coordinates. The mapping table is therefore event -> mark WITHIN a task lane, not a global classification.
2. TWO ACTORS, TWO READINGS - the marks must distinguish what the ARTIST did (publishes, review requests: the dots and milestones) from what ALCHEMY did for them (renders, builds, automation: machine work). Machine work is not noise to discard - it is the leverage being purchased - but it must never read as artist statements. Candidate: machine bursts render as the lane's dimmer worked-span segments or small tick marks, never as dots. This is the timeline form of the three-time-units model (machine/artist/schedule) and the leverage-ratio story.
--- AMENDED 2026-08-09 ---
DECIDED (Tom, 2026-08-09) — the mapping, blessed with one sharpening:
- PUBLISH -> DOT. Tom: 'publishes are more like the equivalent of a PR - a strong signal. It means these assets are available for downstream consumption.' So a creative dot is semantically a PR-merge, not a commit: there IS no commit-tier artist event (saves are machine chatter). Dot tooltips speak availability ('v012 published - available downstream').
- commit_version (version phase) is the crisp 'version became available' record - the dot's timestamp source where present; publish-phase events corroborate.
- REVIEW -> MILESTONE ('Sent to review', 'Approved'). bug_logged -> red MILESTONE.
- RENDER + BUILD -> MACHINE ticks ON the artist's lane. Tom: 'i like renders being ticks on the artist's lane - it's all part of the process.' Small, dim, never dots - leverage visible, never mistakable for artist statements.
- start / app / menus / architect / other -> ignored (session and navigation chrome).
- HOLLOW RULE: a publish with no subsequent review/approval in the lane renders hollow ('unreviewed'), solid once reviewed - flagged, never hidden, resolved by a human act.
- BEGIN diamond: the task's first event (tracker created date when available); foot 'Created MM-DD'; the diamond->first-publish gap is design/wait time.
Grounded against loj: 858 events / 20 task lanes (publish 227, build 241, review 89, render 76, version 27, start 112, chrome 86).
--- AMENDED 2026-08-09 ---
REFINEMENT (Tom): 'Start is a good signal because we know the artist started work there - a great first step mark.' So: the FIRST start-phase event on a task is the lane's BEGIN diamond ('Started MM-DD') - evidence the artist opened the work, not an inference from whatever event came first. Subsequent starts stay ignored chrome. The diamond -> first-publish gap now reads precisely: time from opening the task to the first downstream-consumable version.
#3790Analytics: the Creative Timeline — every asset-task as a lane on one date axisNew featureCreative TimelineCreative Timelineclaude
·App · analytics·3 months ago
Tom, looking at cgl/loj: 'we do have great views that are informative, but none of them are as informative as the timeline view we've just developed. We need the equivalent but for creative tasks.'
THE VIEW — the feature timeline transposed to production work, rendered through the SHARED engine (#3788) with the vocabulary #3789 decides:
- a LANE is an asset-task pair (homestead-Model, jack-Design) — the same unit the existing event view lanes by, and the unit the tracker tracks. Lane card colored by the production TASK's own color (ProductionTaskRegistry colors — the same hues the rest of the app already speaks).
- BEGIN: the task's first activity (first event or tracker created date — whichever #3789 blesses). The gap between begin and first publish is real information, exactly like filed-to-first-commit.
- DOTS: version publishes (the artist's commits). Hollow per #3789's rule (e.g. unreviewed).
- MILESTONES: review approvals, status -> done. Callout labels like 'Approved v012'.
- AXIS: vertical, newest at top, weekday names + warm weekend bands (#3787 rides free through the shared engine).
- CLICK-THROUGH: a lane card jumps to the entity's version view (the /version page) — the timeline reads, never edits, same as features.
PLACEMENT: a new view on the Analytics page alongside Production/Architect/Actuals — likely the LANDING view once it proves out (the #3763 move: the timeline became the features landing because it does most jobs once hydrated). Sequencing: after #3788 (engine) and #3789 (vocabulary); both are small.
VERIFY on cgl/loj: jack-Design's 08-01 burst, the homestead/coffeeCup Model evening work, and videos_theaxe_seq_prv should read as lanes a producer can narrate.
#3791Analytics: loads fast — shell first, heavy panels lazy, effort memoizedAdjust existingCreative TimelineCreative Timelineclaude
·App · analytics·3 months ago
Tom, on cgl/loj: 'we have a lot of the same issues with the Analytics link - it takes a long time to load.'
DIAGNOSIS FIRST — measure before touching (the scanner already has the mtime parse-cache and the #3763 TTL, so the cold walk is only part of it). The /analytics request computes EVERYTHING inline per hit: full scan, aggregate, two SVGs, bug manifests, task_effort + the sessionizer + calendar strips (analytics.py _ctx). Profile a cold and a warm hit on loj and write the numbers into the ticket before fixing.
THEN, the #3763 playbook that already fixed the Features page:
1. SHELL FIRST: analytics_page returns the chrome immediately; the body loads via hx-trigger='load' (the lazy_body pattern features_main.html uses). The link click must paint in under ~300ms.
2. MEMOIZE the expensive reductions keyed on (scan signature, filters) — effort/sessionizer especially; an unchanged-data revisit should be near-instant end to end, not just scan-instant.
3. LANDING VIEW pays the least: whichever view lands first (the Creative Timeline, #3790, is the candidate) should be the cheapest to build — don't compute the Actuals stack for a visitor who never opens Actuals. Split _ctx so each view computes only what it renders.
ACCEPTANCE, on loj: warm revisit interactive well under a second; cold first hit paints the shell immediately with panels streaming in; numbers recorded before/after in the PR body.
#3792Analytics: the Task grid — every asset-task as a row, telemetry columns, 1:1 with the timeline's lanesNew featureCreative TimelineCreative Timelineclaude
·App · analytics·3 months ago
Tom: 'if it's helpful to think of the tasks as a spreadsheet view - that maps to the timeline as a step inbetween so we have a 1:1 with the development tasks - we should do that.'
THE PATTERN, TRANSPLANTED: dev-side has the Ticket grid (the spreadsheet authority, #3773) and the Feature timeline (its projection). Creative-side gets the same pair: the TASK GRID is the spreadsheet whose ROWS are asset-tasks, and the Creative Timeline (#3790) renders those same rows as lanes - one unit, two surfaces, 1:1.
THE ROW is an asset-task (homestead-Model, jack-Design - the tracker's unit, and the unit every telemetry event already carries). Verified on loj: 858 events resolve to 20 task lanes, and nearly all flow through the cookbook phases by design (build 241, publish 227, review 89, render 76, start 112).
COLUMNS (sortable, the #3773 grid grammar - reuse its header/data-col/column-picker mechanics):
Task entity - task, colored by the production task's own color
Status the tracker's status (read-only here; the tracker stays the editing authority)
Versions publish count (the artist's commits, per #3789's vocabulary)
Last last activity date
Artist artist time (sessionized active time)
Machine machine time (renders/builds - what Alchemy did for the artist)
Reviews review/approval count
USER-CENTRIC: with one contributor today the grid reads as 'what Tom did and what Alchemy did for Tom'; a user filter comes when there are users to filter.
Rows are READ-ONLY projections of telemetry + tracker + versions (unlike the ticket grid, the authorities here - tracker statuses, versions - have their own editing surfaces). Click a row -> the version view; the timeline toggle shows the SAME rows as lanes.
#3793Timeline: quiet gaps compress — vertical space goes to the days that have workAdjust existingCreative TimelineCreative Timelineclaude
·3 months ago
Tom, on the first live render: 'we might need some kind of calculation to figure out the vertical spacing - right now we don't have enough vertical spacing to fit everything and we're not expanding it even though most of this work only covers a few days.'
DIAGNOSIS: the axis is linear and loj's stream reaches back to May, so ~70%% of the plot is empty months; the 5200px height cap is spent on nothing and the real nine working days get ~520px - cards and milestone callouts collide.
FIX in the shared engine, OPT-IN so the feature timeline's pinned rendering is untouched: timeline_svg(compress_gap_days=N). Marks (dots/milestones/machine/begin) define ACTIVE intervals (padded, merged); a gap longer than N days collapses to a fixed ~34px BREAK rendered honestly - a visible break glyph with 'Nd quiet' on the gutter, no ticks/bands inside. Active intervals keep the full px-per-day stretch, so the dynamic-height search finally has room to separate lanes and collisions melt. The creative timeline passes compress_gap_days=4; features stays linear (0) for now.
ACCEPTANCE on loj: the 07-30..08-08 cluster gets the majority of the plot height; the May tail reads as one labelled break; no card/callout collisions at default width.
#3794Artist UI work emits task-attributed telemetry — the script editor firstNew featureTelemetry TruthArtist UI buttonclaude
·App · analytics·3 months ago
Found via the theaxe lane (#3790): loj_videos_theaxe_seq_scr shows only its Started diamond because that is ALL its telemetry - 7 start-phase events from task creation. Tom worked the script plenty; that work happened in the Artist UI (script editor) which emits no task-attributed events, so the lane honestly shows nothing. Tom: 'mostly i want to know if there's telemetry data we're tracking that we're not displaying - or if we need more data to show that reality.' For scr-type tasks the answer is: we need more data.
BUILD: Artist UI surfaces that ARE the work for a task emit lifecycle events carrying the task's coordinates - the script editor first (its edits/saves ARE scr work), then the transcript editor (trs), image editor, and beat board. Emit through the same event service the cookbook phases use, with recipe/phase values the #3789 vocabulary already maps (a script 'publish/commit' -> publish phase = a dot; an editing session -> a phase the vocabulary can class as artist activity WITHOUT faking a publish - possibly a new 'edit' phase mapped to the worked-span, not dots).
RULE: never synthesize marks from UI chatter - an edit session is activity, not availability. The dot stays reserved for version/publish acts. Design the 'edit' phase's mark with Tom before wiring more than the script editor.
Grounded: the scr lane will light up between its Started diamond and its publishes the day this lands.
--- AMENDED 2026-08-09 ---
NAMING (Tom, 2026-08-09): we usually know the TRIGGER SURFACE the button was fired from (menu, shelf, context-menu) - carry it on the event and label accordingly: 'Menu Button', 'Shelf Button', etc. Fall back to 'Artist UI Button' only when the surface is unknown.
#3795AI generation emits lifecycle telemetry — style frames and Seedance videos carry their taskNew featureTelemetry TruthImage and video generationclaude
·Service · ai·Alchemy·3 months ago
Tom, auditing the theaxe lane: 'i don't think we're tracking style frame generation as telemetry OR seedance video generation with our transcript tool.' Verified against loj's stream: zero events match style/seedance/nano generation - the days of theaxe work generating style frames and videos are invisible to the timeline. (His Premiere renders DID land: 4 premiere/render events on 08-05, already machine ticks.)
BUILD at the SERVICE layer - one choke point, not per-callsite: the AI services framework (alc/services/ai - nano banana style frames, Seedance video) logs lifecycle events through the same event service the cookbook phases use, carrying the task coordinates of the entity being generated FOR (the transcript tool's previz media generation knows its task; style frame runs know theirs).
MAPPING (the #3789 two-actor model, no new classes needed):
- the generation REQUEST (the artist's prompt/button - deliberate intent) -> an artist-side event;
- the generation RUN (seconds-to-minutes of API/GPU time) -> MACHINE phase with duration -> ticks on the lane, exactly like renders: Alchemy working for the artist, the leverage made visible;
- a generated artifact that gets COMMITTED to a version stays a dot via the existing version/publish path - generation itself never fakes availability.
ACCEPTANCE: regenerate a style frame + one Seedance clip on any task; both appear as machine ticks on that task's lane within the session; the theaxe-shaped hole cannot recur.
--- AMENDED 2026-08-09 ---
NAMING (Tom, 2026-08-09): the lane must distinguish the two kinds of generation, not lump them. Label image/style-frame runs 'Image Generation' and Seedance runs 'Video Generation' - both descriptive enough that AI authorship is obvious without saying 'AI'. Emit distinct event names so the machine-tick tooltip says which.
#3796Version events log a constant from_version and no to_version - the version story is uncountableBug fixTelemetry TruthCreative Timelineclaude
·Service · code_activity·3 months ago
Found wiring the Task grid's Versions column (#3792): every version|create event on loj carries details.from_version='000.000' and comment='New Version Auto-Created with Alchemy' - fourteen version-ups on videos_theaxe_seq_prv all claim the same origin, and none records the RESULTING version. Consequences: the grid can count version-ups but cannot say 'now at v013' (the display refuses to guess and shows nothing); dot labels lose the artist's actual comment when the auto-comment overwrites it.
FIX at the event-emission site (the version service / commit_version path): log the real from_version and the resulting to_version, and pass the artist's comment through when one was given (the auto-comment only as fallback). The Task grid's tooltip already knows how to render 'now at vNNN' the moment the data is real.
--- AMENDED 2026-08-09 ---
Tom: 'versions are tracked in the version msds - so i'm not sure if we pay attention to them currently in telemetry data or not.' We don't - the grid counts version EVENTS only. That reframes the fix as a two-source design:
- VERSION MSDs are the AUTHORITY: real version numbers, artist comments, timestamps - the same records the version view reads. The Task grid's Versions column and the timeline's dot labels should JOIN the version msd registry per task (version identity from the authority), which fixes 'now at vNNN' and real comments TODAY without touching event emission.
- EVENTS remain the TIMING source (the scanner is already warm; dots need timestamps in the same stream as everything else) - and their payload should still be fixed (real from/to, comment pass-through) so the two sources reconcile instead of one papering over the other.
Preferred build order: MSD join first (display truth), payload fix second (stream truth), then a reconciliation check - a version in the msd with no event, or vice versa, is a tracking hole worth a dim flag on the row.
#3797Task grid: Touched — last disk modification under the task's version treesNew featureCreative TimelineCreative Timelineclaude
·App · analytics·3 months ago
Tom: 'the only thing we're missing is a last-modified date stamp - the best way to see that there is still activity in some of these tasks that appear to be dead the moment they are created.'
THE THIRD SOURCE: events say when tracked things happened; version MSDs say what exists; the FILESYSTEM says when anything was touched - including everything untracked (Premiere auto-saves, in-place .blend saves, style frames dropped in by hand). A 'Touched' column = max file mtime under the task's VERSIONS/source and VERSIONS/render trees.
- AMBER RULE: Touched newer than the task's last telemetry event = activity the tracking missed - exactly the audit signal (the scr task reads dead; its folders don't).
- Tooltip: which side (source/render) and the file that moved.
- Mechanism v1: live walk with a short TTL cache (loj-scale is fine; #3791's memoization covers growth). FOLLOW-ON per Tom: persist the stamp into the version_msd for source_files and render_files at publish/scan time so big render trees never need walking at request time.
- Task dirs resolve from the disk itself (VERSIONS/{side}/{tree}/{entity}/{group}/{task}), ids built the same way task_id builds them - no fragile parsing of event ids.
--- AMENDED 2026-08-09 ---
DECIDED (Tom): NO STAMPING for now - 'is it even worth stamping if we will likely refresh every time we load this anyway?' Correct: a version_msd stamp is a CACHE of the walk, and while the walk is cheap and runs per page load, the stamp has no reader. Ship derived-live (walk on Tasks-view render, 60s memo, nothing written anywhere). Revisit stamping only when (a) the walk measurably slows the page (frame-sequence trees at scale - #3791 will notice first) or (b) a second consumer (version view, tracker, reports) wants the value without walking.
#3804i want to be able to filter by multiple key words - for example: theaxe, coffee - and have data containing either of those show up in the Tasks Timeline - this allows me to show when certain artistic tasks hitNew featureMulti-Keyword FilterTom
·App · analytics·Alchemy·3 months ago
#3805we need a timeline label above the timeline and move the task grid label above the task grid on analytics>tasks tab. remove ALL the text except the labels - all of the other text is distracting.New featureTask Timeline LabelingTom
·App · analytics·Alchemy·3 months ago
#3819Every timeline lane needs a spine - no-publish lanes render as a missing lineNew featureLane Spineclaude
·Alchemy·3 months ago
In the shared timeline engine the solid vertical line is drawn ONLY between the first and last publish dot (app/tools/timeline.py:457-462; Lane.start/Lane.end are dots[0]/dots[-1]). A lane with zero publishes - or exactly one, where end > start is False - gets no line at all. Give EVERY lane a continuous spine from its begin diamond to its last activity (last dot, milestone or machine tick), with the published span keeping the current solid emphasis and the unpublished remainder drawn dim/dashed. 'Worked but never published' must read as a state, not as a gap: today absence-of-line and a rendering error look identical.
#3838Joined Timeline: creative and code on one date axis, with a version-scoped phase log per laneNew featureJoined Timelineclaude
·App · analytics·3 months ago
One 'Timeline' link in the left sidebar showing the project's creative lanes and the company's code/feature lanes side by side on ONE shared date axis, so equal height means the same day. Each side filters on its own vocabulary (creative: task keywords + artist; code: capability/ticket/commit terms) under one shared date window, so a reader can scope to the context they care about. A draggable divider splits the two panes; the panes scroll vertically in lockstep and the axis REFITS to the widths the reader gives them (a squeezed pane buys narrower columns with a taller axis, priced so it never returns a worse layout than the page opened with). Each creative lane carries its published task thumbnail as an avatar inside its card; hovering a card opens a peek with the frame at 5x, the Start/Build/Render/Review/Publish log for the LATEST VERSION (with elapsed time centred between phases), and a link into that asset's version view.
#3840Register 3ds Max as a bridged DCC (software id: max)New featureMax · initial plugin build3ds Max dispatchclaude
·Plugin · smart_task·3ds Max·3 months ago
Add the `max` DCC to every registry that gates dispatch, so a max op can be addressed and routed before any max code exists. (1) alc/plugins/smart_task/executor.py: _DCC_PROBE['max'] = 'pymxs', and a _client_for branch importing alc.services.ai.max_bridge.client.get_client. (2) alc/cookbook/constants.py: add 'max' to SOFTWARE. (3) alc/plugins/blender/tasks/tests/test_parity.py: add 'max' to _DCCS so the contract-parity check covers it as soon as verb modules land (it stays quiet for not-yet-implemented tasks). (4) Ship a software icon (png + svg) under app/static/icons/software/ and point the registry entry at it. Done when: `default_executor('max')` returns a BridgeExecutor in the app venv and an InProcessExecutor where pymxs imports.
#3841Register 3ds Max in the cookbook (software.json + max/ package skeleton)New featureMax · initial plugin build3ds Max cookbookclaude
·3ds Max·3 months ago
Give the cookbook a `max` software so recipes can be authored against it. (1) config_files/software.json: add a 'max' entry alongside 'maya' — label '3ds Max', category '3d', website autodesk.com/products/3ds-max, alchemy_plugin true, api {language: python, available: true}, ext '.max', source_extensions ['.max'], svg_icons_supported false. (2) Add 'max' to active_tools. (3) Create the cookbook/max/ package: __init__.py plus empty start/, build/, render/, review/, publish/, menus/, tasks/ dirs and their .cgl files, mirroring cookbook/maya/'s layout. (4) Add a 'max' stanza to registry.json (empty tasks map — the phase tickets fill it). Done when: CFG.software_registry.software['max'] loads and CFG.get_software_cookbooks() includes max without error.
#3842alc/plugins/max package skeleton — lazy __init__ and the Scene gatewayNew featureMax · initial plugin build3ds Max dispatchclaude
·3ds Max·3 months ago
Stand up alc/plugins/max/ following the conventions in alc/plugins/CLAUDE.md. (1) __init__.py: lazy __getattr__ that resolves 'Scene' from alc.plugins.max.alchemy and the verb/setup submodules on demand — importing the package must stay pydantic-only so the etymology trampoline never drags pymxs or the config stack in. Mirror alc/plugins/blender/__init__.py. (2) alchemy.py: class Scene(SmartScene) with software='max'. Resolve a passed filepath through PathObject.path_at(set_filename=True, ext='.max'); with no filepath, _get_current_path() asks the open Max via the plugin query op. Mirror alc/plugins/blender/alchemy.py. (3) tasks/__init__.py so get_task('max', ...) can resolve. Done when: `import alc.plugins.max` in the app venv imports nothing but pydantic, and alc.plugins.max.Scene resolves.
#3843Max(LocalSoftwareVersion) — resolve the 3ds Max install and bootstrap itNew featureMax · initial plugin build3ds Max setupclaude
·3ds Max·3 months ago
Write alc/plugins/max/setup/__init__.py with class Max(LocalSoftwareVersion), id='max', id_label='3ds Max'. Implement get_executable_path (3dsmax.exe), get_python_path (Python/python.exe under the install root), get_cli_path, get_render_exe (3dsmaxcmd.exe), get_batch_exe (3dsmaxbatch.exe), get_local_versions (scan Program Files/Autodesk for '3ds Max <year>'), get_startup_scripts_folder, detect(), and a module-level setup(). FIRST STEP: read the 2027 bundle's actual Python and PySide versions off disk — that decides python_version and which wheels the site-packages bundle needs — and record both 2025 and 2027 in a _PYTHON_BY_VERSION map the way Maya's setup does. detect() picks the NEWEST installed version; nothing downstream may hardcode a year. Add setup/requirements.txt (pydantic and the rest of what alc needs in-session) so the inherited install_requirements() targets the cookbook-scoped bundle at <cookbook_root>/../plugins/max<version>/site-packages. Done when: `python -m alc.plugins.max.setup` prints correct exe/python/render/batch paths for BOTH installed versions, and setup() populates the right site-packages bundle for the detected one.
#3844Deploy the 3ds Max startup script (sys.path + bridge + menus)New featureMax · initial plugin build3ds Max setupclaude
·3ds Max·3 months ago
Write alc/plugins/max/setup/userSetup.py and the startup hook that runs it, so a launched Max can `import alc.*`. It must: put the version-scoped bundled site-packages, the cookbook root and the alchemy repo on sys.path; start the in-Max bridge server (port 20263); then build the Alchemy menu — each step guarded so a failure never blocks the ones after it. Deployment goes through the inherited LocalSoftwareVersion.copy_user_python() into the folder returned by get_startup_scripts_folder(). Because 3ds Max auto-runs MAXScript from its startup dirs, ship a tiny .ms shim next to it that calls python.ExecuteFile on the .py — confirm empirically whether a bare .py in scripts/startup/ is executed on 2025 and drop the shim if it is. Done when: a cold Max launch prints the Alchemy path banner and `import alc` works in the MAXScript/Python listener.
#3845Run a 3ds Max recipe headlessly (3dsmaxbatch) or in GUI when a step needs a viewportNew featureMax · initial plugin build3ds Max headlessclaude
·3ds Max·3 months ago
Add requires_display(recipe_path) and run_recipe(source_file, recipe_path) to alc/plugins/max/setup, plus the module-level run_recipe() entry point that SmartScene._run_recipe and apply_recipe_step call. requires_display parses the recipe json and returns True if any step declares requires_display. When False, run headless through 3dsmaxbatch.exe; when True, launch the full 3dsmax.exe so turntable/preview steps get a real viewport, then quit. Either path injects the alc sys.path entries into the bootstrapper (the spawned Max cannot import alc to find them itself), opens source_file, and executes the recipe via alc.cookbook.preflight.cli_runner.run_preflights. Stream child output live and return True on exit code 0. Done when: a max recipe runs end to end from the app venv in both modes.
#3846Batch-render a 3ds Max scene via 3dsmaxcmd (farm-submittable)New featureMax · initial plugin build3ds Max headlessclaude
·3ds Max·3 months ago
Add Max.render_scene(source_file, *, start, end, camera=None, output_dir=None, image_prefix=None, renderer='arnold', width=None, height=None, image_format='exr') plus the module-level render_scene() entry, so a render preflight step can hand it to deadline_submit and a farm node can import and call it. Formulate the 3dsmaxcmd.exe invocation with range/camera/output/format as CLI flags so they override the scene's own settings, and return True on exit 0. Done when: a local render writes a 4-padded frame sequence to the version's render dir, and the same callable submits to Deadline.
#3847In-Max bridge server — socket on 20263, exec on the Qt main threadNew featureMax · initial plugin build3ds Max bridgeclaude
·3ds Max·3 months ago
Write alc/services/ai/max_bridge/max_plugin.py: a socket server that runs inside 3ds Max and speaks the same length-prefixed protocol as the Substance bridge — recv [uint32 BE length][utf-8 code], send [uint32 BE length][utf-8 json] carrying {ok, result|error}. The code snippet assigns a JSON-serialisable `result`. pymxs is NOT thread-safe, so the socket thread must marshal execution onto Max's Qt main thread and block for the result. Started by the startup script from B2. Done when: an execute() from the app venv runs a pymxs snippet in an open Max and returns its dict.
#3848max_bridge client — execute(code) -> dict from the app venvNew featureMax · initial plugin build3ds Max bridgeclaude
·3ds Max·3 months ago
Write alc/services/ai/max_bridge/client.py with class MaxBridgeClient exposing execute(code) -> dict and a get_client() factory, matching the contract every other bridge honours so the etymology trampoline drives Max unchanged. Host/port default to 127.0.0.1:20263 and are overridable via ALC_MAX_BRIDGE_HOST / ALC_MAX_BRIDGE_PORT. Raise MaxBridgeError with actionable text when the socket is unreachable — name the address AND what the user should check (Max open, Alchemy startup script loaded). Generous timeout: exports and bakes are slow. Done when: BridgeExecutor(software='max').run(...) trampolines a real op into an open Max.
#3849Prove the 3ds Max bridge against a live session, not just a live socketNew featureMax · initial plugin build3ds Max bridgeclaude
·3ds Max·3 months ago
Add a verification harness that proves a trampolined op actually MUTATED an open 3ds Max — create an object through the bridge, then re-query the scene through a separate call and assert it is there, and assert the change survives (no implicit undo/rollback). Cover the failure modes explicitly: Max not running, startup script not loaded, op raising inside Max (error must come back as a dict, not a hang). Land it as a runnable script plus a short section in the docs saying how to verify the bridge before trusting it. Done when: the harness passes against a real open Max and fails loudly with each failure mode simulated.
#38503ds Max common task — CommonCreate / CommonEdit / CommonQuery in pymxsNew featureMax · initial plugin build3ds Max tasksclaude
·3ds Max·3 months ago
Implement alc/plugins/max/tasks/common/{create,edit,query}.py plus __init__.py (one SmartTask subclass declaring software='max', task='common'), covering every op in the contract at alc/plugins/smart_task/common.py: create — add_primitive, add_light; edit — delete, rename, transform, select, deselect, set_frame_range, set_environment, save_file; query — list_objects, get_object, exists, get_selection, get_frame_range, get_environment. Verb modules are flat functions, name-in / dict-out (objects addressed by string name, every op returns a JSON-serialisable dict) — never a live pymxs handle, which cannot cross the socket. Done when: the parity test case max:common/*.py passes for all three ABCs.
#38513ds Max model task — ModelCreate / ModelEdit / ModelQuery in pymxsNew featureMax · initial plugin build3ds Max tasksclaudeModel
·3ds Max·3 months ago
Implement alc/plugins/max/tasks/mdl/{create,edit,query,models}.py plus __init__.py with class Task(SmartTask), software='max', task='mdl', msd_data_model=MsdData, covering every op in alc/plugins/smart_task/mdl.py: create — import_file, export_file; edit — duplicate, decimate, voxel_remesh, reduce_polycount, scale_to, ground, place, freeze_transforms, pivots_to_origin; query — get_msd_data, summary, polycount, bounds. models.py carries the max MsdData payload (geometry entries, bbox, materials) so publish can validate it. export_file must cover the pipeline formats the mdl publish needs: .max, .fbx, .obj, .usd, .glb. Done when: the parity test cases max:mdl/*.py pass, and each op round-trips through the bridge against a real scene.
#38523ds Max: start phase for the model taskNew featureMax · initial plugin build3ds Max model pipelineclaudeModel
·3ds Max·3 months ago
Author cookbook/max/start/mdl/ — the recipe that turns an empty Max session into a correctly-configured model scene at a PathObject coordinate. Steps: set system units and display units to the studio convention (a Max-specific hazard other DCCs do not have), set the frame range, and save the scene to the coordinate's .max path. Register it in registry.json under max/mdl/phases/start. Each step is a PreflightCheck subclass in its own .py, self-describing (name, recipe_type, label, step_type, software, order), with the recipe json carrying goal / pass_criteria / fail_criteria and the min/max hour estimates. Done when: Run All on the start phase produces a saved, correctly-scaled .max at the version coordinate.
#38533ds Max: build phase for the model taskNew featureMax · initial plugin build3ds Max model pipelineclaudeModel
·3ds Max·3 months ago
Author cookbook/max/build/mdl/ — the model build recipe, porting the Blender one step for step: CreateScaleCube (cube at the asset's height from the msd), ImportDesignViews (front ortho from the dsn msd as a reference plane), ImportDesignGlb (design mesh into a hidden DSN group as the UV/silhouette source, not the hero mesh), CreateProxyModel (voxel remesh then decimate to clean low density), TransferUVsToLo (UVs from the design mesh onto the proxy), PlaceGeometry (scale to canonical height, ground at Z=0), EnsureModelGroup (the mdl/hi grouping, DSN hidden). Register under max/mdl/phases/build in registry.json. Done when: Run All on build produces a grounded, UV'd, correctly-scaled hero mesh in the mdl/hi group for a real existing asset.
#38543ds Max: render phase for the model task (turntable preview)New featureMax · initial plugin build3ds Max model pipelineclaudeModel
·3ds Max·3 months ago
Author cookbook/max/render/mdl/ — the model preview render: build a turntable, capture the viewport to an image sequence in the version's render dir, and clean the turntable rig back off the model. The capture step must declare requires_display: true so run_recipe (B3) launches a GUI Max instead of 3dsmaxbatch. Register under max/mdl/phases/render. Done when: Run All on render writes a turntable sequence for a real asset version.
#38553ds Max: review phase for the model taskChoreMax · initial plugin build3ds Max model pipelineclaudeModel
·3ds Max·3 months ago
Author cookbook/max/review/mdl/ — fold the render output into reviewable media: convert the turntable sequence to an mp4 proxy plus a thumbnail through the OIIO media service, attach them to the version's .preview/.thumb/.proxy paths on the PathObject, and flag the version for review. Register under max/mdl/phases/review. Done when: after render, the version shows a playable turntable and a thumbnail in the web app's review surface.
#38563ds Max: publish phase for the model taskNew featureMax · initial plugin build3ds Max model pipelineclaudeModel
·3ds Max·3 months ago
Author cookbook/max/publish/mdl/ — the publish recipe, porting the Blender one: CleanTt (strip the turntable rig), PivotsToOrigin, DeleteHistory (collapse the modifier stack), FreezeTransformations, EnsureMaterialGroups (guarantee the {material}_mtl convention, wrapping a groupless mesh in a default_mtl + clay material), SaveScene, ExportModelFileFormats (.max whole-file plus .fbx/.obj/.usd/.glb of the mdl group into the version's render dir), GenerateSceneDescription (persist the scene description onto the version msd), AddDescription (from shared_data, no dialog — must stay headless/farm safe), and Publish (delegating to PathObject.publish()). Register under max/mdl/phases/publish. Done when: a published max model version lands at publish/latest with its formats and msd payload, and downstream (tex/ldv) can consume it.
#38573ds Max artist UI — cookbook-driven menu and tool launchersNew featureMax · initial plugin build3ds Max artist UIclaude
·3ds Max·3 months ago
Give the artist the Alchemy entry points inside Max. (1) alc/plugins/max/custom_menu.py: build the Alchemy menu from the cookbook's max/menus/* recipes, the way alc/plugins/blender/custom_menu.py and alc/plugins/unreal/custom_menu.py do — buttons are cookbook entries, so adding one is a cookbook edit, not a code change. Built by the startup script from B2, and rebuildable in-session without a restart. (2) alc/plugins/max/tools/launchers.py (a tools/ PACKAGE, never tools.py): thin launchers for the universal Alchemy tool UIs, parenting each widget to Max's main window and holding a Python ref so PySide does not GC it. Done when: an artist opens Max and can run the mdl build and publish phases from the Alchemy menu.
#3858Prove the 3ds Max model pipeline end to end on a real assetNew featureMax · initial plugin build3ds Max PipelineclaudeModel
·Docs·3ds Max·3 months ago
Run the whole spine — start, build, render, review, publish — for one real existing asset in the current project, through the artist UI and through run_task, and fix whatever the run exposes. Then write it down: add the max row to docs/architecture/plugins.md, and distil the sequence actually followed here into a 'porting a DCC to Alchemy' checklist under docs/guides/ — the registries to touch, the setup module's surface, the bridge contract, the contract-parity gate, the five phases, and the artist UI. Done when: a published max model version exists for a real asset, and the checklist is specific enough that the next DCC does not require re-deriving any of this.
#3884Max transform() rotates objects about the world origin and inverts the euler signBug fixMax · initial plugin build3ds Max tasksclaude
·3ds Max·3 months ago
transform() in alc/plugins/max/tasks/common/edit.py corrupts the object when a rotation is passed. Two defects, both confirmed against a live 3ds Max 2027 session: (1) setting rotation MOVES the object - it is rotated about the world origin instead of about its own pivot, so a pure rotation call with no location argument displaces the node; (2) the euler reads back with the opposite sign to the one it was set with. Fix set_euler_radians / euler_radians in alc/plugins/max/tasks/_mx.py so that setting a rotation leaves position untouched and round-trips sign-accurately, then add a live-session check that asserts both.
#3885Headless glb-to-fbx conversion op so Max can import the design model without a glTF pluginNew featureMax · initial plugin build3ds Max model pipelineclaude
·Service · mesh·Alchemy·3 months ago
Add a headless mesh-format conversion op so a DCC can consume geometry it cannot natively read, and so the max mdl build recipe can import the design model without a glTF plugin. Drive it with Blender in background mode - Blender51.get_executable_path() already resolves blender.exe through LocalSoftwareVersion, so no new dependency and it runs on a farm node. Surface: convert(source_path, dest_path, ...) that imports the source and writes the dest, choosing importer/exporter from the extensions, returning a dict naming what it read, what it wrote, and the axis/unit conversion it applied. Primary case is .glb to .fbx for 3ds Max. Idempotent - a dest newer than its source is a cache hit, matching how image_to_mesh handles this.
#3893MCP server core: JSON-RPC lifecycle and pluggable transport, Blender firstNew featureMCP ComplianceMCP Serverclaude
·Blender·2 months ago
Stand up alc/services/mcp/ as a spec-compliant MCP server exposing the Blender bridge over stdio. Add the 'mcp' Python SDK (FastMCP) to requirements.txt rather than hand-rolling JSON-RPC 2.0. The server must implement the initialize/initialized handshake with protocolVersion and capability negotiation, tools/list, and tools/call. tools/list is generated, not hand-written: it comes from build_tools() over the smart_task contract ABCs (70 tools today: common 16, mdl 15, prv 11, cam 8, ldv 8, tex 7, lay 5), renaming input_schema to MCP's inputSchema. tools/call routes through dispatch_op(software='blender'), so the real etymology op runs in the open Blender via the existing bridge socket. One server instance per connected DCC (server key alchemy-blender) rather than one server with a software argument, because a session drives one open DCC and 70 tools x 5 DCCs is a surface no model handles well. The raw length-prefixed bridge socket stays exactly as-is: MCP is a new northbound face, not a replacement for the southbound protocol.
--- AMENDED 2026-08-11 ---
Transport is NOT stdio-only across the sprint. The Python DCC bridges (Blender, Maya, Unreal, Substance, Max) use stdio: the client spawns the server as a subprocess, which then opens a socket to the running DCC. The Adobe bridges (Premiere #3906, After Effects #3907) already run FastAPI + uvicorn apps that own their panel WebSocket, so they serve MCP over Streamable HTTP mounted on that existing app -- one process, one hop, no subprocess. Both are first-class transports in the spec. Build this core so the protocol layer, the generated tool surface and the result envelope are shared, and TRANSPORT is the pluggable part -- do not bake a stdio assumption into anything above the transport boundary. Blender is still the implementation target for this ticket; the requirement here is only that adding the HTTP transport later is a new adapter, not a rewrite.
#3894Lift the generated tool surface out of blender_bridge into a DCC-neutral moduleChoreMCP ComplianceMCP Serverclaude
·Plugin · smart_task·Alchemy·2 months ago
build_tools() and dispatch_op() in alc/services/ai/blender_bridge/agent_tools.py are not Blender-specific: they generate tools from the smart_task contract ABCs and dispatch through SmartTask with a software= parameter. Move them to a DCC-neutral home so the MCP server and the in-app chat agent consume one surface instead of forking. Keep DEFAULT_TASKS and the ALC_BLENDER_AGENT_TASKS override working (add a neutral env name, keep the old one as an alias). After the move, alc/services/ai/blender_bridge/agent.py imports from the new location and behaves identically; its tool list and dispatch must be unchanged. Cross-DCC generation must be exercised: assert build_tools() + dispatch_op(software='maya') resolves without importing any DCC API in the app venv.
#3895MCP tool-result envelope and bridge error mappingNew featureMCP ComplianceMCP Serverclaude
·Blender·2 months ago
Etymology ops return a bare JSON-serialisable dict (the name-in/dict-out contract). MCP tools/call must return content: [{type: 'text', text: ...}] with an isError flag. Add the adapter, and map bridge failures correctly: a BridgeError (DCC unreachable, or the op raised inside the DCC) must come back as a normal result with isError true and the message as content, NOT as a JSON-RPC error. JSON-RPC errors stay reserved for protocol faults (unknown method, malformed params, unknown tool name). This matters because the client-unreachable text in the bridge clients is already good agent-facing recovery guidance -- e.g. MaxBridgeClient tells you to check that the DCC is open and that the startup script loaded the plugin. Surfaced as tool content, a model can act on it; raised as a protocol error, it kills the turn.
The DCC bridges exec on the host application's main thread because the DCC APIs are not thread-safe -- the Max plugin explicitly marshals to Max's Qt main thread for pymxs. An MCP client is free to issue parallel tools/call requests, and the in-app chat agent offloads blocking bridge calls to asyncio.to_thread, so two calls can be in flight at once. Add a serializing lock (or a single-consumer queue) per bridge target so calls to one DCC execute one at a time, with the wait bounded and a clear timeout message rather than a silent hang. Interleaving must be impossible even when the MCP server and the in-app chat agent are both driving the same open Blender. Cover it with a test that fires concurrent dispatches at a fake executor and asserts strict serialization.
#3897Project and shot context for an MCP sessionNew featureMCP ComplianceMCP Serverclaude
·Blender·2 months ago
SmartTask needs production context (company, project, shot/asset, task) to resolve anything, and the MCP protocol has no built-in slot for it. Give the server an explicit context model: read the default from the environment at server start (one server instance per open DCC, so the context matches what the artist has open), and expose a query_context tool so a model can read what it is pointed at before it acts. Getting the context wrong must be a hard error naming what was missing -- never a silent fallback to a default project, which would write into the wrong show. Follow-up option, not in scope here: expose context as a readable MCP resource once #3898 lands.
#3898Expose PathObject addresses as MCP resourcesNew featureMCP ComplianceMCP Resourcesclaude
·Alchemy·2 months ago
Implement the MCP resources primitive: resources/list and resources/read, so an MCP client can READ the production graph rather than only act on it. PathObject addresses are already URIs in all but syntax -- give them a scheme (alchemy://<shot>/<task>/<tag>, exact form to be pinned during the ticket) and a parser that maps a URI back to a resolved PathObject. Expose at minimum: a shot or asset's manifest, its published versions, and its review media (the .preview/.thumb/.proxy pointers that already hang off PathObject). Reads only -- no mutation through the resource surface. Resolution failures are hard errors naming the address that failed, never an empty result that reads as 'nothing there'.
#3899Expose cookbook recipes as MCP promptsNew featureMCP ComplianceMCP Promptsclaude
·Alchemy·2 months ago
Implement the MCP prompts primitive: prompts/list and prompts/get, generated from the cookbook's recipes rather than hand-authored. A recipe becomes a named prompt whose arguments are its required inputs, so an external MCP client gets the pipeline's own semantic arc (start -> build -> render -> review -> publish) as first-class slash commands. Generation must be driven by the cookbook, so a recipe added there shows up without editing this module. Do not duplicate recipe logic here -- a prompt describes and parameterizes the work; running it still goes through the existing run_task orchestrator path.
#3900Register Alchemy MCP servers into Claude Desktop and Claude Code configNew featureMCP ComplianceMCP Serverclaude
·Alchemy·2 months ago
alc/services/ai/comfyui_bridge/claude_desktop/config.py already does idempotent, non-destructive merges into both claude_desktop_config.json and ~/.claude/settings.json, with a timestamped backup before any write and atomic replace. Generalize its _build_server_entry so it can register Alchemy's OWN MCP servers (server key alchemy-blender, and the same shape for other DCCs later) alongside the third-party ones it registers today, pointing at the venv python and the server module from #3893. Keep the existing comfyui and blender-mcp registration behaviour byte-identical -- this is an extension, not a rewrite. Registering twice must be a no-op that reports 'already up-to-date' rather than rewriting the file.
--- AMENDED 2026-08-11 ---
Two entry SHAPES are needed, not one. A stdio server registers as command + args + env (the pattern _build_server_entry already produces for comfyui-mcp: node plus a dist path). A Streamable HTTP server -- Premiere #3906 and After Effects #3907, which serve MCP from their own already-running FastAPI apps on 20261 and 20262 -- registers as a URL instead, with no command to spawn. Support both, and make it explicit in the entry builder which shape a given server takes rather than inferring it. Also worth surfacing: an HTTP-transport server is only reachable while its bridge app is running, so registration succeeding does not mean the server is up -- do not imply otherwise in whatever the CLI or UI reports back.
#3901Second DCC on MCP: stand up the Maya server and prove the cross-DCC claimNew featureMCP ComplianceMCP Serverclaude
·Maya·2 months ago
Stand up alchemy-maya as a second MCP server instance and confirm the cost of an additional DCC is close to zero. Expected shape: same server module from #3893, same generated tool surface, dispatch_op(software='maya'), a different bridge client and port. The point of the ticket is to find whatever is accidentally Blender-shaped in #3893 and fix it there rather than forking a Maya copy -- if this ticket ends up adding a Maya-specific server module, the abstraction in #3893 was wrong. Tool generation must reflect Maya's ACTUAL task coverage rather than advertising ops the DCC cannot serve: assert the generated list against the existing cross-DCC parity check.
#3902Unreal MCP server: the full contracted surfaceNew featureMCP ComplianceMCP Serverclaude
·Unreal Engine·2 months ago
Stand up alchemy-unreal as an MCP server instance. Unreal is the widest surface in the repo: all 7 contracted tasks are implemented (cam, common, lay, ldv, mdl, prv, tex), so the generated tool list is the FULL ~70 and this is the strongest conformance target after Blender. Same server module, same generated surface, dispatch_op(software='unreal'), the existing unreal bridge client. Two tasks -- rig and mtn -- are implemented in Unreal but have NO contract ABC in smart_task, so build_tools() cannot see them; they are out of scope here and tracked as the contract gap, do not hand-write tools for them. Verify against a running editor, not just the app venv: headless Unreal runs with -game and the bridge has its own launch path.
#3903Substance MCP server: the narrow single-task caseNew featureMCP ComplianceMCP Serverclaude
·2 months ago
Stand up alchemy-substance as an MCP server instance. Substance implements exactly ONE contracted task (tex), so the generated tool list is roughly 7 tools out of the 70 -- the narrowest DCC in the repo, and therefore the case that proves the server advertises a DCC's ACTUAL coverage rather than the full contract surface. A client connected to alchemy-substance must not see mdl or cam tools it cannot call. Assert this explicitly with a test: the generated list for software='substance' contains only tex ops. Same server module, dispatch_op(software='substance'), the existing Painter socket plugin on 20260.
#39043ds Max MCP serverNew featureMCP ComplianceMCP Serverclaude
·3ds Max·2 months ago
Stand up alchemy-max as an MCP server instance. Max implements 2 contracted tasks (common, mdl), so the generated list is roughly 31 tools. Same server module, dispatch_op(software='max'), the existing MaxBridgeClient on 20263. Two Max-specific things to get right: (1) the main-thread constraint is hardest here -- max_plugin.py marshals every exec onto Max's Qt main thread because pymxs is not thread-safe, and the client carries a deliberately generous 600s timeout because FBX/USD/Alembic exports and ProOptimizer decimation block that thread for minutes on dense meshes; the serialization work in #3896 must not replace that timeout with a shorter default. (2) The bridge's unreachable message tells the artist to run start_plugin() from Max's Scripting > Python console -- that text must survive into the MCP tool-result envelope per #3895.
#3905Query-verb contract ABCs for the editorial tasks (amc, edt, vid)New featureMCP ComplianceMCP Serverclaude
·Plugin · smart_task·Alchemy·2 months ago
build_tools() generates its tool surface from _CONTRACTS, which today holds only common, mdl, cam, tex, ldv, lay and prv. Premiere's tasks (amc, edt) and After Effects' task (vid) have NO contract ABC, so both DCCs generate exactly ZERO tools -- an MCP server for either would be spec-compliant but useless. Author contract ABCs for amc, edt and vid covering the QUERY verb ONLY. Query-only is the deliberate scope: it is what an MCP client most needs first (what sequence or comp is open, what is on the timeline, what is the cut length), it is read-only so nothing can mutate a live edit, and it is a fraction of the modeling risk of a full three-verb contract for three single-DCC tasks. Derive the ABCs FROM the shipped query implementations rather than designing fresh ops -- this makes existing behaviour addressable, it does not change what Premiere and AE can do. Register the entries in _CONTRACTS and confirm the parity test still passes for every DCC. build_tools() must then produce a non-empty query-only list for software='premiere' and software='after_effects'.
#3909Contract ABCs for rig and mtn so they generate toolsNew featureMCP ComplianceMCP Serverclaude
·Plugin · smart_task·Alchemy·2 months ago
rig and mtn are implemented in Blender and Unreal (alc/plugins/blender/tasks/, alc/plugins/unreal/tasks/) but have no contract ABC in _CONTRACTS, so build_tools() cannot see them and no MCP server can expose rigging or motion ops. Author contracts for both, derived from the shipped implementations rather than designed fresh, and register them in _CONTRACTS. Unlike the editorial tasks in #3905 these ARE multi-DCC, so the cross-DCC parity check applies in full and will surface any signature drift between the Blender and Unreal implementations -- expect that to be the bulk of the work. Where the two genuinely diverge, the contract records the shared shape and the divergence gets flagged, not papered over.
#3912Preflight shared_data does not reach later steps, and failed steps report as passedBug fixMax Sprint Retrospectiveclaude
·Cookbook engine·Alchemy·3 months ago
run_preflight sets PreflightCheck.shared_data as a CLASS attribute once before the loop, then instantiates each step with klass(). Steps write to self.shared_data, so what one step stashes does not reach the next - the recipe runs as a sequence of isolated steps rather than a pipeline. Make shared_data genuinely shared across the run. SECOND, and worse: the runner printed the success banner after three steps had FAILED. It checks `if not inst.status`, but StepStatus.FAILED is a truthy string, so a failed step reads as passing. A recipe must not report all checks passed when steps failed.
#3913run_preflight reads the legacy .cgl, so inherited phases cannot run and drift goes unnoticedBug fixMax Sprint Retrospectiveclaude
·Cookbook engine·Alchemy·3 months ago
run_preflight resolves a recipe by reading the legacy {phase}.cgl aggregate, NOT through Recipe.resolve and not from the per-recipe json. Two consequences. (1) The fallback chain does not apply at RUN time: Recipe.resolve(max, review, mdl) correctly returns alchemy/review/default, but run_preflight(dcc=max, task_phase=review, task=mdl) raises RuntimeError: No preflight mdl for max/review. So a DCC that legitimately inherits a phase cannot RUN it. (2) The .cgl aggregates have drifted from the recipe jsons they duplicate. Make run_preflight the same resolution the rest of the system uses, or regenerate the .cgl files from the jsons and keep them in step.
#3917CustomMenu.set_path_object calls PathObject.from_path(), which does not existBug fixMax Sprint Retrospectiveclaude
·Alchemy·3 months ago
alc/plugins/menus.py CustomMenu.set_path_object calls PathObject.from_path(), which does not exist on PathObject. Every DCC menu that hydrates a path object this way raises AttributeError, so the shared menu base is broken for all of them, not just max. The Max subclass currently guards around it. Find the intended constructor (PathObject(path) / PathObject.from_string / whatever the current API is), fix the base, and drop the per-DCC guard.
#3918Report: the complexity of standing up a full DCC leg, from the 3ds Max buildChoreMax · initial plugin build3ds Max Pipelineclaude
·Alchemy·3 months ago
Write a report that details the COMPLEXITY of what the 3ds Max plugin build actually involved - the closing artifact of the sprint. Not a changelog and not marketing: an honest account of the surface area a full DCC leg covers, aimed at making the scale of the work legible. Draw on the sprint record, which is unusually well evidenced: 22 tickets, two repos, per-ticket provenance commits, and a long tail of measured findings rather than assumptions. Cover at least: the layers a DCC leg spans (registry/dispatch, install resolution and bootstrap, an in-DCC bridge, contract task implementations, cookbook phase recipes, headless vs GUI execution, farm submission, artist UI); what had to be MEASURED against the live application rather than ported from another DCC; the defects found only by running against a real asset; and where 3ds Max genuinely differs from Blender/Maya/Unreal.
#3943path_at ignores the coordinate's own root and grounds on prod_rootBug fixMulti-Root Coordinatesclaude
·Core·2 months ago
PathObject.path_at() must ground on the instance's own root field, exactly as .path does. It excluded 'root' from the dict handed to AlchemyRouteEngine.path_from_dict, so the assembler fell back to the engine's prod_root and emitted an ABSOLUTE production path; path_at's trailing 'Path(self.root) / resolved' join then discarded the stated root entirely (joining a root onto an absolute path is a no-op). Any coordinate deliberately grounded off the live production volume - a tmp root in tests, a second studio root, a sandbox - read and WROTE on E:/Alchemy anyway, including via PathObject.versions, list_dir, io_root and msd_root. Measured 2026-08-11: a test passing root=str(tmp_path) created directories on the real E: drive. Dropping the token also starved the four routes that MATCH on root (io-ingest-assets and io-editorial on {PROD_ROOT}, unreal_disk_* on $ALC_PERFORCE_ROOT, unreal_game_* on /Game), so those could never fire through path_at and a perforce coordinate silently fell through to a baseline prod-rooted route.
#3947[cookbook] [alchemy] [edt] Implement the Generate Transcript buttonNew featurealchemyGenerate Transcriptstub
·Cookbook · alchemy · menus · edt·Alchemy·2 months ago
GIves the user a popup where they can choose the language (English is default) and generates a transcript through AWS followed by an srt transfcript for any editorial package. services found here: alc.services.ai.transcription.create.subtitles(
#3966Unreal bridge server on :20264 — replace multicast remote-exec with a dial-out WebSocket bridgeNew featureUnreal Alchemy PluginUnreal Alchemy Pluginclaude
·Unreal Engine·2 months ago
FastAPI bridge on 127.0.0.1:20264 (port family: substance 20260, premiere 20261, AE 20262, max 20263) mirroring the premiere bridge surfaces (alc/plugins/premiere/bridge/server.py): POST /exec to run Python in the editor, GET /menus + POST /menu/run for the cookbook unreal menus, GET /todos, POST /request. UE side: setup/init_unreal.py dials OUT to the bridge over a WebSocket (same direction as the UXP panel), so no multicast discovery, works over VPN, and gives the app a push channel into the editor. UnrealBridgeClient keeps its execute(code)->dict contract so the smart_task executor is untouched.
A dockable Alchemy panel built entirely from NATIVE UE UI elements — an Editor Utility Widget (UMG, rendering as real Slate) spawned as a dockable tab (EditorUtilitySubsystem.spawn_and_register_tab) from an Alchemy menu entry at editor startup. NO webview, NO HTML/CSS anywhere in this panel: it uses editor-native widgets and the editor style so it looks and behaves like a first-class UE tool, docks next to the Content Browser, and survives editor layout save/restore. The Premiere UXP panel (alc/plugins/premiere/panel/) is the UX GUIDE ONLY — its layout is what we reproduce natively: two-pane (left rail of shelves, main phase region), console strip at the bottom, request pill in the header. Structure the EUW so Python populates it with data at runtime (ListView/TreeView + dynamic entry widgets), keeping all pipeline logic in the alc Python layer.
#3968Panel rail: unreal cookbook menus as custom shelves — native UMG list, no HTMLNew featureUnreal Alchemy PluginUnreal Alchemy Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The panel left rail renders every {cookbook}/unreal/menus/* folder except alchemy as a collapsible shelf section, built from NATIVE UE widgets (UMG TreeView/ListView with expandable categories, or vertical boxes of editor-styled buttons) — no webview, no HTML/CSS. The Premiere panel rail (alc/plugins/premiere/panel/main.js) is the UX guide only: collapsible sections, buttons that run immediately, buttons with an INPUTS spec expanding an inline form (native text/combo/spin widgets generated from the spec). Because the panel is in-process, clicking a button imports and runs the cookbook module directly (reload per click, same contract as custom_menu.py) — no bridge round-trip.
#3969Panel phase widget: alchemy menu as phase bar + Art todos — native UMG, no HTMLNew featureUnreal Alchemy PluginUnreal Alchemy Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The alchemy cookbook menu promoted to a horizontal phase bar in the panel main region (Build / Art / Render / ...), built from NATIVE UE widgets — editor-styled buttons/chips in a horizontal box, active/pending/art states expressed with the editor style (brushes/colors), no webview, no HTML/CSS. The Premiere panel main region is the UX guide only: phases flagged requires_display render a proper native UI (Art = a todo list view with frame sort, type filter, add row, done toggle — UMG ListView with a custom entry widget); other phases show their goal + underlying recipe steps with a Run button. Context header shows the shot/task the open level resolves to. Phase recipes resolve unreal-first with fallback onto the shared alchemy defaults, and a fallback is surfaced visibly in the panel.
A UE-side watcher (started from init_unreal.py) publishes editor events over the bridge WebSocket — actor selection changed, level saved, level/sequence opened — and the bridge forwards them into the app event stream (/ws/agent), so the panel context header and the app follow what the artist is doing in the editor instead of the flow being app-to-editor only.
The header request field from the Premiere panel, reproduced in NATIVE UE widgets (no webview, no HTML/CSS): a collapsed chip that expands to a single-line editable text box + Enter, sitting in the panel header. Premiere panel is the UX guide only. Submitting files the card through the app request pipeline (in-process alc call or the bridge /request — whichever the request pipeline already expects), with the evidence camera photographing the Unreal Editor window (window_grab title substring) and project + coordinate resolved from the open level — one pipeline, one set of receipts.
#3972Stage 2: add a C++ module to the AlchemyUE pluginNew featureUnreal Alchemy PluginUnreal Alchemy Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Add a deliberately small C++ Module to the existing AlchemyUE plugin (#3973 scaffold — the .uplugin descriptor gains a Modules entry; content + Python stay as they are). Frozen scope, only the things Python cannot do: (1) native Slate drag-and-drop from the Alchemy panel into the viewport (drag a character/prop out of a shelf, drop it into the level); (2) the socket transport and tab/toolbar registration in native code, so the integration works without the Python editor plugin enabled (packaged/-game contexts); (3) anything the UMG-from-Python panel provably could not express. All pipeline logic stays in the Python contract layer — the C++ surface should almost never change, so the standing cost is recompile-per-engine-version (5.6/5.7/5.8), not per-feature.
#3973AlchemyUE .uplugin scaffold — proper UE plugin from day one (content + Python, no C++)New featureUnreal Alchemy PluginUnreal Alchemy Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The whole Unreal integration ships as a PROPER UE plugin from the start: an AlchemyUE plugin folder with a .uplugin descriptor, Content/ holding the panel EUW .uassets, and a Python/ folder whose init_unreal.py UE auto-runs when the plugin loads (this subsumes the current loose setup/init_unreal.py wiring). Install = drop the folder into a project Plugins/ dir (or engine plugins) and enable — no per-project setup. No C++ module yet; content+Python plugins are fully supported. The plugin bootstraps sys.path to the alc tree, registers the ToolMenus entries, spawns the dockable panel tab, and starts the bridge dial-out.
#3974Selection grounding: Content Browser selection hydrates the panel (PathObject from /Game paths)New featureUnreal Alchemy PluginUnreal Alchemy Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The panel hydrates itself from what the artist selects in the Content Browser — the pipeline builds itself contextually inside UE. Mechanism (PROVEN live 2026-08-12 against the JLT assets world): Alchemy-imported assets live at coordinate-shaped /Game paths (/Game/assets/chr/tani/rig/default/chr_tani_rig_Skeleton), and the uproject path names co/proj (parse_uproject, shipped in #3971) — so selection maps to a disk path (E:/Alchemy/{co}/{proj}/VERSIONS/source/assets/chr/tani/rig) and PathObject parses it: task=rig, task_id=jlt_assets_chr_tani_rig. Build: (1) grounding.path_object_from_package(package, co, proj) with the VERSIONS/PUBLISHES context choice made explicitly; (2) event_watcher snapshots content-browser selection (get_selected_asset_data package names) and emits asset_selected deltas; (3) the panel re-hydrates on selection: context header shows the task_id, phase widget resolves recipes for the REAL task instead of default, the Art pane loads that task todo manifest (closing the #3969 boundary), the request pill files with the exact coordinate; (4) bridge /todos grounds selection-first with level fallback.
#3975Panel layout: close the gap to the Premiere-panel targetDesignUnreal Alchemy PluginUnreal Alchemy Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Bring the native panel the rest of the way to the target layout (Tom's Premiere panel screenshot, 2026-08-12): (1) full-bleed near-black ground — the ScrollBox root hugs content so the Border frame only covers the content band; likely re-author the EUW root as Border > ScrollBox or find a ScrollBox style brush; (2) softer chip/button chrome — background_color tint keeps the default light outline; explore set_style/widget_style ButtonStyle brushes for the rounded dark look; (3) the add-a-todo input row pinned at the BOTTOM of the Art pane (arrives with the todo-write work); (4) a collapsible console strip along the panel bottom mirroring the Premiere console (run/status lines, click-to-copy); (5) green presence dot next to ALCHEMY (bridge connected state — /status is already live).
#3976MagiCam rig spec: DCC-agnostic control-rig model + Blender template extractorDesignMagiCam Control RigMagiCam Control Rigclaude
·Alchemy·2 months ago
Define ONE machine-readable spec for the MagiCam control rig that any DCC plugin can be built from and validated against. Pydantic model (suggested home: magicam repo, pure model, no bpy) covering: controls (name, semantic role, parent, custom shape ref + per-control shape offset/scale/rotation, rotation mode, per-channel locks, per-channel limits from the Limit Location/Rotation constraints), attachments (camera body parented to the tilt control; image_plane_NN mesh bound to image_plane_NN_ctrl), and declared relationships/drivers as expressions (plane autoscale width = depth * sensor_width / focal * 0.5, height = width / aspect; lens preset list 16/24/35/50/85/120/200 with the snap expression; lens HUD label visibility windows; lens_grouping Z = -(lens/sensor)+0.41). Plus a headless-Blender extractor that reads camera_rig_multiplane_simple2.blend and emits the canonical magicam_rig_spec.json, checked in next to the template. The JSON - not any .blend or .uasset - becomes the source of truth for rebuilding the rig in a new DCC and the input to the parity validator.
#3977MagiCam control shapes: curve point data IN the spec, drawn natively per DCC (no mesh export)New featureMagiCam Control RigMagiCam Control Rigclaude
·Blender·2 months ago
Store the four control shapes as curve/polyline point data inside magicam_rig_spec.json (a shapes section), not as exported mesh files. Extraction (folds into the #3976 extractor): base and dolly are flat mesh outlines (28 and 10 verts) - emit their boundary loops as closed polylines; tilt_arrow and impl are bezier curves (36-pt loop and a 4-pt rectangle frame) - emit control points with handles, plus a sampled polyline for DCCs that just want points. All points in control-local space at origin; per-control offset/scale/rotation stays on the control entries. Each DCC then DRAWS them natively: Blender = curve objects as custom bone shapes (regenerate the utils collection from spec), Maya = NURBS curves (the standard for anim controls), UE = whichever the #3978 spike picks (Control Rig shape-library mesh generated as a thin wire ribbon from the polyline, or spline/procedural-mesh components on the gizmo route). Acceptance: a script rebuilds all four shapes in a DCC from the JSON alone and they read identical to the Blender originals; no binary sidecar files, the whole rig is one diffable JSON.
#3978Spike: choose the UE vehicle for MagiCam controls - Control Rig asset vs gizmo components - via a working dolly prototypeDesignMagiCam Control RigMagiCam Control Rigclaude
·Unreal Engine·2 months ago
Prototype the dolly control (travel only along its rail, real dolly shape, keyable) far enough both ways to make the call: (a) a UE Control Rig asset + ControlRigComponent on BP_MagiCam - native support for control shapes, per-channel limits, hierarchy, viewport manipulation and Sequencer keying, driving the CineCameraComponent and planes; vs (b) plain scene components with visible static-mesh handles, per-channel clamps in the construction script, and a selection helper so clicking the handle grabs the control. Record the decision and rationale in MAGICAM_PORT.md. Acceptance: in the editor viewport you can click the dolly handle, drag it only along its rail with the standard gizmo, and key it in Sequencer.
#3979UE lens control: preset snap 16-200 + lens label HUD per the rig specNew featureMagiCam Control RigMagiCam Control Rigclaude
·Unreal Engine·2 months ago
Bring the Blender lens interaction to the UE rig: a lens_chooser control whose travel snaps focal length to the preset list (16, 24, 35, 50, 85, 120, 200), and a lens readout near the rig whose visibility windows follow the spec drivers. Must route through the existing cam.edit.set_lens / set_body verbs so the caddy, the rig control and the camera-body library stay one pipeline (a body pick from the registry still drives filmback; the chooser drives focal length).
#3980MagiCam parity validator: spec-driven conformance report for any live DCC rigNew featureMagiCam Control RigMagiCam Control Rigclaude
·Alchemy·2 months ago
A validator that loads magicam_rig_spec.json, queries a live DCC (Blender over the blender bridge, UE over unreal_bridge), and reports control-by-control conformance: control exists, parent chain matches, channel locks and limits honored, shape assigned, attachments correct (camera under tilt, planes bound to their ctrl), and declared relationships behaving (nudge a plane, its scale follows depth * sensor / focal, then restore). Output a readable per-control pass/fail table. This is the mechanism that makes improve-it-in-one-DCC-improve-it-in-all enforceable: run it against Blender first (should pass modulo the known drift), then against the UE rig as it lands, and against every future DCC port.
#3981Blender template drift: restore or drop lens_chooser and un-hardcode the 1.77 plane aspectBug fixMagiCam Control RigMagiCam Control Rigclaude
·Blender·2 months ago
Two defects found while dumping camera_rig_multiplane_simple2.blend: (1) the driver on MagiControlRig[lens] targets bone lens_chooser, which is not in the armature - either restore the chooser bone per the lens spec or remove the dangling driver; (2) the image-plane Y-scale drivers hardcode /1.77 while X uses sensor/focal - derive aspect from the camera sensor (or render aspect) instead so non-16x9 formats frame correctly. Fix the template, re-verify TVPaint/Alchemy import + caddy against it, bump the bundled resource in magicam-blender, and re-run the extractor so magicam_rig_spec.json matches.
#3995Unreal bridge: a second editor steals the first editor's connection (and the loser never recovers)Bug fixUnreal Alchemy Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The bridge is single-editor by design ('latest connection wins' in /ws/editor drops the previous socket), so booting a SECOND editor silently disconnects the first — and the first never comes back: bridge_link's WebSocketApp keeps _ws_app set and run_forever does not return, so the reconnect loop never fires. Hit live 2026-08-13: a throwaway test-world editor kicked the working session off the bridge, which then sat there believing it was connected. Fix both halves: (1) SERVER — key connections by editor identity (pid/project) and hold MULTIPLE editors, routing /exec to a named target (default: the only one, or the most recently active), so two open editors is a normal state rather than a race; (2) CLIENT — detect the drop (on_close/on_error handlers, ping/pong keepalive) so run_forever returns and the existing retry loop reconnects on its own.
#3996MagiCam rig is invisible until the Control Rig track is engaged - make engaging automatic and discoverableBug fixMagiCam FunctionalMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Opening a level with MagiCam rigs shows NO controls: the artist sees only the cine-camera proxy mesh. The gizmos appear only after the rig's Control Rig track is selected in an OPEN Level Sequence (that selection is what flips the editor into Control Rig edit mode). After an editor restart the level editor also drops back to Selection Mode, so even a previously-engaged rig goes dark. Blender by contrast always shows bone shapes. Make this automatic and discoverable: opening a shot (or picking a camera in the Camera Caddy) should open the shot's sequence, select the rig's CR track and engage the mode, so controls are simply THERE; and there should be an obvious affordance to re-engage after a restart rather than requiring the artist to know about Sequencer track selection. Acceptance: open the level, pick a camera, see and grab its controls - no manual Sequencer steps, no docs required.
#3998select_control errors when no sequence is open - it should open the shot's sequence itselfBug fixMagiCam FunctionalMagiCam Native Pluginclaude
·Unreal Engine·2 months ago
After an editor restart, cam.edit.select_control raises 'no Level Sequence is open in the Sequencer - open one before running cam ops', so a Camera Caddy control button just errors. The verb should resolve the rig's shot sequence and open it (then engage the CR track and select the control) rather than requiring the artist to have already opened the right sequence by hand. Fall back to a clear actionable message only when no sequence contains the rig.
#3999Unreal bridge silently drives the WRONG editor when two are open - add project targetingBug fixUnreal Bridgeclaude
·Unreal Engine·2 months ago
get_client() picks nodes[0] from multicast discovery (and prefers the dial-out AlchemyUE bridge on :20264) with no way to say WHICH editor. With two editors open this silently retargets: verified 2026-08-13 with 'assets' and 'alcpanel' both running - commands aimed at the MagiCam rig in 'assets' landed in 'alcpanel' instead and reported the wrong level, wrong asset registry, no rig actors. A write op could have modified the wrong project's assets. Add an explicit target: get_client(project=...) plus an ALC_UE_PROJECT env default, matching on the discovery node's project_name (the node dict already carries project_name and project_root). When more than one node is discovered and no target is given, FAIL LOUD listing the candidates rather than silently taking the first - no silent fallback.
#4004MagiCam rig: validate mapping on load so viewport drags drive the camera (works when focused+realtime)Bug fixMagiCam FunctionalMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The rig is inert: moving a control changes the control's value but the camera does not move or rotate. Verified live 2026-08-13 in a fresh editor session on T345_mCam, every combination tried: sequence closed AND sequence open with the CR track engaged; before and after crc.initialize(); with crc.update() after each write. Camera stayed at world (70, 800, 150) pitch 0 throughout while dolly X=200, height Z=100 and tilt pitch=-25 were each applied and read back correctly on the control. The wiring LOOKS right - mapped_elements has tilt->CameraComponent, image_plane_NN_ctrl->PlaneRoot_NN and lens_grouping->LensHUD, all ElementType CONTROL, direction OUTPUT; bUpdateInEditor and bUpdateRigOnTick are both True; the Forwards Solve graph is intact (exactly one BeginExecution node). So the mapping is declared but its output is never applied. Candidate causes to check in order: (1) the ControlRigComponent does not actually tick in the editor world outside PIE, so OUTPUT mapping never runs - crc.update() alone may not drive the transfer; (2) transform_index is -1 on every mapped element - confirm that is the right value for a whole-transform map; (3) the CameraComponent is attached to the actor's root SceneComponent and CineCameraActor may re-assert its own transform, overwriting the rig's output. Acceptance: rotate tilt in the viewport, the camera body rotates with it; raise height, the camera rises; and the same holds after a scrub and in PIE/MRQ.
#4005Assert built-rig control positions against the spec (original premise was false - no geometry bug)Bug fixMagiCam FunctionalMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Measured live on a built T345_mCam 2026-08-13 (rig-local coordinates): main (0,0,0), dolly (120,0,25), height (120,0,150), tilt (120,0,150), CameraComponent (70,0,150), CameraProxyMesh (24,0,126), PlaneRoot_NN attached to the CAMERA at +500..2300 X. Two problems: (1) the camera is 50 cm BEHIND the tilt control that is mapped to drive it - they should be co-located, since tilt is the pivot the camera body rotates about; (2) the whole control chain sits at X=120 while the spec, converted through the recorded axis map UE(X,Y,Z)=(B.y,B.x,B.z)x100, puts tilt at (0,0,150) - the Blender tilt bone head is (0,0,1.5). Neither offset is in magicam_rig_spec.json and neither is explained in MAGICAM_PORT.md. Decide the intended layout, fix the builder, and add a parity check so the built rig's control positions are asserted against the spec rather than eyeballed. Acceptance: every control's rig-space position matches the spec through the axis map, and the camera is exactly at tilt.
#4032Camera Caddy: playhead-driven shot header - Camera at Playhead, Start/End jump, View RangeNew featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The native dockable Camera Caddy (#3983) shipped the core loop - camera list, engage, control selection, key, copy/paste, lens, per-plane visibility/opacity/depth. These Blender-caddy features are deliberately NOT built yet and were left OUT rather than stubbed as dead buttons: (1) Camera at Playhead - jump to/adopt the camera under the sequencer playhead; (2) shot Start/End readout plus View Range; (3) Camera Body dropdown driving filmback via cam.edit.set_body and the camera registry; (4) per-plane texture assignment. Add them once Tom has driven the panel and confirmed which he actually reaches for - his usage list should drive the order, not this ticket's order.
--- AMENDED 2026-08-14 ---
SPEC SHARPENED BY TOM 2026-08-14 (with the Blender caddy header on screen). The whole top block is driven by the SEQUENCER PLAYHEAD - the 'current shot' is whichever shot the playhead is currently over in the open sequence, and everything else reads from that: (1) CAMERA AT PLAYHEAD - select/adopt the camera belonging to the shot under the playhead, i.e. follow the camera-cut track rather than making the artist find the actor; picking it should engage the rig the same way a dropdown pick does. (2) START / END buttons - these DISPLAY the current shot's first and last frame (the Blender panel reads 'Start 100' / 'End 142'), and CLICKING one moves the playhead to that frame. So they are both a readout and a jump control, and they must re-read whenever the playhead moves to a different shot. (3) VIEW RANGE - set the Sequencer's visible/working range to the current shot's range, so the timeline zooms to the shot being worked. All three follow from one resolve: playhead frame -> shot -> {camera, start, end}. Build that resolution once (a query verb returning the shot under the playhead with its camera and range) and have the three controls consume it, rather than three independent lookups. The camera dropdown and this header must stay consistent - picking a camera in the dropdown and picking Camera at Playhead should leave the panel in the same state.
#4036Bridge link prints from the socket thread - trips a PythonScriptPlugin data-race ensure every reconnectBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
bridge_link._run_forever's on_error and on_close handlers call print() directly on the WEBSOCKET DAEMON THREAD. UE redirects Python stdout through PythonScriptPlugin's output device, which is game-thread-only, so every reconnect attempt writes to it off-thread and trips: 'EnsureFailed: Ensure condition failed: UE::Private::FMTAccessDetectorOptions::bSuppressCheckFailure ... Data race detected: acquiring write access on thread N concurrently with writers' with a PythonScriptPlugin callstack. Tom hit this on a normal editor restart with the Alchemy bridge server down (WinError 10061, connection refused): the loop retries every 2 s, printing each time, so the ensure repeats and each one costs ~4.4 s in SubmitErrorReport/SendNewReport. Fix: never touch Python stdout or any unreal API from the socket thread. Queue the message and let the existing game-thread _pump drain and log it (the module already has exactly this pattern for calls - the file's own docstring says received calls are 'never executed on the socket thread'; the logging path just missed the rule). Also quieten repeated identical reconnect failures - a dead server should not emit an ensure storm - and consider backing off beyond the fixed 2 s. NOTE websocket-client itself also prints from that thread ('ERROR: ... - goodbye'), so suppress its logging too (websocket.enableTrace(False) is not enough; set its logger to not propagate to stdout).
#4037Cold editor: MagiCam controls do not appear until CR_MagiCam is double-clicked - engage must load the rig assetBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Unreal Engine·2 months ago
After an editor restart the MagiCam controls do not draw until the artist double-clicks /Game/Alchemy/Cameras/CR_MagiCam in the Content Browser (which loads and compiles the Control Rig blueprint). Everything engage_rig does - resolve the shot sequence, open it, select the CR track, revalidate mapping, realtime on - assumes a rig INSTANCE exists on the ControlRigComponent; on a cold editor the component's Control Rig reference has not been resolved, so there is nothing to instantiate, select or draw. Fix: engage_rig (and anything else that reaches for the rig) must ensure the Control Rig asset is resident before engaging - resolve the asset path from the component rather than hardcoding it, load it, and re-fetch the rig instance; if the component still reports no rig after loading, say so plainly rather than silently engaging nothing. Acceptance: restart the editor, open the caddy, pick a camera - controls appear, with no Content Browser detour.
#4047Opening a location in Unreal does not start the Unreal bridge - Premiere and After Effects both doBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Core·Unreal Engine·2 months ago
alc/path/path.py brings the bridge up before launching the DCC for .prproj (Premiere) and .aep (After Effects), each with the same comment: the panel dials out to a local bridge server it cannot start itself, so start it first and the panel connects on the first try. Unreal is missing from that seam. Opening a location from the Alchemy Unreal view (double-click a location card -> 'Opening homestead in Unreal 5.8...') launches the editor with nothing listening on 20264, so the editor's dial-out is refused every retry and the panel is silently disconnected until someone runs ensure_running() by hand. Add the same guard for .uproject: ensure_running() from alc.plugins.unreal.bridge.launch, wrapped in the same try/except-and-warn so a bridge failure never blocks opening the editor. Acceptance: double-click a location in the Alchemy Unreal view with no bridge running - the editor comes up already connected, no reconnect lines in the log.
#4048Alchemy panel in Unreal renders then goes blankBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
The Alchemy panel inside the Unreal editor draws its content on open and then goes BLANK a moment later - it does not fail to load, it loads and then empties. Reported by Tom 2026-08-14 in a freshly restarted editor. Reproduce with the editor opened from the Alchemy Unreal view, then determine which of the panel's population paths is clearing it: a refresh/rebuild firing after first paint with empty data, a failed bridge call whose empty result replaces good content (the bridge was NOT running at the time - see #4047 - so an unguarded 'no data' response is a prime suspect), or a Slate rebuild dropping the child widgets. Whatever the cause, an empty response must never blank a populated panel: render the prior content plus an error/offline state instead. Acceptance: open the editor with and without the bridge running - the panel keeps its content in both cases, showing an explicit offline indicator rather than blankness when it cannot reach the app.
#4049Every project Alchemy launches or creates must get the AlchemyUE plugin - install_plugin exists but nothing calls itBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
install_plugin(world_dir) already exists (project_env.py:201 - junction into the project's Plugins dir + enable in the .uproject) and has a passing test, but NOTHING in production code calls it. Only its test does. Meanwhile configure_unreal_python_env - the Python-path/env setup - IS wired in at both seams: project creation (unreal.py:157) and location open (unreal.py:382). Wire the plugin the same way, at the same two points, so the guarantee is: every project Alchemy CREATES gets the plugin, and every project Alchemy LAUNCHES is checked and repaired before the editor starts. Treat it as the same contract as the Python paths and the cookbook menu hookup - Tom's framing: 'every project we launch through the alchemy interface should get the plugin - we should check that when launching and we should ensure that on creation of a project - same way we do with the python path variables and ensuring the cookbook can connect to the traditional menu system'. It must be idempotent (the test already covers a second call), must not fail the launch if it cannot install (log and continue, same as the bridge guards), and should report what it did so the app can surface 'plugin installed/repaired' rather than staying silent. Consider whether AlchemyUE belongs in _REQUIRED_PLUGINS (project_env.py:132, currently PythonScriptPlugin/HDRIBackdrop/SequencerScripting/MovieRenderPipeline) or stays a separate install step because it needs the junction as well as the descriptor entry.
#4062Camera Caddy: every op WITH arguments fails - pretty-printed JSON breaks the Python string literalBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Selecting a camera in the native Camera Caddy fails every op that takes arguments: 'SyntaxError: unterminated string literal' on edit.engage_rig and query.get_magicam_state, with the log showing the statement cut off right after the opening quote of the kwargs payload. Two causes in AlchemyPythonCall.cpp, fix both: (1) ToJson uses TJsonWriterFactory<>::Create(&Out), whose DEFAULT print policy is PRETTY - it emits newlines and tab indentation - so the kwargs JSON is multi-line; use TCondensedJsonPrintPolicy<TCHAR> so the payload is a single line. (2) PyLiteral escapes backslashes and single quotes but NOT newlines or carriage returns, so any multi-line value breaks the single-quoted Python literal it builds; escape \\n and \\r (and prefer a raw-safe construction) so the helper is correct for ANY input rather than only for the inputs that happen to be single-line today. Argument-less ops (query.get_magicam_rigs) work, which is why the camera LIST populates and nothing else does. Acceptance: pick a camera in the caddy - it engages, the state loads, and every control button, the lens dropdown and the plane rows do real work.
#4066Camera Caddy: a Sequencer area naming the live sequence everything reads fromNew featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Add a Sequencer area ABOVE the camera row showing which Level Sequence is currently live - the one the panel is reading from. Every value in the caddy (the shot under the playhead, start/end, the camera list's engaged state) derives from that sequence, so leaving it unnamed makes the panel's state ambiguous. It matters more than it looks because engage_rig can OPEN or resolve a different sequence than the artist expects: after picking a camera, the live sequence may have changed, and the panel currently gives no way to see that. Scope for now is a READ-ONLY label showing the active sequence's name, updating whenever the active sequence changes (including when the panel's own actions change it). Show an explicit empty state (e.g. 'no sequence open') rather than a blank row. Build it as its own area/section so it can later grow into a sequence SWITCHER - Tom: 'i can see that eventually growing to the ability to switch between sequencers, but we don't need that immediately' - so leave room for that without building it: do not add a dropdown, a list, or any switching affordance in this pass.
#4079Camera Caddy: Look Through This Camera - view-lock a viewport to the selected rigNew featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Add a control that puts the selected MagiCam's view on screen without the artist knowing UE's viewport menus. Setting up 'see my camera while still working in perspective' currently means: split the level viewport, open the target viewport's Perspective dropdown, find the camera under Placed Cameras, and know NOT to use Pilot. Encode that. Behaviour: a Look Through This Camera control that view-locks a viewport to the caddy's currently selected camera - a VIEW lock, not Pilot, so navigating elsewhere never moves the shot. Prefer targeting a second viewport when one exists so the artist keeps a free perspective view; if only one viewport is open, either split it or lock it and make the state obvious and reversible (an unlock/return-to-perspective affordance, since a locked viewport that cannot be escaped is worse than no button). Offer the camera-cuts variant too: lock to the sequence's camera cut track so scrubbing follows whichever camera the playhead is over - that pairs directly with the playhead-driven header from #4032 and is the better default for shot-to-shot work. Show which mode is active.
#4080Camera selection is CONTEXT ONLY - no Unreal side effects from the dropdown or Camera at PlayheadBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Camera at Playhead currently calls SelectCamera(Shot.Camera), which engages the rig - and with Sequencer's camera-cut lock on, refreshing the sequence makes EVERY open viewport snap to that camera. Tom had three viewports all commandeered. Two fixes: (1) SEMANTICS - the button must ONLY set the camera in the dropdown. The dropdown IS the caddy's context: it is what tells the panel which rig 'Main'/'Dolly'/'Height'/'Tilt' should act on. Tom: 'we literally just want to set the camera in the dropdown menu - the camera caddy uses what's in the dropdown menu to give itself context so that when you select main it selects the main controller for the right rig.' So resolve the shot under the playhead, set the dropdown to its camera, update the Shot readout, and stop. Engaging is the Engage Rig button's job (and whatever a control press implies). (2) VIEWPORTS - the caddy must never take over the artist's viewports as a side effect. Verified live: LevelSequenceEditorBlueprintLibrary.is_camera_cut_locked_to_viewport() was TRUE in Tom's editor, set by the legacy cam.edit.set_active (edit.py:79 set_lock_camera_cut_to_viewport(True)), which is sticky and global. The caddy must not set it, and should not rely on it; if a viewport-following mode is wanted it is opt-in and belongs to #4079 (Look Through This Camera), scoped to ONE viewport. Consider whether set_active should still force it at all, or leave it to the caller. Acceptance: with several viewports open, pressing Camera at Playhead changes only the dropdown and the Shot readout - no viewport moves, no rig is engaged.
--- AMENDED 2026-08-14 ---
GENERALISED BY TOM 2026-08-14 - this is the rule for the whole panel, not just the Camera at Playhead button: 'i'm not expecting any UE behavior when i select a camera in the dropdown - i'm selecting the camera caddy to be paying attention to that camera and only that.' So: CHANGING THE DROPDOWN MUST DO NOTHING IN UNREAL. No engage, no actor selection, no viewport change, no sequence opened, no rig instantiated. It only sets which camera the panel is watching, and refreshes what the panel DISPLAYS for that camera (state, lens, planes, shot readout) - reads are fine, writes are not. Camera at Playhead is then simply 'set the dropdown to the shot under the playhead', the same no-op-in-UE path. Every UNREAL action stays behind an explicit button: Engage Rig engages; Main/Dolly/Height/Tilt select controls (engaging first if that is required for the control to be selectable, because the artist asked for that by pressing it); Key/Copy/Paste/lens/plane controls write. This supersedes #3983's original 'pick a camera -> engage_rig(adopt=False)' behaviour, which was written before anyone drove the panel. Design rule to carry forward: SELECTION IS NOT ACTION - the panel observes by default and only mutates when a control is pressed.
--- AMENDED 2026-08-14 ---
REMOVE THE ENGAGE RIG BUTTON (Tom, 2026-08-14): 'engage rig button is something you invented - i don't have that button... so i don't even know what engage rig is for... i didn't design that.' He is right and it supersedes the line above that said Engage Rig is where engaging belongs. Engagement is a UE IMPLEMENTATION DETAIL - Control Rig gizmos only draw while the rig's CR track is selected in an open sequence - and Blender has no equivalent concept because bone shapes are always visible. We hit that constraint and surfaced it as a button, making our problem the artist's vocabulary. Fix: DELETE the Engage Rig button. Engage IMPLICITLY, at the moment an action needs it - pressing Main/Dolly/Height/Tilt, keying, pasting a pose - so the rig becomes grabbable because the artist asked for a control, not because they knew to press a setup button first. The word 'engage' should not appear in the UI at all. Also re-word or drop the status line 'Engaged in <sequence> (open)' - the same invented vocabulary; if a status line is useful it should say something an artist recognises (which sequence the panel is reading), which is what #4066's Sequencer area already does. STANDING RULE: the Blender caddy is the design reference. A control that exists ONLY in the UE panel is an invention and needs justifying against a real artist need - it is not enough that the implementation wanted it.
#4081Camera Caddy layout: Start / End / View Range belong on their own rowAdjust existingMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
In the shipped panel the Sequencer header packs Camera at Playhead, Start, End and View Range onto ONE row. Tom wants Start / End / View Range on their OWN row, separate from Camera at Playhead - matching the Blender caddy, where 'Camera at Playhead' is a full-width row and Start/End/View Range sit beneath it as a three-up row. Same controls and behaviour, just the row split.
#4082Set Layout: build the Alchemy previs viewport layout, and make the top viewport the caddy's targetNew featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Add a Set Layout button to the Camera Caddy that builds the standard Alchemy previs viewport arrangement in one press, and establish it as the convention every viewport-touching feature obeys. Layout: a TOP/BOTTOM split. TOP is the LIVE SHOT VIEW - it shows what the sequencer's current camera sees, so scrubbing the frame range updates it live as the camera cuts change. BOTTOM is a free working perspective the artist navigates without disturbing the shot. THE CONVENTION (the point of the ticket): whenever the caddy changes a viewport, sets a camera or looks through something, it does it in the TOP viewport - never 'the active viewport', never all of them. That determinism is what stops the class of bug in #4080, where acting on whatever viewport UE considered active commandeered all three of Tom's. Requirements: idempotent (pressing it twice is a no-op, not a cascade of splits); does not fight the artist (if they rearrange afterwards, the caddy targets the top viewport if it still exists and degrades honestly if it does not - say so rather than grabbing a random one); works from whatever layout is currently up. KNOWN CONSTRAINT to solve or work around: LevelSequenceEditorBlueprintLibrary.set_lock_camera_cut_to_viewport is GLOBAL - verified live 2026-08-14, it locked every open viewport at once, which is what produced the three-hijacked-viewport screenshot. If per-viewport camera-cut following is not reachable, find another route for 'top viewport follows the sequencer camera' (e.g. view-locking the top viewport to the current cut camera and updating it as the playhead crosses cuts) rather than turning the global on and hoping. Investigate the real UE 5.8 API for setting a viewport layout configuration before designing - LevelEditorSubsystem exposes viewport config KEYS (editor_get_active_viewport_config_key / editor_set_viewport_realtime take one), which suggests layout is addressable, but confirm rather than assume.
#4087Caddy: Sequence dropdown that opens the shot sequence AND loads its levelNew featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Every time Tom opens the editor he has to navigate to the right Level Sequence and then load the level it belongs to - a lot of clicking before any work starts. Put a dropdown next to the caddy's Sequence readout listing the sequences worth working on, and picking one should OPEN it and LOAD its level in one action. Candidates come from Alchemy's coordinates, not a content-browser scrape - the shape is /Game/videos/<project>/SEQ/<task>/<variant>/<name> e.g. /Game/videos/theaxe/SEQ/prv/default/theaxe_SEQ_prv_Shots_Sequence. Derive the pattern from the existing path/coordinate layer rather than hardcoding the string, and work out the sequence->level mapping from the same root (this project has theaxe_SEQ_lvl as the persistent level and theaxe_SEQ_cam_lvl for cameras) - do not guess a naming rule without checking how the previs build wrote them. Behaviour: picking loads the level THEN opens the sequence, is safe to re-pick (no-op when already current), and reports plainly if the level or sequence is missing. Keep the dropdown honest about what it found - if no sequences match the coordinates, say so rather than showing an empty control.
#4088Caddy control buttons: selection blinks on and off - the panel path clears it, the verbs do notBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Clicking Main/Dolly/Height/Tilt in the native caddy selects the control and it immediately deselects - the artist sees the highlight blink and can never grab the gizmo. EVIDENCE, measured live 2026-08-14 in loj/homestead on 010_0010_mCam: calling the SAME verb from Python leaves everything selected and it SURVIVES every read the panel fires afterwards - after edit.select_control: tracks 1, controls [dolly]; unchanged after query.get_magicam_state, get_playhead_shot, get_viewport_target and get_magicam_rigs. After Tom clicks the button, state reads tracks 0, actors 0, controls [] - even the ACTOR selection is gone. So the verbs are correct and something in the panel's own click path (SAlchemyCameraCaddy.cpp, its RefreshState, or the round-2 AlchemyViewportLibrary work) clears the whole selection after the op returns. Find what actually clears it rather than re-asserting selection as a workaround - candidates to check in order: a second engage triggered by the refresh (re-running mapping validation rebuilds the rig instance and would drop control selection), a viewport/cinematic-control call from the round-2 layout work resetting Control Rig edit mode, or ExecPythonCommand being issued from a context that resets editor selection. Acceptance: click Dolly, the control stays selected and draggable, with the pointer still over the panel.
#4092MagiCam image planes: opacity default, plane controls do not move planes, hide unused controls, stop auto-keying on createBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Four findings from Tom's first productive session actually moving cameras (2026-08-14). (1) OPACITY DEFAULT - UE transparency reads very differently through the camera vs the perspective viewport: he needs about 0.05 to see THROUGH a plate in the camera but about 0.3 to see it at all in the viewport. Default the plane opacity to 0.3, since that is the workable value while blocking. (2) PLANE CONTROLS DO NOT MOVE THEIR PLANES - moving image_plane_NN_ctrl does not move the plane; the mapped element (image_plane_NN_ctrl -> PlaneRoot_NN) is not driving. Find why the plane roots do not follow their controls, and when they DO move, the plate must re-scale to the frustum at its new depth (the sensor-derived autoscale W = depth*sensor_w/focal, H = depth*sensor_h/focal that Blender does via drivers and _refit_plane does in UE) - moving a plane closer or further has to keep it filling frame. (3) HIDE UNUSED PLANE CONTROLS - controls whose plane has no image connected should not draw; ten always-visible plane rings eat the screen. Hide by whether the plane actually carries a plate, and re-show when one is assigned. (4) STOP AUTO-KEYING ON CREATE - camera creation currently writes keyframes at the first frame of each shot; Tom does not want them and has to delete them. Build the rig posed but UNKEYED unless keys are explicitly asked for.
#4105MagiCam rig does not drive the camera when a Sequencer Control Rig track owns the rig instanceBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
On the previs-built rigs in loj/homestead, moving ANY control moves the rig but never the camera. Evidence chain measured live 2026-08-14, all read-only except writes that were exactly restored: (1) the control hierarchy DOES propagate - height local +100Z moves tilt global 150 -> 250; (2) the camera does NOT follow - CameraComponent relative stays (0,0,150) after crc.update(0.0333) AND after _sync_actor AND after set_mapped_elements revalidation + warmup; (3) the mapping is well formed - tilt -> CameraComponent, ElementType CONTROL, direction OUTPUT, space WORLD_SPACE, and the actor really does have a CineCameraComponent named CameraComponent (BP_MagiCam_C, a CineCameraActor); (4) crucially, crc.get_control_rig() returns an instance owned by the SEQUENCE: ...theaxe_SEQ_prv_Shots_Sequence:MovieScene_0.MovieSceneControlRigParameterTrack_0.CR_MagiCam_C_0. HYPOTHESIS to test first: when a Sequencer Control Rig track exists for the actor, the track owns the rig instance and the ControlRigComponent's own transfer reads a DIFFERENT instance, so writes to the controls never reach the mapped components. That would explain why the same code worked on /Game/Alchemy/Cameras/BP_MagiCam in the assets project earlier the same day (tilt -> camera verified moving) and fails on every previs-built rig here. Determine which instance the component transfers from, whether a sequencer-owned rig can drive MappedElements at all, and what the correct architecture is - e.g. let Sequencer evaluate and drive the components, or keep the component authoritative and have the track write through it. Do NOT add another per-call patch; this is the third layer of the same symptom and each patch has revealed the next one. Acceptance: with the shot sequence open and its CR track present, dragging main/dolly/height/tilt in the viewport moves the camera live.
#4106Rebind the Control Rig delegate after load - a reloaded MagiCam never transfers to its cameraBug fixMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
A MagiCam built in the current session drives its camera correctly. After a save + editor restart the SAME rig never transfers again: moving main moves tilt's global transform, the camera never follows. Mechanism, from UE 5.8 ControlRigComponent.cpp rather than inference: Update() transfers nothing; TransferOutputs() is called from exactly one place, HandleControlRigExecutedEvent, which is bound to the rig's OnExecuted delegate ONLY inside SetControlRig() (line 1343). ControlRig is a transient UPROPERTY. On a fresh build the component creates its own instance via SetupControlRigIfRequired() -> SetControlRig(new) so the delegate binds, and the Sequencer track then re-outers that same object, which is why a freshly built rig drives fine WITH a track. On reload the track's serialized instance reaches the component without SetControlRig() ever running, SetupControlRigIfRequired() sees a non-null rig matching the asset reference and early-returns at line 1294, and the delegate is never bound. crc.initialize() routes through the same early return, which is why it cannot repair a reloaded rig. FIX: add to the AlchemyUE C++ module a RebindControlRig(UControlRigComponent*) that calls Comp->SetControlRig(Comp->GetControlRig()) - the binding block at 1336-1352 runs unconditionally, so passing the SAME instance re-binds OnExecuted and re-runs ValidateMappingData via Initialize() while keeping the track's instance intact. Expose it BlueprintCallable so Python can reach it (SetControlRig is CONTROLRIG_API but not a UFUNCTION), and call it once per rig from the live tick's first pass and/or on engage. Acceptance: dev/magicam_check.py returns USABLE=True on a rig AFTER a save and editor restart, and dragging a control moves the camera.
#4113Caddy image plane rows: replace the depth readout with a select-control buttonAdjust existingMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
In the Blender caddy an image-plane row lets the artist SELECT that plane's control (the blue name area), set opacity, and toggle visibility with the eye icon - and only planes that actually exist are listed. The UE panel lists the right planes and has opacity, but it lost the ability to select the plane's control and instead shows a DEPTH/distance value that Tom does not use. Replace the depth readout with a button that selects that plane's Control Rig control (image_plane_NN_ctrl) via cam.edit.select_control - the same path the Main/Dolly/Height/Tilt buttons use, so it clears other selections and gives a grabbable gizmo. Keep opacity and the visibility toggle. Tom: 'we could replace the distance with a button that selects the control and the UE version becomes just as functional.' Depth is still settable through the verb layer (set_plane_depth) - it is only the panel readout that goes, since moving the control IS how an artist changes depth now that plane controls drive their planes.
#4124when i'm working in the transcript view i'd like to be able to have access to creating my otio timeline - right now i think that's hidden until the previs prep is complete.New featureSave TimelineTomPrevis
·App · transcript·Alchemy·2 months ago
#4184on the gantt we should group assets by category - as that's typically how they are assigned - so 1 artist should not be assigned characters, props and locations randomly where possible we should assign characters to the same artist, locations to the same artist and props to the same artist. and we should go in order, character, environment, prop when assigning.New featureSchedule PipelineCategory-Grouped AssignmentTom
·App · schedule·Alchemy·2 months ago
#4185Reorder the tasks on an artist's Gantt row to change their scheduled orderNew featureSchedule PipelineHand-Ordered RowsTom
·App · schedule·Alchemy·2 months ago
On the Schedule Gantt, each person's row (a department seat) is a sequence of task bars laid out by the greedy scheduler. The producer should be able to drag a bar within its own row to change the ORDER that person works their tasks, and the chart should re-flow: the row's bars re-pack against the same start, keeping their durations and business-day math, and everything downstream of them (dependent tasks in later departments) reschedules from the new dates. The chosen order persists per project in the schedule settings (config_files/schedule/{project_id}/settings.json) so it survives a re-render and a re-publish of the bid, and it is applied as a manual override on top of alc.services.schedule.schedule_items (which is otherwise deterministic and priority-driven). Reordering is within one row only, not across rows or departments.
#4189Show a task's dependency chain on the Gantt when you click itNew featureSchedule PipelineDependency Chain ViewTom
·App · schedule·Alchemy·2 months ago
On the Schedule Gantt, clicking a task bar reveals the dependency chain that task sits in: thin connector lines from each upstream task's end to this task's start, and on to its downstream tasks, drawn across the department lanes. Lines are NOT always on — the chart has hundreds of bars and permanent connectors would bury it. They appear on selection (click a bar; click empty space or the same bar again to clear), the selected bar and its chain stay at full strength while everything else dims back, and the lines follow the bars when the zoom (day_px) changes. The dependency data already exists server-side: alc.services.schedule.build_items resolves each item's 'deps' (upstream task ids within the same entity, from the Production Tasks graph) — today it is used only to order the schedule and is dropped before render. Carry a stable bar id plus its dep ids into the render model so the client can draw the chain.
#4190Auto-Set entity types in the Project AuditorNew featureAuto-Set Typesclaude
·App · script·2 months ago
The script parser routes every all-caps token it cannot place into the props pool, so a 507-row audit of The Iron Giant showed 361 props - including HOGARTH, the lead, because his name appears all-caps in an action line of a scene where he never speaks. Add a wand button per row plus an Auto-Set All button in the Type column header that re-decides every row's type from the whole script (who speaks, what the scene headings say, how each word is shaped) and drops the answer into the Type dropdown. Each suggestion carries the rule that fired and a plain-English reason, shown on the button tooltip, so an artist can see why and tune the word lists when it misses. Retyped rows stay unconfirmed and are ringed for review - the classifier never confirms its own guess.
#4191Sluglines without INT./EXT. are parsed as Action and leak into the props poolBug fixHeading Sanity Checkclaude
·Ontology · models·2 months ago
The PDF line classifier decides a slugline by one test - does the line open with INT./EXT./EST. That misses every sub-heading a writer drops mid-scene (DEEP SPACE, A SHIP ON A STORM-TOSSED SEA - NIGHT, FLASHBACK: KENT'S OFFICE), which arrive as Action paragraphs. The entity extractor then scans Action blocks for all-caps runs and files anything it does not recognise as a prop, so sluglines become fake prop assets. On The Iron Giant that is 48 misfiled paragraphs. Add a sanity check that runs after paragraph assembly - where a slugline is a short standalone all-caps paragraph and the shape is obvious - promoting Action to Scene Heading or Shot. It must only ever promote, never demote, and must return a report of every change rather than fixing silently. Also expose it as a CLI so scripts ingested before the check existed can be repaired in place.
#4201[cookbook] [alchemy] [dsn] Implement the Add to dailies playlist buttonNew featurealchemyDesign review submissionstub
·Alchemy·2 months ago
Add the mp4 review to the dailies playlist. if the dailies playlist doesn't exist, create it.
#4212Script templates: pipeline_primitive + pipeline_tutorialNew featurePipeline VideosPipeline Video Templatesclaude
·Core·2 months ago
Add two builtin ScriptTemplates to the registry in alc/config/script_templates.py: 'pipeline_primitive' — a 60s marketing-short shape for one cookbook recipe (Hook / Map / Run / Receipt / Chain-card, with Run at ~55% share) — and 'pipeline_tutorial' — a long-form shape that walks every recipe step at reading pace. Authored as seconds-at-reference-duration like the existing builtins, with slots (Hook: the after-state; Run: one beat per cookbook step; Receipt: the published output; Chain-card: feeds/built-on links). Tested in tests/test_script_templates.py alongside the other builtins.
#4213Videos catalog page on the Pipeline tabNew featurePipeline VideosPipeline Video Catalogclaude
·App · walkthroughs·2 months ago
New Videos page on the Pipeline tab (sibling to Cookbook/Walkthroughs, new tool dir app/tools/videos/). Rows are DERIVED live from the cookbook — one row per phase recipe and per artist-UI button, grouped by the same lenses the Cookbook editor uses (Phases / Artist UI / Tools / Smart Tasks / Ingest-Deliver). A button that fires a recipe shares that recipe's row (no double-counting); button-only tools get their own. Each row has a Tutorial slot and a Marketing slot with a status chip (none → scripted → recorded → published) and links to the existing script/video when present. Storage convention: videos live in the ckb project via the existing alchemy_videos route (entity_type=videos, child VID), slug {dcc}-{phase}-{task}-{aud}, e.g. alchemy-build-dsn-mkt; status is derived from what exists at that coordinate, not from a hand-maintained list — a new recipe automatically shows an empty row.
--- AMENDED 2026-08-24 ---
AMENDED addressing convention (settled with Tom 2026-08-24): use the route's real axes instead of a flattened slug. video token (parent) = the pipeline, e.g. 'alchemy-dsn'; child = the phase ('build', 'publish', ...) so each primitive video is a SHOT of the pipeline; child=SEQ is the container AND the chain video (the whole-pipeline cut assembled from its children, exactly like a scene's edit assembles its shots); variant = audience 'mkt' / 'tut' (two cuts of one unit sharing the run recording — separate scr scripts per variant). Button-only tools: e.g. 'alchemy-tools'/'height'. Filenames fall out as parent_child_task: alchemy-dsn_build_scr. NOTE the earlier spec's 'VID' child was wrong — create.py stamps SEQ; the pathmap comment saying VID is stale and should be fixed in passing. Caution: alchemy_videos route lists 'variant' without the '?' optional marker — confirm an unset variant doesn't truncate the path.
#4214Seed a video script from a cookbook recipeNew featurePipeline VideosVideo Script Seedingclaude
·App · script·2 months ago
Seed-script action on each Videos-page slot (see #4213): creates the video entity at ckb/videos/{slug}/VID/scr via the alchemy_videos route and seeds a Script from the matching template (#4212 — pipeline_primitive for Marketing, pipeline_tutorial for Tutorial), then lands the user in the Script tool at that coordinate. Prefill: Run beats one-per-cookbook-step from the recipe's step labels and goals (reuse the resolution logic in Walkthrough.pipeline_plan / scaffold_from_pipeline); Receipt beat from publish outputs / pass_criteria; Hook hint from the task label; Chain-card from phase adjacency (what this feeds / what it's built on). Should ride the existing script-seeding path in the script tool rather than adding a parallel one. Acceptance: seed Marketing for alchemy-build-dsn → Script tool opens a timed 60s skeleton whose Run beats name Check for Front Image, Check for T-Pose, Ensure Headshot, Generate Model, Generate Model From Tpose.
--- AMENDED 2026-08-24 ---
AMENDED addressing convention (settled with Tom 2026-08-24): seed target is ckb/videos/{pipeline}/{phase}/scr with variant = audience, NOT a flattened slug with VID child. Example: marketing short for the Design build phase = video 'alchemy-dsn', child 'build', task scr, variant 'mkt' → filename alchemy-dsn_build_scr (variant-tagged). The chain video is the container itself: video 'alchemy-dsn', child SEQ — its edit assembles the phase children like a scene assembles shots, so the pipeline_primitive template seeds children and the chain script seeds at SEQ. Earlier 'VID' reference was wrong (create.py stamps SEQ). Caution: confirm unset variant doesn't truncate the alchemy_videos path (template lists variant without '?').
--- AMENDED 2026-08-24 ---
AMENDED (default variant, decided with Tom): seeding with no audience choice creates variant='tut' EXPLICITLY (stamped, not left unset — 'untagged means tutorial' would be tribal knowledge and the catalog chips need to distinguish). Rationale: the tutorial is the raw run-through Tom records first, and the marketing short is a cut-down of that same footage — default follows both creation order and derivation order. 'mkt' is seeded later as the derived cut.
The pathmap (cgl-cookbook cookbook/pathmaps/alchemy.yaml) still carries the pre-videos-entity 'cookbook_videos' route (when project=ckb, project_type marketing) that shoehorns videos through episode/category/asset_sub_type/asset. The ckb project should resolve through the standard alchemy_videos route (project_type: all) like every other project. FIRST inventory what files still live under the old convention on E:/Alchemy/cgl/ckb before touching anything; then remove or fence the legacy route so it can't shadow alchemy_videos, and migrate/leave-in-place the old files per what the inventory shows. Non-destructive: nothing deleted without the inventory reviewed.
#4222Script preview panel on the Pipeline Videos catalogNew featurePipeline VideosBatch Script Reviewclaude
·2 months ago
Selecting a catalog row opens a preview of that video's seeded script WITHOUT leaving the page, docked on the LEFT side of the screen (there's room; catalog sits right of it). V1 = view: embed the script column widget from the script tool, homed on the video coordinate, so the panel renders the real script surface — and editing comes almost for free since the widget already knows how to edit. Per-audience (Tutorial/Marketing). Prev/next through catalog rows for batch review — the point is reviewing dozens of seeded scripts quickly to dial in the generation prompts. Later: table read via existing voice synthesis.
#4223Editor ops drop the variant token — edits re-home to the default variantBug fixPipeline VideosVariant-Safe Editingclaude
·App · transcript·2 months ago
The transcript/script editor's op context (_target_ctx / _coord_query / _ctx_from_po in app/tools/transcript/transcript.py, and the _cq string in partials/transcript_rows.html) carries entity_type/asset_category/asset_name/task/version but NOT variant (or user). A doc opened at a variant coordinate (e.g. /at/videos/alchemy-dsn/build/scr?variant=mkt — the pipeline-video audience convention from #4213/#4214) renders correctly, but every edit op resolves without the variant and silently creates+writes scr/default/.../transcript_edit.json instead of the mkt/tut doc on screen. REPRODUCED: POST insert_row with the ctx query wrote a second manifest under scr/default beside the seeded scr/mkt. Fix: thread variant through the op context keys, the _cq template string, the data-* ctx stamps, and the resolve_location call — explicit token, no inferred fallback. Blocks: editing seeded pipeline-video scripts anywhere, and embedding the script column widget in the Videos catalog preview (#4222) with live editing.
#4230the action / narration character should default tot he default voice set in the project. right now it seems like it picks the first in the list.New featureProject Default VoiceTomVoice Over
·App · transcript·Alchemy·2 months ago
#4232Videos catalog drops non-core pipelines: unreal rig has no video slotBug fixPipeline Video Coverageclaude
·Alchemy·2 months ago
The Pipeline Videos catalog claims 'one video slot per cookbook automation' but _pipeline_pairs() (app/tools/video_catalog/video_catalog.py:127) yields exactly ONE software per production task — the task's core software. Rig's core is blender, so only a blender-rig slot exists; the unreal rig pipeline (cookbook unreal/{start,build,render,publish}/rig, 5 build steps, hydrates fine) gets no slot at all. Filtering the page by Unreal Engine shows only Lookdev because ldv is the only task whose core software is unreal. Fix: _pipeline_pairs should yield every software that has its OWN pipeline plan for the task (Walkthrough.pipeline_plan non-empty, own recipes — not the cross-DCC alchemy fallback), core software first for ordering. Verify the software filter dropdown copes with a task appearing under multiple softwares.
#4233when i "record" a voice track the timeline doesn't update accordingly - it seems like it keeps the previous duration rather than editing it.New featureRecorded Take TimingTomEditing
·App · transcript·Alchemy·2 months ago
#4234Diversion VCS backend for Unreal locationsNew featureDiversion Version Controlclaude
·2 months ago
New Location bootstraps a versioned world end-to-end: REST repo create (DP_{co}_{proj}_{world}) -> dv clone --new-workspace into the pipeline path -> UE .dvignore -> scaffold -> first commit. Track adopts an existing local world in place (dv init); Cloud ghost cards clone repos that exist remotely but not on this machine. Token via ALC_DIVERSION_TOKEN / alc/config/.env; dv CLI found via PATH or ~/.diversion/bin.
#4235Sync-on-open and one-click Push for Unreal worldsNew featureArtist Sync Loopclaude
·App · routes·2 months ago
Opening a world in Unreal runs the Diversion open-time hook first: tracked worlds pull latest (dv update), untracked ones are adopted automatically; VCS trouble reports in the launch flash but never blocks the editor launch. The Push chip commits all local changes (dv commit -a) - and looks before committing: a clean workspace reports already-in-sync instead of claiming a push happened. Sync to other machines is automatic; there is no push step to teach artists.
#4236Locations panel shows real per-world VCS state in one placeNew featureLocation Sync Statusclaude
·App · web ui·2 months ago
One footer chip per location card is the ONLY place workspace/repo state appears, and it is the action when the state needs one: Synced (quiet), Push (n) commits, Pull syncs, Track adopts, VCS ?/Not linked carry their reason in the tooltip. State is derived live: workspace identity from .diversion metadata on disk, uncommitted counts from the workspace-status API, Pull from branch head vs base commit; API failures render as explicit unknown, never a stale green Synced. Session chip is honest: Live only when the UE bridge reports an editor open on the world, Target for a stored session pointer.
#4237Locations Reveal button never opened ExplorerBug fixReveal On Diskclaude
·App · web ui·2 months ago
Clicking Reveal on a location card opens the world folder in Explorer/Finder. It had never worked: |tojson emits a double-quoted string inside a double-quoted onclick attribute, truncating the handler into a syntax error - nothing ran (not even stopPropagation), the click fell through to the card and fired connect, reading as a page refresh. Fix: single-quote the attribute (the documented tojson pattern). Same pattern may lurk in other templates - a sweep is worth a follow-up.
#4238on the video-catalog page i'd like a filter that allows me to only show videos that have been seeded for either a tutorial or a marketing video.New featureTomVideo
·Service · videos·Alchemy·2 months ago
#4239i need to work on what information i can pull from button information to help with my copy template. we can use the button copy template and the alchemy Design Build UE RIG button in the cookbook as an exampleNew featureButton Copy PlaceholdersTomDesign
·App · cookbook·Alchemy·2 months ago
#4240when i save a timeline - i'd like to see if there's a way to insert it into the default timeline within the premiere proj template file or to create a copy of that sequence to insert it into.New featurePremiere ConformTom
·App · transcript·Alchemy·2 months ago
#4242Open Edit button on the script editor opens this version's Premiere projectNew featurePremiere Conformclaude:otio-premiereScript
·App · transcript·Alchemy·2 months ago
A button in the script editor header (beside Save Timeline) that opens this version's .prproj (seeded by the scr start recipe into the source dir) via alc.path.path.start(), which also ensures the Premiere bridge is up so the panel connects on first try. If no .prproj exists in the version's source folder, say so instead of failing silently.
Reorder the VIEW_MODES dict in app/navigation.py so the header workspace switcher renders tabs in this order: My Tasks, Browse, Planning, Pipeline, Unreal, Dashboard, Cloud Studio. The switcher renders in dict insertion order, and owning_mode() fallback also walks that order, so a pure key reorder is the whole change. Story tab position is irrelevant here (separate ticket removes it).
#4245Remove the Story workspace tabAdjust existingclaude
·App · shell·2 months ago
Drop the 'story' entry from VIEW_MODES in app/navigation.py and remove _STORY from SIDEBAR_NAV taggings. Its sidebar items (media, transcript, script, style, voices, edit) are already tagged into Browse; video_templates is _STORY | _PIPELINE, so keep it visible via Pipeline (and add to Browse if it should stay reachable there). Stale 'story' view cookies must fail open per visible_nav()'s unknown-mode rule — verify a user with view=story lands somewhere sane.
#4246Dashboard Timeline is very slow to loadBug fixclaude
·App · analytics·2 months ago
The Dashboard > Timeline page (combined timeline, app/tools/analytics/combined_timeline.py) takes a long time to render on first load. Profile where the time goes (data assembly server-side vs payload size vs client render) and make initial load fast — e.g. cache the assembled timeline, lazy-load detail rows, or paginate the range. Acceptance: clicking Timeline in the Dashboard sidebar shows the timeline without a noticeable stall.
#4247Dashboard Timeline: fit horizontally, expand vertically — one scroll axisAdjust existingclaude
·App · analytics·2 months ago
The Dashboard Timeline currently scrolls on both axes. Re-lay it out so the time axis compresses/readjusts to fit the viewport width and the content grows vertically instead, leaving vertical scroll as the only scroll axis. No horizontal scrolling in the normal case.
#4248Analytics: demote the 'open bugs' count — it's bugs logged, not active bugsAdjust existingclaude
·App · analytics·2 months ago
The Analytics page prominently shows an open-bugs count (e.g. '31 open bugs') sourced from open_bugs in app/tools/analytics/analytics.py. This reads as active product bugs, but most logged bugs are resolved during the development process — the number measures bugs logged, not bugs outstanding. Move this stat out of the prominent position to a secondary spot (or relabel it to say what it actually counts) so it doesn't misrepresent project health.
#4249Remove Activity Report from the Dashboard workspaceAdjust existingclaude
·App · shell·2 months ago
Drop the Dashboard tagging for the 'activity_report' sidebar item in SIDEBAR_NAV (app/navigation.py, currently _DASHBOARD | _PIPELINE) — its content (features shipped, /features route) is now covered by the Timeline, so it's redundant in Dashboard. Leave it reachable from Pipeline unless triage decides to retire the item entirely.
#4250Remove the Remote (tailscale) link and remote-access wizard entirelyChoreclaude
·App · routes·2 months ago
The tailnet/Tailscale remote-access workflow is dead — remove it completely, not just hide it. Delete app/routes/remote_access.py and app/templates/partials/remote_access/ (wizard.html, panel.html); drop the remote_access import and router include from app/main.py (~lines 140, 177); remove the 'remote' entries from app/navigation.py (SIDEBAR_NAV tagging, the mode item list ~line 215, and the ToolPath 'config/remote' registration ~line 344); remove the Remote sidebar block from app/templates/partials/production_sidebar.html (~line 334). Leave app/win_accept_guard.py alone — its Tailscale mention is just a comment about network binds. Acceptance: no Remote link anywhere in the app, /config/remote route gone, app boots clean.
#4252Project switch reverts without ctrl+r — stale workspace tabs + cookie-blind dashboard groundingBug fixPer-Tab Projectsclaude
·App · routes·2 months ago
Switching projects in the header must stick without a manual refresh, and two browser tabs must hold two different projects independently. In-place switch OOB-swaps the view-mode switcher (workspace tabs) and <title> so no shell link keeps the old ?project=; /c/{cid}/ without ?project grounds to the per-browser alc_active_project cookie instead of stale last_project_id; select_company redirect honors the same cookie.
#4253ticket_cli audit: reconcile in-progress tickets against merged history at PR/merge timeNew featureBoard Reconciliationclaude
·App · kanban·2 months ago
Add an 'audit' subcommand to dev/ticket_cli.py that lists every in_progress ticket and cross-checks it against commits reachable from origin/main in BOTH repos (provenance.repos()): (a) MERGED-NOT-DONE — a 'Feat/Fix/Chore #<n>:' commit is on origin/main but the ticket still sits in_progress -> suggest 'done <n>'; (b) STALE CLAIM — in_progress with no #<n> commit on ANY ref and no update in N days (default 7) -> suggest 'release <n>'. Match the subject convention '#<n>:' with word boundaries and EXCLUDE 'Merge pull request #<n>' subjects — PR numbers collide with ticket numbers (a 2026-08-31 sweep false-positived ticket #98 against PR #98's merge commit). Then wire it into the skills: .claude/commands/alc_do_ticket.md step 5.6 and alc_sprint.md section 7 run 'ticket_cli audit' right after recording the PR URL, and report/fix what it flags — that makes the PR/merge stage the reconciliation point. Baseline data: a 2026-08-31 manual sweep found 12 in_progress tickets of which 2 were merged-not-done (#3784, #3903) and 8 were dead claims with no commits anywhere.
#4254Delete the orphaned alc/services/remote_access serviceChoreclaude
·Service · remote_access·2 months ago
Ticket #4250 removed the Remote link, wizard templates, and app/routes/remote_access.py — which was the only importer of alc/services/remote_access/ (base.py, tailscale.py, the RemoteState machine and tailscale.exe CLI wrapper). The service package is now dead code; delete the directory. Verify nothing else imports alc.services.remote_access (grep) and the app boots clean.
#4260Headshot generation: neutral stage-lit background, grounded on main + T-pose referencesAdjust existingalchemyHeadshot GenerationclaudeDesign
·Service · ai·Alchemy·2 months ago
The headshot deliverable (design view HEADSHOT tile button, and the EnsureHeadshot build preflight — both already route through alc.services.ai.design.generate_view) currently composes its prompt with the project style spine spliced in and grounds on the anchor image only. Change the headshot branch of generate_view to: (1) ground on the character's MAIN image and front T-POSE image together as reference images (falling back to the anchor path only when neither exists); (2) drop the style spine from the headshot prompt — the references carry the look, a prose restatement competes with them; (3) ask for a neutral, stage-lit studio background. One implementation, both callers.
#4261Build UE Rig: run the Meshy cloud-rig app-side, not inside a frozen editorBug fixalchemyDesign to UE RigclaudeRig
·Cookbook · alchemy · shelves · dsn·Alchemy·2 months ago
The Build UE Rig button's UE leg runs CreateRigFromDesignGlb inside the spawned editor, where the Meshy rigging round-trip (upload + while-True poll, 5-15 min) blocks UE's game thread. The 'headless' UnrealEditor-Cmd launch also shows the full editor UI on the worker's desktop, so every press produces a visibly hung 'not responding' editor for minutes; it gets closed, the wrapper finds no result report, and the job fails with 'the headless editor never reported a result' (hogarth Aug 21: two Meshy tasks billed, both orphaned). Fix: (1) BuildUeRig inserts an app-side Deadline leg between mdl and the UE job that resolves the published design glb, runs alc.services.mesh.meshy.run_rig, and writes the rig glb at the rig task's render coordinates - the UE recipe's step 0 already cache-hits when that file exists; (2) add -RenderOffscreen to run_recipe_headless's build launch so no editor window pops on the worker's desktop; (3) give alc_headless_run.py / alc_headless_result.json per-run unique temp names - the fixed names clobber under concurrent runs on one box.
Add an 'expressions' AI service that generates the standard character-expressions video (Ekman 7 + eye states, same slot list and prompt as the Seedance 2.0 expressions workflow) locally via ComfyUI using the open-weights MiniMax H3 video model on the local 4090. Mirror the existing seedance service shape under alc/services/ai/, route through the ComfyUI bridge (ensure_running(), submit workflow JSON, poll, retrieve video), target ~420p output, and harvest expression frames from the resulting video using the existing frame-one harvesting convention so the service is a drop-in local alternative to the Seedance path.
#4270Beat split service: AI breaks action prose into drawable storyboard beatsNew featureBeat SplitclaudeScript
·Service · ai·Alchemy·2 months ago
An alc/services/ai service that takes a script action paragraph and returns an ordered list of BEATS - one per drawable moment - preserving every word and only choosing cut points ('Hogarth sets the metal down, then retreats a safe distance...' -> two beats, two drawings). Structured-output LLM call; commit to ONE model behind the service seam (the one the existing script-analysis services use), not pluggable backends. Surfaced two ways: (1) a first-pass button in the scene script editor beside Generate boards - splits only rows that are not yet voiced or boarded (never re-decides; razor/combine remain the manual gestures); (2) a preflight in the scene build ordered BEFORE the table read and board generation (parse -> beat split -> table read -> boards), since splitting a voiced row invalidates its take timing and splitting a boarded row orphans its drawing. Dialogue blocks stay atomic - a take owns its line.
#4271Previs prep: live roster with removals, and scenes get the Start Previs exitNew featureScene Boardingclaude
·App · transcript·Alchemy·2 months ago
Three connected gaps found in the Iron Giant A rehearsal: (1) the previs-prep band derives from beat rosters but never refreshed - it now rides every rows swap OOB, so tagging a prop (the camera) on a beat shows its slot instantly; (2) no way to remove an asset from the roster - band slots now carry an x (characters/props, transcript editor only) that untags the asset from every beat, unblocking the previs gate without touching the project registry (audit junk like 'off'/'emergencyShut' word-matched into beats was gatekeeping previs); (3) Start Previs was videos-only - scenes now take the same exit at their own coordinate (entity_type shots), and register_scene_roster writes the LIVE editor roster onto the scene's registry entry at previs time, so the published-script roster stops being a stale authority over what the previs imports.
#4272harvest modal duplicates on widget refresh - finished render not shownBug fixLocal Expressionsclaude
·App · web ui·2 months ago
The design-slots widget's harvest modal lives OUTSIDE #design-slots-widget (so slot re-renders don't yank the video mid-scrub), but every htmx POST that re-renders the widget (action refresh/assign/generate, target #design-slots-widget, swap outerHTML) returns the WHOLE design_slots.html render - widget div PLUS another modal + script/style - so each swap leaves an orphaned duplicate #alc-harvest-modal in the DOM. After a harvest finishes, pollOnce()'s refreshSlots -> alcHarvestOpen() updates the FIRST modal in document order while the stale duplicate paints on top: the artist sees an unrefreshed modal and must close/reopen to see the new clip. Affects both engines (Seedance and MiniMax). Fix root-cause: the POST design-slot route should render the widget div only (e.g. ctx['widget_only']=True guarded in the template around the modal/script/style block), so the modal exists exactly once per page.
#4273ComfyUI install plugin: LocalSoftwareVersion setup for the portable buildNew featureLocal Expressionsclaude
·2 months ago
Add alc/plugins/comfyui/setup/ defining class ComfyUI(LocalSoftwareVersion) with a setup() override, TRELLIS-shaped: download the ComfyUI portable Windows build to the tools root (default X:\AI\comfy), verify/upgrade to ComfyUI >= 0.30 (run update/update.py with the embedded python - the H3 nodes need 0.30+, the update also refreshes torch), and write/point ALC_COMFYUI_LAUNCH_BAT at run_nvidia_gpu.bat so alc.services.ai.comfyui_bridge.client.ensure_running() finds it. Idempotent: an existing healthy install is detected (comfyui_version.py) and only updated when below the floor. Follows the SETUP_notes.md doctrine - plugin installs + launches software on a machine, the bridge service only calls it. No UI of its own: it serves the Expressions harvest's local engine.
#4274minimax weights setup: one-command H3 model downloadNew featureLocal Expressionsclaude
·Service · ai·2 months ago
Add alc/services/ai/minimax/setup.py, sam-shaped (python -m alc.services.ai.minimax.setup): download the 8 MiniMax H3 model files from HF Comfy-Org/MiniMax-H3 into the ComfyUI models tree (resolve the tree from the comfyui_bridge workspace / ALC_COMFYUI_LAUNCH_BAT install, not a hardcoded X: path). Derive the file manifest from minimax/models.py constants (MODEL_I2V, MODEL_R2V, TEXT_ENCODER, VIDEO_VAE, AUDIO_VAE, TURBO_LORA_I2V + the other two turbo LoRAs) so there is ONE source of truth for names. ~70GB total: download with resume (curl -C - or ranged urllib), and VERIFY each file's byte size against the HF content-length before renaming .part into place - a short file must fail loudly, never land under the final name. Idempotent: size-matched files are skipped, truncated ones resumed. Print per-file progress and a final manifest table.
#4278when i add a company the app should refresh to that company - i currently have to choose the company from the menu after creating it.New featureAuto-Switch on CreateTom
·App · routes·Alchemy·2 months ago
#4279On the task phase right column - if i'm in "dev" mode i want a "edit recipe" button instead of the + button - this would take me to the recipe's URL in a new tab so i can edit it.New featureEdit Recipe ShortcutTomModel
·Alchemy·2 months ago
#4280Cookbook load is very slow - we need some ideas to make that fasterNew featureCookbook Load SpeedTom
·App · cookbook·Alchemy·2 months ago
#4281[cookbook] [alchemy] [dsn] Implement the Create FDX from json buttonNew featurealchemyExport FDXstub
·Cookbook · alchemy · shelves · dsn·Alchemy·2 months ago
Create an fdx file from the main script json file in the source files directory.
#4282Cookbook Artist UI: default the task tab to the current task, empty state when it doesn't existNew featureTask-Aware CookbookclaudeScript
·App · cookbook·Alchemy·2 months ago
When the cookbook opens on ARTIST UI (Menus / Shelves / Context-Menus), every task tab (DSN, LRA, PRV, SCR, ...) is shown and the first one is selected regardless of the task the operator arrived from. Instead: (1) if a recipe exists for the current task (the task selected in the cookbook's task context / the task the operator came from), select that tab by default; (2) if it doesn't exist, show an empty state for that task ('No scr shelf yet') with a create action that mints the recipe, rather than silently landing on another task's tab.
#4283console on the phase panel should be more verboseNew featureLive Phase ConsoleTom
·Alchemy·2 months ago
i get all this great information in the printout when i run the recipes, i'd like the console to be this verbose:
INFO: 🎬 Recipe wrapper triggered execution for: Alchemy Dsn Build
🚀 Headless Runner | Recipe: dsn.json
📍 Asset: E:\Alchemy\tst\ija\VERSIONS\source\assets\env\homestead\dsn\default\tom\000.000\high
🎬 Running: Check for Front Image...
INFO: Executing action: IdentifyCoreViews
E:\Alchemy\tst\ija\VERSIONS\source\assets\env\homestead\dsn\default\tom\000.000\high
E:\Alchemy\tst\ija\VERSIONS\source\assets\env\homestead\dsn\default\tom\000.000\high
E:\Alchemy\tst\ija\VERSIONS\source\assets\env\homestead\dsn\default\tom\000.000\high
INFO: [cgl_copy_single] Executing: copy "E:\Alchemy\tst\ija\VERSIONS\source\assets\env\homestead\dsn\default\tom\000.000\high\cabin.png" "E:\Alchemy\tst\ija\VERSIONS\render\assets\env\homestead\dsn\default\tom\000.000\high\cabin.png" /Y
Alchemy Image: Thumbnail created. E:\Alchemy\tst\ija\VERSIONS\render\assets\env\homestead\dsn\default\tom\000.000\high\.thumb\cabin.thumb.png
INFO: Found core views: {'main': 'E:/Alchemy/tst/ija/VERSIONS/source/assets/env/homestead/dsn/default/tom/000.000/high\\cabin.png', 'front': [], 'back': [], 'side': [], 'left': [], 'right': [], 'top': [], 'bottom': []}
INFO: [CheckForFrontImage] PASSED: Check for Front Image Passed
self.data {}
saving json E:/Alchemy/tst/ija/VERSIONS/render/assets/env/homestead/dsn/default/tom/000.000/high/env_homestead_dsn.msd
INFO: Version MSD Committed: E:\Alchemy\tst\ija\VERSIONS\render\assets\env\homestead\dsn\default\tom\000.000\high\env_homestead_dsn.msd
✅ Passed: Check for Front Image
⏭️ Skipping (Not applicable to 'env' assets): Check for T-Pose
self.data {}
saving json E:/Alchemy/tst/ija/VERSIONS/render/assets/env/homestead/dsn/default/tom/000.000/high/env_homestead_dsn.msd
INFO: Version MSD Committed: E:\Alchemy\tst\ija\VERSIONS\render\assets\env\homestead\dsn\default\tom\000.000\high\env_homestead_dsn.msd
⏭️ Skipping (Not applicable to 'env' assets): Ensure Headshot
self.data {}
saving json E:/Alchemy/tst/ija/VERSIONS/render/assets/env/homestead/dsn/default/tom/000.000/high/env_homestead_dsn.msd
INFO: Version MSD Committed: E:\Alchemy\tst\ija\VERSIONS\render\assets\env\homestead\dsn\default\tom\000.000\high\env_homestead_dsn.msd
⏭️ Skipping (Not applicable to 'env' assets): Ensure Facial Expressions
self.data {}
saving json E:/Alchemy/tst/ija/VERSIONS/render/assets/env/homestead/dsn/default/tom/000.000/high/env_homestead_dsn.msd
INFO: Version MSD Committed: E:\Alchemy\tst\ija\VERSIONS\render\assets\env\homestead\dsn\default\tom\000.000\high\env_homestead_dsn.msd
🎬 Running: Generate Model...
INFO: Executing StepType.ACTION: GenerateModel
modelling from E:/Alchemy/tst/ija/VERSIONS/source/assets/env/homestead/dsn/default/tom/000.000/high\cabin.png
Creating glb from image E:\Alchemy\tst\ija\VERSIONS\source\assets\env\homestead\dsn\default\tom\000.000\high\cabin.png
glb_file E:\Alchemy\tst\ija\VERSIONS\render\assets\env\homestead\dsn\default\tom\000.000\high\env_homestead_dsn.glb
demo_glb X:\IO\demos\dsn\env_homestead_dsn.glb
Generating data URL for: E:\Alchemy\tst\ija\VERSIONS\source\assets\env\homestead\dsn\default\tom\000.000\high\cabin.png
URL generated, starting mesh generation...
[MESHY] Task 01a090d3-502e-7290-85da-3573f3814ce6 started...
#4284Transcript locations carry INT/EXT scope and time of day, so the exported slugline reads EXT. HOMESTEAD - MORNINGNew featureExport FDXclaudeScript
·App · transcript·Alchemy·2 months ago
The transcript editor's declared locations (TranscriptEdit.locations, alc/models/transcript_edit.py:672 — today a bare list[str] of env ids) gain a scope (INT / EXT / INT./EXT.) and a time of day (DAY, NIGHT, MORNING, ... the KNOWN_TIMES set in alc/models/script/models/fdx.py). Editable in the transcript tool's Locations UI; a beat pinned to a different location can carry its own. TranscriptEdit.to_script_model writes them onto ScriptScene.scope / ScriptScene.time (already exists in the model, alc/models/script/models/script.py:282) beside the scene location it now sets. No exporter change needed: ScriptContainer.from_script_model already composes the slugline from scope + location + time as far as they are known, so the theaxe export goes from 'HOMESTEAD' to 'EXT. HOMESTEAD - MORNING' the moment the data exists.
#4286Storyboard Re-generate ignores the panel's existing frameBug fixIterate On Frameclaude
·App · transcript·2 months ago
When a storyboard beat already has a picture in its media square (dropped from disk, or previously generated), Re-generate should send that frame as the background / composition reference in place of the location's published design (e.g. HOMESTEAD), and the Generation Payload modal should show it as such. Today _visual_gen_payload (app/tools/transcript/transcript.py) only anchors on v.media for slot=style_frame; the storyboard slot rebuilds from the location plate + framing guide as if the square were empty, so the artist's frame is discarded and the payload reads Image 1 = HOMESTEAD.
#4287Company and project dropdown menus should be alphabetized.New featureSorted SwitchersTom
·App · shell·2 months ago
#4288i attempted to createa video with the "Add Video" button - but nothing is created.Bug fixCreate VideoTomVideo
·Alchemy·2 months ago
#4289if i have the project menu active, then select the company menu - i see both - the project menu should hide in this scenarioBug fixHeader SwitchersTom
·App · web ui·Alchemy·2 months ago
#4290Move structure on the left panel in the transcript editor right underneath the characters.New featureScript Rail Section OrderTomTranscript
·App · transcript·Alchemy·2 months ago
#4291Fold Blender Lab blender-mcp registration into alc.services.mcp.registrationChoreMCP Registrationclaude
·Blender·a month ago
Same move #3900 made for comfyui_bridge. blender_bridge/claude_desktop/config.py still carries its own copy of claude_desktop_config_path / _atomic_write_json / timestamped backup / mcpServers merge. McpServerLaunchSpec.to_config_entry(transport) should produce a stdio_entry('blender', command=uv, args=..., env={BLENDER_MCP_HOST, BLENDER_MCP_PORT}) and blender_bridge's register_with_claude_desktop should delegate to alc.services.mcp.registration.register_with_claude_desktop, keeping its public signature so orchestrator.py is untouched. Entry bytes unchanged. Gains Claude Code (~/.claude/settings.json) registration, --dry-run and RegistrationResult.summary for free. Rewrite blender_bridge/tests/test_config_merge.py to mirror comfyui_bridge's. The .mcpb fetch, uv preflight, add-on installer and prefs seeder are NOT MCP structure and stay in blender_bridge.
Phase 1 (spike): determine what Toon Boom Harmony exposes - scripting API (QtScript/JS, plus Python/PyQt hooks if any), batch/headless CLI (HarmonyPremium -batch, -compile, render), import/export formats (XDTS, XML scene files, PSD/PNG sequences, OTIO/EDL if any), and whether an internal MCP/bridge is feasible. Document findings in dev/ and the handbook. Phase 2 (framework): scaffold alc/plugins/harmony following the Blender/Premiere plugin patterns (bridge service + panel + tasks, contract-first DCC layer). Map Harmony scenes/shots/nodes/layers onto Alchemy ontology; import and export scenes and shots; keep all editorial data (timing, sequence order) in our OTIO structure as source of truth; register basic ops via the etymology {dcc}.{verb}.{task}.{op}: open scene, list scenes/shots, import, export, render current scene (batch render to image sequence -> mp4 proxy via OIIO media service). Done = findings doc + working import/export of one scene and a render-current-scene op.
A standard Alchemy menu inside Toon Boom Harmony with Version Up, Build, Render, Review, Publish, defined by the cookbook menu recipe (same source as Blender/Maya menus). Hello-world print statements per item to prove the wiring; install script puts the package in Harmony's user scripts folder.
Generate unoccluded fill-in images and the six ortho views for 6-8 assets from ONE concept, across 2-3 candidate image models/prompt recipes. Produce a contact sheet and a verdict on which model/recipe holds a consistent style. Biggest risk; run first. this is an image inside alchemy we can use as a test: E:/Alchemy/bls/arc/VERSIONS/render/assets/env/botanicGardens/dsn/default/tom/000.000/high/env_botanicGardens_front.png
#4315when i click lanes, then choose my lanes, i'm expecting the popup to close when i click anywhere on the page - right now i have to click "lanes" again to close it.New featureuser
·Alchemy·3 days ago
#4316I want the concept of a "batch" of tickets. It's essenitally what our current sprint is now - a group of tickets. But i'm finding that i need a sprint to be able to contain both batches and tickets. We should create a plan/tickets for thisNew featureuserConcept
·Alchemy·3 days ago
#4317Batch model: WorkItem.batch, WorkBoard.batches/batch_sprints, effective_sprintNew featureBatches and To DoBatchesclaude
·Ontology · models·Alchemy·3 days ago
Add WorkItem.batch (str), WorkBoard.batches (registered containers) and batch_sprints (batch->sprint). Add WorkBoard.effective_sprint(item) = item.sprint, else its batch's sprint. register_batch/delete_batch/prune_batch_if_empty mirror the sprint rules; sprint_progress and prune_current_sprint_if_finished count batch tickets via effective_sprint. A ticket is in at most one batch; a batch in at most one sprint. Tests cover resolver precedence and pruning.
#4318ticket_cli batch commands: create/list/add/remove/delete/plan-into-sprintNew featureBatches and To DoBatchesclaude
·Alchemy·3 days ago
Add 'ticket_cli batch' subcommands: --list, <numbers> --name B (add), --clear (remove), delete, and 'batch plan --name B --sprint S' to place a batch in a sprint. 'show' and 'sprint --list' print batch and effective sprint. Same RESULT line conventions as cmd_sprint.
#4319Kanban board: render batches inside sprint lanes, drag tickets/batchesNew featureBatches and To DoBatchesclaude
·App · kanban·Alchemy·3 days ago
Sprint cards in the Sprints and Current Sprint lanes show their batches as nested groups plus loose tickets. Drag a ticket onto a batch to join it, drag a batch onto a sprint to plan it, drag a ticket out to leave. Progress bars use effective_sprint. Add a Create batch control beside Add sprint.
--- AMENDED 2026-10-24 ---
Retargeted: use the To Do lane (see To Do queue tickets), NOT the Current Sprint lane, which is being retired. Batches nest inside Sprints lane cards only.
#4320sprint_cli and /alc_sprint: resolve effective sprint, batch = default chainNew featureBatches and To DoBatchesclaude
·Alchemy·3 days ago
sprint_cli list/prompt use WorkBoard.effective_sprint so batch tickets appear in their sprint. Rows show their batch. /alc_sprint chain grouping treats a batch as one default chain (still overridable by depends_on and shared-surface rules). Update .claude/commands/alc_sprint.md.
--- AMENDED 2026-10-24 ---
Retargeted: the working set is the To Do queue, not current_sprint; effective_sprint still resolves batch tickets. Overlaps the sprint_cli To Do ticket - do together.
#4321Convert a sprint into a batch (CLI + board) and document batchesNew featureBatches and To DoBatchesclaude
·App · kanban·Alchemy·3 days ago
Add 'ticket_cli batch convert --sprint S' and a board action that turns a sprint into a batch (tickets keep membership, sprint container removed or kept empty by choice). Document sprint/batch/ticket nesting in docs/ and the handbook.
#4322To Do queue model: WorkBoard.todo holding sprints, batches or tickets; migrate current_sprintNew featureBatches and To DoTo Do Queueclaude
·Ontology · models·Alchemy·3 days ago
Add WorkBoard.todo: ordered list of TodoEntry{kind: sprint|batch|ticket, ref}. Helpers: queue_add/remove/move, todo_tickets() (expanded, ordered, de-duplicated), prune_finished_todo() dropping entries whose tickets are all done. Migration on load: a set current_sprint becomes the first entry. Tests for mixed entries, de-dup and prune.
#4323Kanban board: To Do lane accepting sprints, batches and ticketsNew featureBatches and To DoTo Do Queueclaude
·App · kanban·Alchemy·3 days ago
Add a To Do lane between Sprints and In Progress. Drop zone accepts a ticket, a batch or a sprint; each shows as a card (sprint/batch expand to tickets with progress). Drag to reorder. Drop-move route handles the new target; entries drop out when finished.
#4324sprint_cli and /alc_sprint source from the To Do queueNew featureBatches and To DoTo Do Queueclaude
·Alchemy·3 days ago
sprint_cli list/prompt read WorkBoard.todo_tickets() instead of current_sprint; --sprint NAME still overrides. Update .claude/commands/alc_sprint.md wording.
#4325Retire Current Sprint: lane, route, prune, current drop target, docsChoreBatches and To DoTo Do Queueclaude
·App · kanban·Alchemy·3 days ago
Once the To Do lane and sprint_cli To Do sourcing have landed, remove current_sprint, the Current Sprint lane in kanban_board.html and _kanban_modal.html, the /kanban/sprint/current route, the 'current' drop target, prune_current_sprint_if_finished (kanban.py, sprint_cli.py, ticket_cli.py, provenance) and 'current sprint' wording in docs and alc_sprint.md. Existing boards keep working via the To Do migration.
#4329Replace workspace lenses with concept groups in the nav registryNew featureNav ReorgConcept navigationclaude
·App · shell·Alchemy·3 days ago
Drop VIEW_MODES and the per-lens SIDEBAR_NAV sets. SECTIONS becomes CONCEPTS: concept -> sub-pages (see dev/nav_reorg_outline.txt). TOOL_PATHS and nav ids stay stable so walkthrough page references and deep links keep working. Kanban moves under My Work, Production Tasks under Pipeline. Remove restore_view_mode lens recovery from shell_restore and fix tests/test_shell_restore.py. Landing falls back to My Tasks (Home comes later).
#4330Rebuild the sidebar as accordion concept groupsNew featureNav ReorgConcept navigationclaude
·App · web ui·Alchemy·3 days ago
Single rows: Shots, Assets, Library. Expanding groups: My Work, Story, Media, Plan, Insights, Pipeline, Studio. One group open at a time; the active page's group opens on load; clicking a group opens its first sub-page; open group remembered per browser; collapsed sidebar shows group icons with a flyout. Remove the header workspace switcher.
#4331Shots hub page with All / Scenes / Videos chipsNew featureNav ReorgShots hubclaude
·App · routes·Alchemy·3 days ago
One Shots page replaces the Scenes and Videos sidebar rows. Filter chips All | Scenes | Videos; chip is URL-addressable so existing /scenes and /videos routes keep working as deep links. Sequences are scenes - no separate level.
#4332Assets hub page with category chipsNew featureNav ReorgAssets hubclaude
·App · routes·Alchemy·3 days ago
One Assets page replaces Environments, Characters, Props, Dressing rows. Chips All | Characters | Environments | Props | Dressing, URL-addressable; existing /assets/<category> routes stay as deep links. Reuse the Shots hub chip component.
#4333Remove the Assessment pageChoreNav Reorgclaude
·App · routes·Alchemy·3 days ago
Nobody knows what Assessment does. Grep the routes, templates, tests and walkthroughs for use first; if unused, delete the page, its nav id and TOOL_PATHS entry. If something uses it, stop and report instead of deleting.
#4334Unreal / File system toggle on the version pageNew featureNav ReorgUnreal mode toggleclaude
·App · routes·Alchemy·3 days ago
Version page gets a switch: File system | Unreal. In Unreal mode the page shows UE information for the task instead of disk. The Unreal location picker lives inside the toggle and auto-picks (one connected editor, else project default, else last used) so most users never choose. Replaces the Unreal lens and the Locations sidebar row; fold the unreal_versions route split into the one version page.
While in Unreal mode: persistent colored frame or banner on the page, the connected location name in the header, one color used nowhere else in the app, and the mode never changes silently (survives navigation within the version page; switching is always an explicit click). Verify rendered, not just DOM.
#4336Retire the Unreal lens and Locations sidebar rowChoreNav Reorgclaude
·App · shell·Alchemy·3 days ago
After the toggle ships: remove the unreal lens, unreal_locations and unreal_versions nav ids, and the Locations sidebar row. Keep /unreal/locations reachable from the toggle's picker. Fix shell-restore for old unreal URLs.
#4337Update handbook and walkthroughs for the new navigationChoreNav Reorgclaude
·App · shell·Alchemy·3 days ago
Handbook (/handbook) and walkthrough steps that mention lenses, workspace switcher, Scenes/Videos/Characters rows or the Unreal lens. Walkthrough page references are by nav id, so check each still resolves.
#4338Batches can depend on other batches or ticketsNew featureBatch dependenciesclaude
·App · kanban·Alchemy·3 days ago
Today only a ticket can declare depends_on, and the sprint skill treats any declared dependency as 'same chain', so cross-batch ordering forces serial execution. Add batch-level depends_on (a batch waits on another batch or a ticket; ticket_cli batch --depends-on / --remove), shown on board cards and in sprint_cli list as [BLOCKED by batch X]. Sprint skill rule: a batch is one chain; batches with no dependency path between them run in parallel, dependent batches run after the blocker lands. Without this, Nav Reorg batches 'Shots and Assets hubs', 'Unreal mode' (after 'concept shell') and 'docs' (after all) cannot be expressed.
#4340Track AI spend: cost per generation run in telemetry, rolled up per task/shot/projectNew featureclaude
·Service · ai·Alchemy·3 days ago
Every AI generation run (image, video, later text/audio) records what it cost, so people can see AI spend and predict costs. Capture usage at the same transport seam telemetry.run() already wraps (nano_banana client.generate_content, seedance fal_client.subscribe): tokens/images/seconds from the response usage metadata, model id, provider. Compute cost from a small versioned rate table (per model: per image / per 1M tokens / per video second) stored as data, not code, so price changes don't need a deploy. Store raw usage AND computed cost, plus the rate-table version, so old runs can be re-priced. Failed runs record usage too (a 429/402 still costs attempts). Roll up by task, shot, asset, project, model and day; show in the analytics panel. Also expose an estimate op: given a planned batch (N assets x M views x model) return predicted cost before running.
A clear service, breakdown_environment_to_assets(concept_image, ...) -> manifest of assets: id, description, bounding box/crop in the concept, category, occlusion flag, relative scale hint (human scale reference), and a crop image per asset. Built as a harness with a model-agnostic interface so the detecting/segmenting/describing model (VLM, SAM, etc.) can be swapped per call or per config, and so the same manifest can be produced by different backends and compared. Every stage emits inspectable output (manifest json, overlay of boxes on the concept, crop sheet). Reuse the Alchemy ontology for manifest fields.
#4344There's about a 3 second lag when i drag a batch into a sprint or when i delete something or when i drag something from one column to another.Bug fixBoard Responsivenessuser
·Alchemy·3 days ago
#4346Asset naming modal: review breakdown thumbnails, give each asset a category, name and variantNew featureclaude
·App · library·Alchemy·3 days ago
A tool on the DESIGN (dsn) task page, defined in the cookbook like Alchemy's other tools (alchemy/tools.cgl + the dsn task), that opens a modal. ENTRY POINT: a right-click context-menu item 'Breakdown Environment' on environment design assets (cookbook alchemy/context-menus.cgl). It lets the person choose the EXACT file to break down (the design asset they right-clicked), and it creates the 'assets' folder in that PathObject's render directory. THE FOLDER IS CREATED THROUGH THE COOKBOOK TOO: the menu item runs a cookbook recipe step (software-first, in the alchemy software, e.g. alchemy/<stage>/<task>/BreakdownEnvironment.py) that makes the folder via PathObject, and then opens the modal; the app does not hardcode the mkdir. ENABLEMENT: the modal tool on the design page is disabled unless the render directory of the PathObject being worked on contains an 'assets' folder (that folder is where the isolated images will land), so the right-click step is how that folder comes to exist; the disabled state should say why and point at the right-click item. THE MODAL: given the chosen concept image it runs (or loads) the environment breakdown (#4341) and shows one row per detected asset: crop thumbnail, category (select, from the asset registry's categories), name (text), variant (text, defaults to none). Names default to assetA, assetB, assetC... in manifest order and are fine to keep. A row can be rejected so only kept assets proceed. Validate: name required, unique per category+variant. Save writes a curated manifest next to the breakdown manifest (kept rows only: category/name/variant plus source box and crop id), the input to the next step that registers the assets and renders isolated images into render/assets/<category>/<name>/... Naming only: it does NOT generate images.
#4347Board Play + drag-to-link; batch/sprint skills split; batch dependenciesNew featureBoard Play and Linksclaude
·App · kanban·3 days ago
Kanban board: Play button launches a ticket/batch/sprint in a new live Claude session; drag a link handle between tickets/batches to declare hard dependencies (chips with x to remove); batches can wait on tickets/batches and tickets on batches; /alc_batch (old sprint workflow) and a new /alc_sprint orchestrator that plans parallel waves; sprint_cli --batch and --via ledger labels.
#4348Env dsn publish: register curated assets, create each asset's first design version, copy its image into sourceNew featureclaudeDesign
·Cookbook · alchemy · publish · dsn·Alchemy·3 days ago
Publish-recipe steps (cookbook alchemy/publish/dsn, software-first) that consume the curated.json written by the #4346 asset naming modal (source/breakdown/<concept>/curated.json, kept rows only: category/name/variant, id, box, crop). Per kept row: (1) REGISTER the asset through the single register_asset() choke point under its category (chr/env/prp/drs), name and variant; (2) CREATE THE FIRST VERSION of that asset's dsn (design) task via PathObject, so it is a normal design version; (3) COPY the asset's image into that first version's SOURCE folder. The image is the box crop from the concept (write the crop file if crops/<id>.png does not exist yet: the modal only cut thumbnails on demand, so curated.json crop paths do not exist on disk). Steps are separate recipe steps, idempotent (re-running over an already registered/created asset is a no-op), and refuse a curated.json whose category/name/variant collide with an already-registered asset. No background cleanup here: that is a build-phase concern of each asset's own design task.
#4350i need right click delete on ticketsQuick entry · agent-ready tickets
·3 days ago
#4352Extract pose_picker's 3D tumble/pan/zoom into a shared viewport-navigation module (+ F to frame)ChoreB-4Environment Scale ReferenceViewport Navigationclaude
·App · image_editor·3 days ago
Pull the camera navigation out of app/tools/image_editor/templates/pose_picker.html into a shared JS module any three.js view can use: Blender-style tumble (orbit), pan, dolly/zoom, and 'F' to frame a given bounds (grid, object, or selection). pose_picker keeps working unchanged on top of the module. No new UI beyond F-to-frame.
#4353Scale reference stage: 100m x 100m grid + camera with environment design as image-plane backdrop, lens controlNew featureB-4Environment Scale ReferenceScale Reference Stageclaude
·App · image_editor·3 days ago
From an environment design image, open a 3D 'Scale Reference' view: a 100m x 100m ground grid (metres) and a viewing camera whose image plane shows the design as its background. User controls the camera lens (focal length in mm, sensor/FOV sensible default) and moves the camera with the shared Blender-style tumble/pan/zoom; 'F' frames the grid. The backdrop stays locked to the camera view (screen-space image plane, aspect-correct) so the grid can be lined up to the drawing's perspective.
#4354Scale reference: place 3 human figures with a ground-plane move control, always facing the cameraNew featureB-4Environment Scale ReferenceScale Humansclaude
·App · image_editor·3 days ago
In the Scale Reference view, add up to 3 human reference figures (default ~1.7m tall, simple billboard/mannequin). Each has a move control (drag on the ground plane, optionally height/XYZ numeric) and always rotates to aim at the camera (yaw-only billboard, stays upright). User drags them to where they sit correctly in the drawing's perspective so they read at the right scale; figure height editable per human (e.g. child/adult) so scale can be tuned.
#4355Scale reference: save the setup (camera, lens, humans) and report the derived scale on the environment designNew featureB-4Environment Scale ReferenceSaved Scale Setupclaude
·App · image_editor·3 days ago
Persist the Scale Reference setup as JSON beside the environment design (camera transform, lens, backdrop ref, human positions/heights). Reopening restores it. Expose the derived scale (metres per image unit at the humans' depth, camera distance to each human) so downstream tools (previs, 3D layout) can reuse it. Optionally export a composite still of the humans over the design.
#4356Mint permanent numeric IDs for batches (e.g. B-3), shown on the board and usable in ticket_cli and skillsNew featureB-4Environment Scale ReferenceBatch IDsclaude
·App · kanban·3 days ago
Batches are currently just a name string on each ticket, with no ID. Give every batch a board-minted, permanent, never-reused number (displayed 'B-<n>', like tickets get #n). Show it on the batch box and card/modal batch chips. ticket_cli (batch --list/--plan/--unplan/--delete, link --batch/--depends-on-batch) and the alc_batch/alc_sprint skills accept either the B-n or the name. Renaming a batch keeps its number. Existing batches (Concept-to-Layout A/B/C, Environment Scale Reference) are backfilled with numbers on first load, oldest first, without changing their names or ticket membership.
#4357Make PathObject preview/thumb/proxy paths extension-aware so every source file has its own unambiguous mediaNew featureB-5Per-File MediaPer-file media pathsclaude
·Core·3 days ago
Media paths derived in alc/path/path_object.py (get_preview_path, get_thumb_path, get_proxy_path) currently take their name from the version, so several files in one version can collide. Apply ONE uniform rule that includes the source extension: {stem}.{ext}.{media_ext}, e.g. image.psd -> .thumb/image.psd.png, image.png -> .proxy/image.png.png, image.jpg -> .proxy/image.jpg.png. No per-format exemptions (image.png and image.jpg must not map to the same name). Keep the .preview/.thumb/.proxy folders and nested-subpath mirroring. Migrate existing literal-path consumers (version/task/review/shotgrid/blender callers) and leave old on-disk files readable via fallback until regenerated.
#4358Manifest file objects: source_files/render_files entries carry path, preview, thumb, description, refreshed on every version loadNew featureB-5Per-File MediaPer-file manifestclaude
·Ontology · models·3 days ago
Change source_files and render_files in the version .msd from ext -> [path strings] to a list of file objects: {path, preview, thumb, description, exists, mtime} (paths from the extension-aware PathObject rule, #4357). On EVERY load of a version, rescan the source and render folders and refresh ONLY those two sections, merging by path: update path/preview/thumb/exists/mtime, add new files, mark missing ones, and PRESERVE description (and any other per-file metadata) so nothing a user or AI wrote is clobbered. No other manifest field (status, history, work_log, etc.) is touched by the refresh. Readers fall back to computing the path if an entry is missing, so a stale manifest costs speed, not correctness. Migrate old string-list manifests on first refresh.
--- AMENDED 2026-10-24 ---
MODEL: the per-file attrs (preview, thumb, description, exists, mtime) are added to the AlcFile model (alc/file/core.py, today just path + tags), so the manifest entry IS a serialized AlcFile and every consumer gets the same fields. The manifest stores AlcFile.model_dump() per file; the refresh builds AlcFile objects from disk and merges by path.
--- AMENDED 2026-10-24 ---
REFRESH = REPAIR: the on-load refresh is also the repair path. Run against any existing manifest (old string lists, empty lists, stale entries, missing preview/thumb, entries for deleted files) it brings source_files/render_files to the correct state, so no separate repair script is needed (expose the same function as a CLI/batch command to sweep all versions). IDEMPOTENT / NO-OP: compute the desired file-object lists, compare to what is stored (ignoring nothing but volatile ordering), and if identical do NOT write the .msd at all (no mtime bump, no rewrite). Write only when something changed, and only those two sections.
#4359AI-generated description for every image in a version's file objectsNew featureB-5Per-File MediaImage descriptionsclaude
·3 days ago
For each image file object in a version's manifest (design files first, e.g. env dsn previews), generate an AI description with a vision model through the AI services framework and store it in the file object's description field. Generate when a file has no description or its mtime changed since the description was written; never overwrite a hand-edited description. Show and edit the description wherever the file's preview is shown.
--- AMENDED 2026-10-24 ---
Store the AI text in AlcFile.description (alc/file/core.py), the same field the manifest file objects serialize.
#4360Change "Kanban" to "Code Kanban" - put a placeholder for Creative Kanban as well. it doesn't need to do anything yet - just a grey placeholder.New featureuser
·Alchemy·3 days ago
#4376i'm on this page (the characters page) after clicking the back button from the version you see in my url: http://127.0.0.1:8080/c/cgl/project/lib/versions/at/assets/chr/genMan/mdl?u=tom&v=000.000 but the url didn't update when i clicked back, just the view. now i have to refresh for the app to figure out whtere it's atBug fixuser
·Alchemy·3 days ago
#4387[cookbook] [alchemy] [dsn] Implement the Ensure Design Camera buttonNew featurealchemyDesign Camera Checkstub
·Cookbook · alchemy · publish · dsn·Alchemy·3 days ago
Ensure a design camera is set for this environment asset by checking for the json file created by the Scale Reference tool.
#4388when i drag a step - we get duplicate steps as well as cuplicate "Active Steps" in the panelBug fixuser
·Alchemy·3 days ago
#4390Design image-role context menu on source file rows (cookbook-defined)New featureImage Rolesclaude
·App · web ui·3 days ago
Right-clicking an image row in Source Files of a design (dsn) version offers a design-specific context menu, defined in the cookbook (context-menus recipe type), to set the image as main/front/back/left/right/top/bottom. Choice is written to the MSD data fields (one image per role, setting a role moves it from the previous holder). Available roles vary by design type (character, prop, environment) and come from the cookbook definition.
#4391Show image-role pills on file rows; make main image obviousNew featureImage Rolesclaude
·App · web ui·3 days ago
File rows in Source Files and Renders/Exports show a pill (MAIN, FRONT, BACK, LEFT, RIGHT, TOP, BOTTOM) for images carrying that role in the MSD data. The main image is much more visually prominent (stronger pill/highlight, and surfaced in the version header slots) and is settable in one click.
#4392Tagging a source image with a role copies it into renders as the tagged imageNew featureImage Rolesclaude
·Cookbook engine·3 days ago
When a source file is given a role (main/front/back/left/right/top/bottom), immediately copy it into the renders/exports area; that copy becomes the actual role image referenced by the MSD (and by the Ensure Main Image publish step). Re-tagging replaces the copy; untagging removes the role reference.
#4393Add word wrap to the titles of tickets so that we can read the full title even on a narrow column.New featureuser
·Alchemy·3 days ago
#4394when i click any of the filters (All, Characters, Props, Dressing) after entring this page by hitting the back button of the versions page - nothing happens.New featureuser
·Alchemy·3 days ago
#4395i want the ability to right click on a left panel item and add it to "My Work" almost like a shortcut. for example i'd want to do that with the "CookbooK" linkNew featureMy Work Shortcutsuser
·Alchemy·3 days ago
#4397I need to be able to see the "Applies to" menu on Artist UI elements like i have on phases. some of these dsn menu items for example only apply to environments etc...New featureuser
·Alchemy·3 days ago
#345Author the BP_MagiCam rig Blueprint asset — caddy controls are dead without itBug fixMagiCam Control RigMagiCam Control RigclaudeCamera
·Unreal Engine·2 months ago
Create the BP_MagiCam Blueprint asset in the UE project (per MAGICAM_PORT.md): nested Scene Components Base -> Height -> Dolly -> Tilt -> CineCameraComponent, plus Plane_01..10 image-plane components with a translucent material exposing an Opacity scalar param. Then have prv.create.create_magicam_from_shot spawn BP_MagiCam per shot instead of a bare CineCameraActor, keeping the {shot}_mCam label convention and the storyboard plate on Plane_01.
--- AMENDED 2026-08-13 ---
SPEC-FIRST AMENDMENT (MagiCam Control Rig sprint): build the rig from magicam_rig_spec.json (#3976), NOT from the component order written above. Corrections from the template dump: control hierarchy is main -> dolly -> height -> tilt (the Base->Height->Dolly->Tilt order in MAGICAM_PORT.md / #258 is wrong); the image-plane controls are children of TILT (they follow tilt rotation), not of the root; each control gets its real gizmo shape from the shape package (#3977), per-channel locks/limits per spec (dolly travels only along its rail, tilt translation pinned), and is built on whichever vehicle the spike (#3978) picks - Control Rig asset vs gizmo components. The existing spawned-per-shot plumbing and the *_mCam label convention stay as written.
#423[cookbook] [alchemy] [dsn] Implement the Ensure Design Model buttonNew featurealchemystub
·Cookbook · alchemy · publish · dsn·Alchemy·3 months ago
Checks for a .glb file in the source / render directory, copies it to the render directory if found in source.
#1027Register a beat board's cast + locations as a SceneAsset (and stop script publish omitting videos)New featureScript Start PrevisclaudePrevis
·App · transcript·Alchemy·3 months ago
A beat board has a cast, but nothing registers it, so a board's previs has nothing to query (see the read-side ticket, which teaches the import steps to ask the registry).
Scenes already work: the script publish writes SceneAsset.character_ids / prop_ids / location into scn.json. A video has no entry, so the same query comes back empty for a board.
DECIDED: REUSE SceneAsset. A video and a scene are the same thing functionally and from a modelling point of view - a story-unit container with a location, a cast, props and a duration. SceneAsset already carries every field a board needs. A thin wrapper/alias may be added for syntax integrity (reading VideoAsset where a video is meant) but it should be the SAME model underneath, not a parallel one to keep in sync.
WHAT TO BUILD
Persist the board's roster as a SceneAsset keyed by the video slug: the locations the video declares plus every character and prop tagged on its beats. That roster is ALREADY COMPUTED - _prep_band_ctx in app/tools/transcript/transcript.py derives it (doc.location_usage() plus the beats' character/prop tags) to drive the prep band's readiness. This is persistence, not derivation; do not build a second roster that can disagree with the band.
THE ONE THING THAT WILL BITE IF IT IS MISSED
SceneRegistry's publish merge ends with:
for scene_id, scene in self.scenes.items():
if scene_id not in seen:
scene.status = OMITTED
Anything in the registry that is not in the freshly published SCRIPT is marked omitted. A video is never in the film's script, so as written, every script publish would silently mark every video OMITTED - and a board's previs would go empty some days after it last worked, for a reason nobody would connect to publishing the script.
Use the discriminator that is already there: SceneAsset.type (default "scenes"). Register videos as type "videos", and make the omit sweep consider only scene-typed entries. That is the "wrapper for syntax integrity" in its cheapest honest form - one field, no parallel model.
ALSO CHECK, same cause:
* Scene-facing surfaces read the registry - the Scenes count in the rail, the schedule, the tracker. They should filter on type, or videos will start appearing as scenes.
* Keyspace: scenes are Dict[str, SceneAsset] keyed by id, so numeric scene ids ("010") and video slugs ("theaxe") coexist safely. Worth stating that a video named like a scene number would collide, and deciding whether that is worth guarding.
WHEN IT UPDATES: the board is edited constantly, so decide whether the roster is written on every edit, on previs, or on an explicit publish. Writing it as part of previs prep is the cheapest correct answer and keeps Start Previs one click.
DONE WHEN: a beat board's cast and locations are registered as a SceneAsset the previs import can query; the import step uses ONE code path for scenes and boards; a script publish does not omit videos; scene-facing UI does not start listing videos; and a board's previs contains its own cast and nothing else.
#3491Analytics: task effort table — schedule days and artist time next to machine timeNew featureAnalytics & Effort Attributionclaude
·App · analytics·3 months ago
The Production lens answers "how long did Alchemy run" (56.7 m total on ppe) but not "how long did this take to get through the shop". Add a TASK EFFORT table to the Production lens that puts three units side by side per task.
Columns, per task row:
- Machine — sum of event durations (already computed by telemetry.aggregate).
- Artist — sessionized human time (time_actuals), measured-span only; see the ramp fix below.
- Touches — number of distinct work sessions that included this task.
- Days — distinct LOCAL calendar days with >=1 event on the task.
- Span — last-event day minus first-event day + 1 (calendar days elapsed).
- Leverage — Artist / Machine ratio.
All six derive from data already parsed into TelemetryEvent (ts, duration, task_id, user). No new capture, no schema change, no writes under PUBLISHES.
Add project-level KPI tiles above it: active days, elapsed span (first..last), session count, machine total, artist total.
WHY THIS SEPARATES THE TWO KINDS OF WORK (real ppe numbers, verified):
oswald - Design 13.6 m machine / 16.6 m artist = 1.2x | 2 days | 4 span | 2 sessions
oswaldComputer - Design 4.6 m machine / 6.7 m artist = 1.5x | 1 day | 1 span | 1 session
videos_idesignalchemybuilds_seq_scr 1.8 m machine / 9.3 h artist = 305.0x | 4 days | 4 span | 5 sessions
videos_kanbanworkflow_seq_scr 1.4 m machine / 1.4 h artist = 58.0x | 2 days | 5 span | 2 sessions
Design tasks: leverage near 1, one or two sessions, span == active days -- published once, never reopened. Script/edit tasks: leverage in the hundreds, many sessions, span WIDER than active days -- worked across a week between other things. That contrast is the point of the view.
REQUIRED FIX -- SESSION_RAMP_SECONDS pollutes per-task artist time:
time_actuals.SESSION_RAMP_SECONDS (45 min) is credited to whichever event ends a session, so it lands on an arbitrary task. On ppe production events, 10 of 28 tasks read EXACTLY 45.0 m -- that is the constant, not measured work. Of 26.9 h reported artist time, 11.2 h is ramp and only 15.6 h is measured span.
Fix: per-task Artist = measured span time only (tail_seconds=0). Report the ramp once as a project-level line ("+11.2 h session ramp, unattributed"). For a task whose only signal is a session tail, render "single touch" rather than a fake precise duration.
SECOND FIX -- exclude the code telemetry buckets by task_id, not label:
The code / cookbook / automation buckets carry 3,168 h of backfilled git history and swamp any project total. Their task_ids are "code", "cookbook", "ppe_automation" while their labels are "code", "cookbook", "automation" -- filtering on task_id silently misses ppe_automation. Filter on recipe (cookbook/menus/architect) or on the label set, and state on screen that they are excluded.
NICE TO HAVE (the visual that makes it obvious at a glance):
A per-task calendar strip -- one cell per day across the filtered window, filled on days the task saw events. Design tasks render as one or two adjacent filled cells; scr tasks render as scattered cells across two weeks. Same bar_chart_svg / inline-SVG approach already used in telemetry.py, no chart library.
SCOPE: read-only, additive. New reduction functions in time_actuals.py, new SVG builder in telemetry.py, new section in partials/analytics_body.html. Reuse the existing scanner, filters and mtime cache -- no second scan.
ACCEPTANCE: on /c/cgl/project/ppe/analytics the Production lens shows the table; oswald - Design reads leverage ~1x with 2 sessions, videos_idesignalchemybuilds_seq_scr reads 4 active days across a 4-day span with 5 sessions; no task displays exactly 45.0 m of artist time; project totals exclude the code/cookbook/automation buckets.
#3997Control shape orientation + scale are hardcoded guesses - bake the corrected values into the rig builderBug fixMagiCam Native UE PluginRig Control Shapesclaude
·Plugin · unreal·Unreal Engine·2 months ago
_SHAPES in alc/plugins/unreal/magicam/spec.py carries hand-guessed rotations and unit-scale sizes that were never run through the Blender->UE axis conversion, so gizmos point the wrong way (height pointed forward instead of up) and are 1-3 cm on a 150 cm rig. Corrected values were derived empirically in the live editor 2026-08-13 and are currently saved in the CR_MagiCam ASSET only - a rebuild from spec.py would revert them. Bake these in (pitch, yaw, roll / uniform scale): main 0,0,0 / 90; dolly 0,-90,0 / 35; height 0,0,90 / 35; tilt 0,0,90 / 45; image_plane_NN_ctrl 90,0,0 / 30; lens_chooser 0,0,0 / 6; lens_grouping 0,0,0 / 4 (hidden). KEY FACT that cost the iterations: UE's Arrow_Thick gizmo points along its local Y, not X - rotating about Y (pitch) does nothing to it. Also size shapes rig-relative rather than absolute so they stay readable if rig scale changes.
·Cookbook · alchemy · build · dsn·Alchemy·2 months ago
If the facial expressions don't exist - we need to create them - using the same tooling we use to generate the 420p video of all the expressions from the head shot.
when we click the "clip" button and render all/rerender all.
we also need a way to auto-assign facial expressions, i know we already have a video here: "E:\Alchemy\cgl\hag\VERSIONS\source\assets\chr\hogarth\dsn\default\tom\001.000\high\chr_hogarth_dsn_expressions.mp4" so we could potentially start by figuring out how to find the best frame for each expression.
we can use this asset as an example: E:/Alchemy/cgl/hag/VERSIONS/source/assets/chr/hogarth/dsn/default/tom/001.000/high
Once the facial expressions exist - copy them to the render directory and register them in the msd.
#346Camera Caddy: make every rig control functional per camera (Blender MagiCam parity in Unreal)New featureMagiCam Control RigMagiCam Control RigclaudeCamera
·Plugin · alchemy camera caddy·Unreal Engine·2 months ago
Every Camera Caddy control must do real work on EVERY camera in a sequence, matching what the Blender MagiCam caddy gives an artist. Depends on #345 (author BP_MagiCam and spawn it per shot). Per control: (1) Base/Height/Dolly/Tilt - select the named sub-control so the viewport gizmo grabs THAT control, not just the actor; UE has no Python API to select a sub-component, so this likely needs the Blueprint to expose each control as a child ACTOR (or an editor-utility selection helper). (2) Opacity - needs the plate material to be translucent with an Opacity scalar param on a dynamic instance; today the amc plate material is opaque unlit with no param, so the slider is inert. (3) Key Camera - key the four control transforms into the shot's camera-cut range at the playhead. (4) Copy/Paste pose - keyed by control name so a pose maps across shot rigs. (5) Plane depth/visibility/texture - already work on the amc plate; must keep working on BP_MagiCam's ten planes. (6) Body/Lens - verify filmback + focal length drive the frustum refit on both rig flavours. Acceptance: with 32 cameras in a sequence, an artist can pick any camera in the caddy and pose/key/adjust it entirely from the panel.
#533Visual beats: explicit FRAMING reference + show what the AI is usingNew featureclaude
·App · transcript·3 months ago
A transcript visual beat can today feed three reference roles to nano-banana, resolved from its tags in _visual_gen_payload (transcript.py): location plate, characters, props. There is no way to say 'compose it like THIS'. An image dragged onto a beat becomes v.media - display/output only, never sent as a reference - and _generate_visual_image writes gen_<visual_id>.png over that same field, so the first Re-generate DESTROYS the picture the artist dropped in as a framing guide.
The script-side composer already solved this: build_render_prompt (alc/services/media/storyboard.py) orders references environment plate, camera framing guide, speaker, remaining cast - driven by block.camera, a Shot from alc/models/framing.py (Framing/ShotRole/CameraAngle/CameraMovement, each rung carrying a prompt phrase and a guide_image). The transcript beat is the sibling that never got the framing role. Converge, do not invent.
WHAT IT SHOULD DO
1. Model - VisualDescription (alc/models/transcript_edit.py) grows camera: Shot (the existing vocab) and framing_media: Optional[str], a root-relative path to a dropped composition plate. framing_media is a SEPARATE field from media so a generate can never overwrite it. Setters on the doc model (set_visual_framing_media / set_visual_camera) following the set_visual_media pattern, plus routes mirroring upload_visual_media / copy_visual_media / clear_visual_media.
2. Composer - _visual_gen_payload gains a fourth reference role, framing, ordered FIRST (Image 1). A dropped framing_media plate wins over the framing term's generic guide_image; the term's prompt phrase rides the prompt text either way. Callout must be layout-only, e.g. 'Image 1 is the framing guide - match its composition, camera angle, subject placement and scale; take the layout only, not its content or style.' Carry over build_render_prompt's rules: skip the abstract guide on wide/establishing (the environment plate already carries the framing), and the shallow-DOF phrase for anything tighter than medium-wide. _generate_visual_image needs no change beyond reading the new reference list, since it already sends payload['references'] verbatim.
3. Drop semantics - the framing slot is a drop target on the visual tile (drag from disk, browse, or drag another beat's thumbnail onto it). A beat-to-beat drag COPIES the file into the script media tree under a deterministic framing_<visual_id> name, same reasoning as #528: the image editor saves back in place, so a shared path would make retouching one beat rewrite another beat's guide. The framing plate must not be cleared by clear_visual_media or replaced by a generate.
4. Disclosure panel - a shared partial (e.g. partials/visual_references.html) rendering FOUR labeled groups with thumbnails and published/missing state: FRAMING, LOCATION, CHARACTERS, PROPS. It renders in the PREVIEW PANE (#trs-preview in transcript_main.html) for the beat currently on screen, so the frame and the ingredients that produced it read as one unit, and the SAME partial replaces the flat 'Reference images' list in the Generation payload modal (partials/visual_payload.html). Both surfaces must be fed by the same resolver the real generate uses - one reader, so the panel, the modal and the actual nano-banana call can never disagree about what the AI is getting. Each row states its role in one phrase and where it came from (published dsn design vs dropped plate vs framing term guide); flagged-but-unpublished assets keep the existing amber treatment.
WHY: the artist needs to know, without guessing, that the app is using the dragged-in image as framing, the Homestead as the location, and the Jack/Coffee Cup designs as the cast and prop references. Today three of those four are invisible and the fourth does not exist.
OUT OF SCOPE: video-level default framing (location-style inheritance); a one-click 'use the previous beat's image as my framing'.
#670Beat board: drag a block's OUT point to set its screen time — ripple, never gapsNew featureApp · transcriptclaudeScript
·App · transcript·Alchemy·3 months ago
Playing a first-pass storyboard back makes the timing problems obvious, but a beat's screen time can't be changed. Give the beat board ONE edit: drag a block's OUT point.
THE CONSTRAINT THAT MAKES THIS EASY (do not relax it)
This tool is a strict 1:1 written beat : visual exercise for getting the first pass of storyboards out. There are NO gaps, no moving clips, no left-edge trims and no overlap math. A block's position is the sum of what precedes it, so changing one duration IS the ripple — every later clip re-derives its place with nothing to slide. Anything that can open a hole in this lane is out of scope; the anchored transcript lane is where gap/overlap editing lives (set_block_time / clamp_trim / move_no_overlap), and none of it should be reached from here.
WHAT TO BUILD
1. RIGHT EDGE ONLY. Sequenced clips are currently emitted with ro: True (see ScriptTimeline metadata) and app/static/js/timeline.js only seeks on them (the pointerdown handler returns early). Allow a right-edge grab on the SEQUENCED lane: mode 'r' already exists with snapping, the out-point tooltip and onTrim — this is opening a door, not building one. No left handle, no move.
2. WHAT THE DRAG WRITES. A per-BLOCK explicit screen time on TranscriptRow. It must NOT rewrite word times: those are row-local and drive the word-level playback highlight, and a re-voice would overwrite them anyway. Note row.t_start/t_end exist today but are documented as bounds for a WORDLESS visual block and ignored when a row has words — either widen that contract deliberately or add an explicit field; don't quietly overload it. The beat's visual follows the block 1:1, so there is exactly one duration per row.
3. DERIVED STAYS DERIVED. With no override a block keeps its current length: the take's real spoken length when voiced, the word-count estimate (sec_per_word=0.4) when not. Double-clicking the out point clears the override and returns to derived — a re-voice must not be trapped behind a stale hand-set length.
4. CUTTING INTO A TAKE IS ALLOWED, AND SAID OUT LOUD. Shortening past the take's audio is exactly what a storyboard pass wants (the line runs long, the picture shouldn't). Permit it, and mark the clip as clipping its voice rather than silently truncating audio.
5. PER-CLIP MUTE. Track mute/solo already exists (TrackState + set_track_state); add mute for ONE block, so a trimmed block can be silenced deliberately instead of playing a chopped word. The create-mode virtual transport (scriptMedia in transcript_editor.js) must still HOLD that block's duration while playing no audio — muting changes sound, never timing.
6. LIVE RIPPLE. While dragging, later clips move with the edge — the point is judging the new rhythm, and a commit-time-only reflow makes that impossible to feel.
7. Runtime totals (structure gauge / target_duration) read the new length; nothing else should need teaching.
DONE WHEN: dragging a block's out point changes its screen time and shifts everything after it with no gap and no overlap; the visual beat matches its block exactly; double-click restores the derived length; a block trimmed shorter than its take reads as clipping and can be muted on its own; and playback through the transport matches what the lane shows.
#1026Previs imports should query the scene registry, not glob every asset on the showNew featureScript Start PrevisclaudePrevis
·Plugin · blender·Alchemy·3 months ago
Previs imports every published asset on the show instead of the ones the thing being previs'd actually contains.
ImportCharacterRigs globs every chr/*/rig publish and lays them on a 5-wide grid. ImportEnvironments globs every env/*/mdl the same way. On a real project that means a beat board for "theaxe" gets bugLord and every other character in the show, and a scene gets the entire cast list regardless of who is in it. The artist deletes what they do not need before they can judge a camera.
It went unnoticed for a good reason (Tom): the early tests ran in a project whose only assets WERE the test assets, so "every rig on the show" and "the cast of this thing" were the same set. It only diverges once the project is real.
THE REGISTRY ALREADY HOLDS THE ANSWER — THE WRITE SIDE IS DONE
SceneAsset (alc/models/script/models/manifests.py:1317) already carries character_ids, prop_ids and location, and the script publish already populates them (manifests.py ~1449 / 1466 / 1546 / 1563, from scn["characters"]). So the script updating the scene registry with its linked assets is NOT new work for scenes — it is already happening.
What is missing is that NOTHING IN THE COOKBOOK READS IT. Today the only readers of character_ids are the tracker UI and the rename refactor. The import steps do not query the registry at all; they glob the publish tree.
WHAT TO BUILD
Have the previs import steps ask the registry which assets this scene contains, and import only those:
* ImportCharacterRigs -> SceneAsset.character_ids
* ImportEnvironments -> SceneAsset.location
* ImportDesignModels -> the same cast + props
Keep the grid layout for placement, but drive its contents from the registry.
DECIDE AND STATE:
* What "no cast registered" means. Options: import nothing (an empty previs is honest), or fall back to the current import-everything grid. Prefer the former with a clear message — a previs quietly containing the whole show is how this hid.
* Whether the all-assets grid is still wanted as its own thing. The ImportCharacterRigs docstring says "find any character rigs on the show, import them, and lay them out on a grid", which reads as a deliberate character-library STAGING scene. If that is still useful, it should be its own step with its own name rather than the default behaviour of previs.
* A character in the cast whose rig is not published: skip and report (the same contract the incomplete-rig skip now uses), not a crash and not silence.
RELATED, ALREADY FIXED: ImportCharacterRigs used to subscript render_files.get(".blend") directly and died with "'NoneType' object is not subscriptable" on the first character whose published rig had no .blend — one half-published asset anywhere on the show blocked previs everywhere. It now skips and names those. Cast-scoping would have avoided that particular crash too, by never touching the asset.
OUT OF SCOPE: the video/board case (its own ticket — a video has no SceneAsset); the timeline step (#1024).
DONE WHEN: a scene's previs contains that scene's cast and location and nothing else; a character in the cast with no published rig is named in the step result rather than crashing or vanishing silently; the placement grid still works; and "no cast registered" behaves the way this ticket decides rather than by accident.
PROPS AND DRESSING: THE REGISTRY WORK ALONE WILL NOT COVER THEM (checked live)
Scoping the imports to the registry answers "which assets", but for props and dressing there is currently nothing to answer it WITH:
* Published MODELS — no step exists. ImportEnvironments covers env/mdl, ImportCharacterRigs covers chr/rig, and there is no equivalent for prp or drs anywhere in cookbook/blender/build/prv. A board whose cast includes the coffeecup gets no cup, however well the registry is queried.
* Design GLBs — one line away. ImportDesignModels is structurally general (it loops parents, globs the dsn publishes, imports the glb and auto-scales it), but line 19 pins it: _ASSET_GROUPS = ["env"]. Adding "prp" and "drs" brings their design models in. Note it hardcodes parent="ENV_DSN" as the destination collection, so props would land under ENV_DSN/ — that name wants to become per-group, not a constant.
Decide whether this ticket grows to cover them or whether a sibling "import props and dressing" step is its own item. Either way, do not let a registry-scoped previs ship reporting success while silently containing no props: if the cast declares a prop and nothing can import it, SAY so, the same way the incomplete-rig skip now names who was left out.
Verified live on 2026-08-01 by running each prv step against the open theaxe previs over the Blender bridge.
#3492Analytics: derive artist time from DCC session artifacts (Premiere auto-saves), not the ramp constantNew featureAnalytics & Effort Attributionclaude
·App · analytics·3 months ago
Sessionized event spacing is a weak proxy for artist time on tasks that emit few events -- most tasks in ppe have only the 7-event task-creation scaffold (CreateFolders / CopyDefaultFiles / ChangeStatusToInProgress / ...), so their "artist time" is just SESSION_RAMP_SECONDS. Meanwhile the DCCs are already writing a high-resolution activity trace we never read: the auto-save folder.
SIGNAL (verified on ppe, videos/idesignalchemybuilds/seq/scr):
VERSIONS/source/videos/idesignalchemybuilds/seq/scr/default/tom/{000.000,000.001,000.002}/high/
Adobe Premiere Pro (Beta) Auto-Save/ -> 93 .prproj snapshots
Filenames carry their own timestamp, so this survives copy/move and does NOT depend on mtime:
idesignAlchemyBuilds_scr--<uuid>-2026-08-05_16-40-12.prproj
Grouped with a 30-minute gap rule that yields 11 editing sittings over 4 calendar days:
08-05 16:40->17:00 (12 saves, 21m) | 08-05 18:08->18:14 (6, 6m)
08-06 06:37->06:51 (9, 14m) | 08-06 10:45->11:17 (12, 33m) | 08-06 12:01->13:28 (28, 87m)
08-07 08:38->08:48 (4, 9m) | 08-07 10:06->10:59 (10, 53m) | 08-07 12:37->13:36 (6, 60m)
08-08 07:10->07:44 (3, 35m)
= 5.3 h butt-in-seat
Compare the three numbers for the same task:
machine time 1.8 m
event-sessionized artist time 9.3 h (inflated by ramp constants)
auto-save derived artist time 5.3 h (defensible, per-sitting resolution)
WHAT TO BUILD:
1. A session-artifact reader (suggest alc/services/, consumed by app/tools/analytics/) that walks a task's VERSIONS tree for known auto-save/backup folders and returns timestamped save events. Parse the timestamp from the FILENAME, fall back to mtime only if unparseable.
2. Per-DCC adapters, since the folder name and filename format are app-specific:
Premiere "Adobe Premiere Pro (Beta) Auto-Save/*.prproj" ts in filename (VERIFIED)
After Effects, Blender (.blend1/autosave), Maya (autosave/) -- add as encountered; unknown DCC = no signal, not an error.
3. Feed it into the Actuals lens as a THIRD artist-time source, ranked above event-sessionization when present. Label which source produced the number on screen ("from 93 Premiere auto-saves" vs "estimated from event spacing") so nobody mistakes an estimate for a measurement.
4. Roll up to the same (user, task, local-day) grain task_day_actuals already uses, so it drops into the existing table and the ShowTracker read-API without a new shape.
WHY IT MATTERS: this is the difference between "we think this took about 9 hours" and "here are the 11 sittings that add up to 5.3 hours". It is free -- the DCC already writes it, no capture plugin, no artist action, read-only.
CAVEATS TO STATE ON SCREEN, NOT HIDE:
- Auto-save means "app open with unsaved changes", not "person attentive". It is a good lower bound, not a timesheet.
- A gap-rule span still counts idle inside the window; 30 min is a tunable dial like SESSION_GAP_SECONDS.
- Auto-save folders get cleared/rotated by the DCC, so history is not guaranteed complete going back.
RELATED: #3491 (task effort table) is where these columns land. This ticket supplies the trustworthy Artist number that table needs.
ACCEPTANCE: for videos/idesignalchemybuilds/seq/scr the Actuals lens reports ~5.3 h across 11 sittings on 4 days, sourced from auto-saves and labelled as such; videos/kanbanworkflow/seq/edt reports a single sitting (it has exactly 1 auto-save -- the multi-day edt pattern genuinely is not there); tasks with no auto-save folder fall back to event sessionization with the source labelled.
#3983Native Camera Caddy: UE-native dockable panel reproducing the Blender caddy control-for-controlNew featureMagiCam Native UE PluginMagiCam Native Pluginclaude
·Plugin · unreal·Unreal Engine·2 months ago
Reproduce the Blender Camera Caddy N-panel as a native UE dockable panel (C++/Slate/UMG per the #3982 architecture - NO PySide): camera dropdown listing every MagiCam rig in the level/sequence; Camera at Playhead; shot Start/End readout + View Range; control buttons main/dolly/height/tilt that SELECT the named Control Rig control AND engage the rig so the gizmo visibly appears (the mode-dependence gap found 2026-08-13 - selection without visible engagement is failure); Copy/Paste pose keyed by control name; Key Camera into the shot camera-cut range at the playhead; Lens preset dropdown (16/24/35/50/85/120/200) routed through the same lens path as the rig chooser control; Image Planes list with per-plane visibility eye toggle + opacity + depth. The PySide caddy (#261/#346) stays as the pipeline-side reference implementation - this panel must reach feature parity with it on the same underlying rig.
--- AMENDED 2026-08-13 ---
FRESH REWRITE, NOT A PORT (Tom, 2026-08-13). The original spec framed this as "a UI PORT of a known-working thing". Tom's direction supersedes the implementation half of that: build the native panel FRESH in C++/Slate/UMG. Do not port the PySide widget code, do not extract a shared UI abstraction across the two, and do not let the PySide layout dictate the native one - UE-native idiom wins where they disagree.
What the original framing was PROTECTING still stands, so keep it: this is not a licence to redesign the control vocabulary. The control SET and its behaviour are the settled, proven ones (#346 verified them per control), and cam.edit remains the single source of behaviour - the panel calls the verbs, it does not reimplement rig logic in C++. Fresh in HOW it is built; unchanged in WHAT it does.
The PySide caddy is NOT being retired by this ticket. It stays as the pipeline-side reference implementation, and it is the only surface that runs standalone from the app venv over the bridge (the native panel cannot, by construction). Retiring it is a separate decision, worth revisiting once the native panel has been used in anger.
ENGAGEMENT IS ALREADY SOLVED - DO NOT REBUILD IT. This ticket's acceptance line "SELECT the named control AND engage the rig so the gizmo visibly appears" was closed on 2026-08-13 by #3996 / #3998 / #4004 (PR #153) as a VERB: cam.edit.engage_rig(actor=..., adopt=...). It resolves the rig's shot sequence from the asset registry, opens it, selects the Control Rig track (which is what flips the editor into CR mode), revalidates the component's transient mapping and turns viewport Realtime on. The native panel must CALL it, not reimplement any of that.
Three traps it encodes, all found live, all of which a fresh native implementation would otherwise re-discover the hard way:
- creating the CR component track REBUILDS the rig instance, so any handle read before engaging is stale (get_control_rig() returns None);
- set_mapped_elements RESETS the rig pose to rest and silently swallows the NEXT control write, so validation is guarded to once per component per session, with a warmup update and a forced Sequencer re-evaluation after;
- engaging a rig that no shot sequence carries will ADOPT it into whatever sequence is open, writing a stray binding. Automatic engages (e.g. on camera pick) must pass adopt=False; only an explicit artist action adopts.
The panel's camera picker should engage on pick and expose an explicit "Engage Rig" affordance, matching what the PySide caddy now does - the state must be VISIBLE ("engaged in <sequence>" / "no shot sequence"), not guessed at.
--- AMENDED 2026-08-13 ---
CORRECTION to the paragraph above (Tom, 2026-08-13): "once the plugin exists, i don't care about this pyside ui anymore." The native panel SUPERSEDES the PySide caddy - it is not a second UI held at parity with a surviving one.
Practical consequences for whoever builds this:
- The PySide caddy is a REFERENCE while you build (it is the proven behaviour, control for control) and a DEAD SURFACE once the native panel works. Do not invest in it: no new features, no keeping it in step, no shared abstraction bridging the two.
- Parity with it is the acceptance bar for THIS ticket only - the bar for shipping the native panel, not a standing contract.
- Its removal is a separate follow-up, sequenced after this lands, so a half-built native panel never leaves the rig with no control surface at all.
- The cost this concedes, recorded so nobody rediscovers it as a surprise rather than a decision: the PySide caddy is the only surface that runs standalone from the app venv over the bridge; the native panel cannot, by construction. Tom's call is that this does not matter, and the in-editor path is the one that counts.
The verb layer is unaffected either way. cam.edit stays the single source of rig behaviour and outlives both UIs - which is exactly why dropping one of them is cheap.
--- AMENDED 2026-08-13 ---
SCOPE DECISION 2026-08-13 (Tom): build this INSIDE the existing AlchemyUE plugin NOW - do not wait on #3982's standalone/Fab architecture. That dependency is removed. AlchemyUE already carries the compiled C++ module and native Slate/UMG panel patterns from PR #145, so the dockable panel has a home today; #3982 later decides what moves into a standalone MagiCam plugin. Tom's stated preference: a DOCKABLE WIDGET (a real editor tab he can dock beside the outliner), not a floating window. His goal for this pass, verbatim: 'make sure that we can rebuild the camera caddy and all the features i have with it in unreal'. He will drive cameras himself on his return and report the gaps - so favour a WORKING core loop end to end over breadth: a panel that docks, lists the cameras, and can select/engage/pose/key one is worth more than every control stubbed. Architecture: native Slate shell calling the existing Python verbs (cam.edit / cam.query) through IPythonScriptPlugin ExecPythonCommand - cam.edit stays the single source of rig behaviour, the panel is a face over it. Engagement is already solved: call cam.edit.engage_rig (opens the shot sequence, selects the CR track, revalidates mapping, realtime on) so controls VISIBLY appear when a camera is picked.
#4349dsn build phase: clean up an asset's source image (remove background / isolate) per assetNew featureclaudeDesign
·Cookbook · alchemy · build · dsn·Alchemy·3 days ago
A build-phase step in the dsn (design) task recipe (cookbook alchemy/build/dsn, software-first) that cleans up the design image in an asset's source folder: remove the background / isolate the subject so the asset stands alone, writing the cleaned image where the rest of the dsn build reads it (same naming the existing front-image / design deliverables use). Per asset, run from that asset's own design task, so it works for any dsn asset: a crop handed over by the environment breakdown (#4348) and reference an artist drops in by hand alike. Leaves the original in place (non-destructive). Reuse the existing background-removal / matting capability if one exists (check the services and the pro_crop recipe from #4299 before building anything); route through the one pipeline rather than a new backend.
#2Assign the right character templateDesign
·3 months ago
Infer template from character metadata at scr publish.
#685Beat board: embed Scene Prep, and make Process Designs idempotent so one click finishes whatever is leftNew featureApp · transcriptclaudeScript
·App · transcript·Alchemy·3 months ago
A finished beat board is meant to go two ways: straight to video (Seedance) or straight to 3D for previs, where cameras and timing get nailed properly. Previs needs published DESIGNS, MODELS and RIGS for the cast. The scene/script editor already solved the front of that with the Scene Prep band — drop a concept image on a slot, hit Process Designs, and the dsn to mdl to rig chain fires on the farm. The beat board, which is where the work actually happens now, has none of it.
Embed the same band at the top of the beat board, and close the two gaps that show up the moment you use it for real.
1. EMBED IT WHERE THE WORK IS. Put the band at the top of the Create Script page, fed by the BOARD's cast rather than a script scene: the locations the video declares plus every character and prop tagged on its beats (the same registry context the beat tiles already resolve their chips from). Reuse the existing machinery rather than forking it — build_scene_prep_slots, the stage and process routes and partials/scene_prep_band.html all live in app/tools/script/. Drag an image onto a slot, click once, get a rig. One place, while you are thinking about it.
2. READINESS IS A CHAIN, NOT A BOOLEAN. A slot today is only designed-or-not: it counts as designed when the asset has a published dsn thumbnail in the media registry. That is exactly the state that lies on a day like today, where every design got published (to add more images) but no models or rigs followed — the band reads 4/4 designed and looks finished while previs is impossible. Track the whole chain per asset: designed, modelled, rigged, with rig applying to characters only and locations stopping where they stop. Read each from the same publish source of truth the design state already uses, one task up. PROPS: decide explicitly whether they need models for previs and say so in the band rather than leaving them ambiguous.
3. SAY WHAT IS LEFT. The band should state remaining work in the same glance that shows progress — 4/4 designed, 2/4 modelled, 1/3 rigged — and each slot should show which link is missing. The point is never having to guess whether this board can go to previs.
4. PROCESS DESIGNS BECOMES IDEMPOTENT — THIS IS THE HEADLINE. Today the route refuses an empty request (400, no staged designs to process): it only ever processes slots STAGED in this session, so there is no way to say finish whatever is outstanding. Make the button sweep the whole board and fire only the missing links, so pressing it with nothing staged does the right thing instead of erroring, and pressing it twice is harmless. The chain is already close to this — the dsn GenerateModel step finds an existing glb on disk and skips — so the work is scoping (all assets, not just staged) and gating (skip links that are already published), not new pipeline. One click that guarantees everything that CAN be done IS done is worth more than a second button that only reports.
5. The band must not fire farm work nobody asked for. Idempotent means safe to press, not automatic: nothing submits without the click.
OUT OF SCOPE: changing the dsn/mdl/rig cookbooks themselves; the Seedance path (this ticket is the 3D/previs prerequisite); moving the band OUT of the script editor — it stays there too.
DONE WHEN: the beat board shows a prep band covering the video's own locations, characters and props; each slot reports design, model and rig state independently; the band states what is outstanding; dropping an image still stages a design; and clicking Process Designs with nothing staged fires exactly the missing links and is safe to click again.
#1025Video working files were named {task}.ext — pathmap fixed; the two silences that hid it remainBug fixScript Start Previsclaude
·Core·Alchemy·3 months ago
Every working file under videos/ is named {task}.ext - prv.blend, not theaxe_SEQ_prv.blend - because the filename rule for videos names a token that does not exist on the model.
THE CHAIN, CONFIRMED
pathmaps/alchemy.yaml declares a `video` token (line ~51, "freeform video slug") and the videos route says "parent-slug in the `video` token, constant SEQ child" (~line 414). Its filename rule is:
default_tokens:
videos: [video, task]
But PathObject has NO `video` field - 'video' in PathObject.model_fields is False - and `parent`'s only validation alias is `sequence`. So:
* a po PARSED from a video path gets parent='theaxe', video=None -> the rule asks for `video`, finds nothing, skips it as an empty optional token, and only `task` survives -> "prv"
* a po BUILT with video='theaxe' gets parent='' - the kwarg is SILENTLY DROPPED, no error - and also yields "prv"
Both roads lose the identity. The rule can never fire as written.
NOT A PREVIS BUG. It affects every task under videos/ - scr, prv, anything - and it has been quietly true since videos were added. It surfaced now because Start Previs (#1007) is the first thing to build a working FILE for a video rather than a manifest with a fixed name.
THE THREE-WAY DISAGREEMENT it produces, all for one asset:
* on disk (what copy_default_files wrote): prv.blend
* what a token-built po computed for the same file: theaxe_prv.blend
* what the convention says (parent_child_task): theaxe_SEQ_prv.blend
A file the pipeline cannot agree on the name of is a file the next step cannot open - which is exactly how this was found: the build wrote one name and then failed to open the other.
FIX - PICK ONE AND SAY WHICH
(a) Point the rule at the tokens the model actually carries: videos: [parent, child, task]. Matches how videos are really routed on disk (videos/{parent}/{child}/{task}) and gives theaxe_SEQ_prv - the convention every other entity type follows. Smallest change, no model work.
(b) Give PathObject a real `video` field (or make it an alias of `parent`) so the pathmap's vocabulary becomes true. Keeps the route's language but touches the model everything depends on.
Recommend (a): videos already route exactly like shots, and a second name for `parent` is the thing that caused this.
FIX THE SILENT PART TOO, whichever is chosen. Two separate silences made this hard to see and both are worth closing:
* PathObject(video='theaxe') dropping an unknown kwarg without complaint. An unknown token in a coordinate should not be quietly discarded.
* version_basename accepting ANY truthy engine result (`if name: return name`), so a rule that degenerates to one token looks like a valid answer instead of falling back to the parent_child_task default. A name that lost its identity should not read as a name.
DONE WHEN: a video's working file is theaxe_SEQ_prv.blend (or whatever the chosen rule states) on disk AND from any po that addresses it; a token-built po and a path-parsed po agree on the basename; building a po with an unknown token does not silently succeed; and scenes/assets naming is unchanged.
VERIFY WITH: the existing file at E:/Alchemy/cgl/loj/VERSIONS/source/videos/theaxe/SEQ/prv/default/tom/000.000/high/ - currently prv.blend.
#3493SmartTask: version activity harvesting as a core ontology op with per-task/per-DCC extensionsNew featureAnalytics & Effort Attributionclaude
·Plugin · smart_task·3 months ago
#3492 proposes reading Premiere auto-saves to measure artist time. That is correct but it must NOT be built as a Premiere special case. Every DCC leaves a different activity trace -- Premiere writes timestamped auto-saves, Blender writes .blend1/.blend2, Maya writes an autosave/ dir, some write nothing at all -- so timestamp harvesting belongs in the SmartTask ontology as a core capability with a sane default and per-task/per-DCC extensions, feeding ONE central model.
THE SHAPE (follows the PATH_FIELDS precedent in alc/plugins/smart_task/paths.py -- per-task declarative spec + one shared helper):
1. Central model. ActivitySample(ts, source, kind, path, version, task, software, user) -- one timestamped observation that something happened inside a version. Everything below produces these; nothing below aggregates them.
2. Default harvest, in the `common` namespace. `common` is already the first-class task-agnostic peer (alc/plugins/smart_task/common.py), which is exactly the right home. Default = walk the version dir and stat every file: modified, and created where the platform exposes it. Works for any task, any DCC, including ones we have never integrated. A task with no extension still gets a real signal.
3. Per-task / per-DCC extension. A declarative ACTIVITY_SOURCES spec on the contract -- glob pattern relative to the version dir plus a timestamp-extraction rule (from filename regex, else mtime). Known cases:
premiere / edt+scr : "Adobe Premiere Pro (Beta) Auto-Save/*.prproj", ts from filename
(VERIFIED: idesignAlchemyBuilds_scr--<uuid>-2026-08-05_16-40-12.prproj)
blender : "*.blend1", "*.blend2", ts from mtime
maya : "autosave/*.ma|*.mb", ts from filename where present
unknown : falls through to the common default, NOT an error
4. Resolution order, reusing the existing convention-based lookup in registry.py:
{software}.{task} override -> {software} override -> common default.
CRITICAL CONSTRAINT -- THIS MUST NOT GO THROUGH THE EXECUTOR:
Activity harvesting is pure filesystem. Reading Premiere auto-saves must never require Premiere to be running -- otherwise you have to launch the DCC to find out how long someone spent in the DCC. The smart_task package is already import-clean in the app venv by design (no bpy / maya.cmds at import); the harvester must keep that property and be callable straight from the analytics panel with no DCC present, no bridge socket, no in-process dispatch. It is a contract + registry + filesystem adapter, NOT a VerbBinder op.
PLATFORM TRAP TO HANDLE EXPLICITLY:
os.stat().st_ctime means creation time on Windows but inode-change time on POSIX. The farm is Windows today, but the "created vs modified" distinction must be read through a platform-aware helper (st_birthtime where available), not by assuming st_ctime is creation. Getting this wrong silently inverts the meaning of the default harvest on Linux.
WHAT CONSUMES IT:
Roll ActivitySamples up to the same (user, task, local-day) grain time_actuals.task_day_actuals already uses, so this drops into the Actuals lens and the future ShowTracker read-API without a new shape. Label the source on screen so a measured number is never mistaken for an estimate (see #3491, #3492).
WHY IT IS ONTOLOGY, NOT PLUMBING: "how much human time went into this version" is a property of a version, the same way its paths and its MSD payload are. It should be answerable for every task in the system by construction, with per-task fidelity where the DCC gives us more to work with -- not bolted on per integration.
ACCEPTANCE: harvest_activity(path_object) returns ActivitySamples for a Premiere scr task (filename timestamps), a Blender task (.blend1 mtimes), and a task with no adapter at all (plain file stat) -- all without any DCC installed or running, called from the app venv; resolution order is covered by tests; st_ctime is never read as creation time without a platform guard.
RELATED: #3491 (task effort table), #3492 (auto-save artist time -- this ticket generalises it).
#426in the Phase Details Section of the Phases panel, i want to be able to click a link and open the actual cookbook recipe on the cookbook page in a new tabNew featureTomDetails
·App · cookbook·Alchemy·3 months ago
#643with the image editor we need a way to flatten the AI layers or edit them, the current implementation is a bit odd.DesignApp · transcript
·App · image_editor·Alchemy·3 months ago
my first choice would be to treat these like any other layer
i'd also want a "flatten" button to flatten the layers into one image like photoshop.
#1028Default rig: put a bone on a published model so props can be animatedNew featureScript Start PrevisclaudeRig
·Plugin · blender·Blender·3 months ago
A prop that has to move needs a rig, and there is no way to make one. The coffee cup in theaxe has to be dropped; nothing in the pipeline can put a bone on it.
Build the base case of the rig task: take a published model and produce a minimal animatable rig — a single bone bound to the mesh, at the model's origin, published as a normal rig so everything downstream treats it like any other.
THE SHAPE: ONE `rig` TASK, A DIFFERENT PATH THROUGH IT — USE parent_filter
The chain is already design -> model -> rig; it simply assumes a CHARACTER on the far end (mGear / ARP, a full control hierarchy). Do NOT invent a parallel "prop rig" task. Use the cookbook's existing flagging system: steps declare `parent_filter` for the asset kinds they serve, and step_applies() skips the rest (recorded as SKIPPED, so the reason stays visible). The dsn build already does exactly this — GenerateModel declares parent_filter "prp, env, drs".
So the rig recipe carries BOTH paths and picks by asset kind:
* the character steps filter to `chr` — mGear / ARP, untouched;
* a new single-bone step filters to `prp` (and `drs` where wanted).
Downstream vocabulary stays "rig" — the previs import, the publish, the prep band and the registry all keep treating a rigged cup as simply a rigged asset — while the implementation diverges exactly where it should. Same task, different route through the recipe, no new concept for anything downstream to learn.
WHAT TO BUILD
1. One bone, bound to the model, at a sensible origin. For a dropping cup that is genuinely all that is needed: one transform to key.
2. Published through the SAME rig publish the characters use, so the render tree gets the .blend that ImportCharacterRigs-style imports read. Tonight's lesson: a rig published only as .ma is invisible to Blender previs (see #1026 notes) — a default rig must land as a .blend in the render tree or it cannot be imported.
3. Idempotent, like every other link: re-running finds the existing rig and does not replace it.
4. Works headless — it is a build step, and previs is going to want to fire it without a person in Blender.
DECIDE AND STATE:
* WHICH ASSET KINDS get one. Props certainly. Dressing probably not by default (a tree that never moves does not need a bone, and rigging every tree is cost with no benefit) — but a dressing item that DOES move needs a route to one. Prefer: any asset can have a default rig, nothing gets one automatically, and the thing that decides is whether the asset is animated in the shot.
* BONE PLACEMENT. Origin is the honest default. Say whether it is the model origin, the bounding-box base (better for a cup that lands on a table), or the published pivot — and why. PivotsToOrigin already exists in the mdl publish, so there may be a convention to inherit.
* WHETHER THE ARTIST CAN UPGRADE IT LATER. A default rig should not block a real rig replacing it. Decide whether it is a distinct variant or simply the first version of that asset's rig.
THE KNOCK-ON THAT MUST BE HANDLED — THE PREP BAND LIES OTHERWISE
The beat board's prep band currently models the chain as design -> model -> rig with RIG APPLYING TO CHARACTERS ONLY. That decision was made on the grounds that nothing poses a prop, and this ticket makes it false: a board whose cup cannot be animated would still read "ready for previs", which is exactly the class of lie the band was built to stop. Update the readiness model so an asset that NEEDS a rig is counted whatever its kind, and decide how "needs a rig" is known — declared on the asset, inferred from the beats that animate it, or set by hand.
OUT OF SCOPE: character rigging (mGear / ARP); dressing scatter and instancing (a separate problem — placing many trees is not rigging); the animation tooling itself.
DONE WHEN: a published prop model can be given a default rig in one step; the rig publishes as a .blend the previs import can read; a previs containing that prop brings in something animatable; re-running is harmless; and the prep band counts the prop's rig instead of reporting a board ready that is not.
THE ACTUAL EDITS — SMALLER THAN "BUILD A DEFAULT RIG" (checked live)
cookbook/blender/build/rig/rig.json today:
0 ImportLatestPublishedModel parent_filter=None
1 AutoRigModel parent_filter=None
Tom's plan, confirmed against the recipe: pin the existing auto-rig to characters, then add the prop path beside it.
1. AutoRigModel -> parent_filter "chr". It is the character auto-rig and currently claims every asset kind.
2. Add the prop step with parent_filter "prp" (and "drs" if a dressing item ever needs to move).
3. Leave ImportLatestPublishedModel unfiltered — a prop rig needs its model imported exactly as a character's does. Confirm rather than assume.
PRIOR ART — STALE, TREAT AS A HINT NOT A FOUNDATION
blender/build/rig/ImportDefaultRig.py already exists and imports a prebuilt rig into a `rig` collection, and the asset it points at is real:
.../cgl-cookbook/cookbook/default_files/blender/rig.blend
Tom: "it's just old - there's a lot of old code we have to clean up in the cookbook repo at some point." So do NOT treat it as a working head start. It is not wired into rig.json and would fail if it were: it calls CFG.get_cookbook_root(), which does not exist (the attribute is CFG.cookbook_root), and CFG.cookbook_root resolves one level ABOVE where the file lives — the right root is CFG.company.cookbook_dir, which is what AlchemyRouteEngine uses.
Read it for the INTENT (someone already decided a default rig should ship as an importable .blend rather than generated bones) and for the asset, then write the step to current conventions rather than reviving it. If it is dead, delete it in the same change — a stale step sitting next to a working one is how the next person loses an hour.
Design choice it implies, worth confirming: default rig by IMPORTING a prebuilt one-bone rig.blend, versus procedurally generating a bone. Importing is versionable and art-directable (improve the rig in Blender, no code change); generating adapts to each model's size and pivot. The prebuilt file suggests the earlier call was import — but that call is old too.
TURNTABLE: NO CALISTHENICS (Tom). A prop's rig review does not need the range-of-motion exercise a character rig gets — the turntable can stay a turntable. Worth stating in whatever review the prop rig publishes into, so nobody adds calisthenics to it later "for consistency".
#3498Features: find related work by file path or UI element (reverse lookup filter)New featureAnalytics & Effort Attributionclaude
·App · features·3 months ago
THE PROBLEM IN ONE NUMBER: 71 of 5,973 commits are tagged to a feature -- 1.1% coverage. The FeatureRegistry, the areas tree and the tagging UI all exist and work. What is missing is a way to SELECT a batch of related commits in one action, so tagging stays a commit-at-a-time grind and coverage never moves.
WHAT TO BUILD: a filter/search over the work, with two entry points that match how a person actually remembers what they built.
1. BY FILE PATH -- "show me everything that touched this file"
E:/Alchemy/cgl/config/cgl-cookbook/cookbook/alchemy/shelves/dsn/BuildUeRig.py
Accept an absolute path, a repo-relative path, or a partial/glob and resolve it.
2. BY UI ELEMENT -- "show me everything behind this button"
A visible label ("Build UE Rig", "Attach Rig") grepped across templates, shelf
definitions and JS to find the files that render it, then resolved through (1).
This is the same trick /alc_submit_ticket already uses with --ui-keyword to route
a ticket to its code; here it routes a QUERY to its commits.
THE DATA IS ALREADY ON DISK -- THIS IS A FILTER, NOT A HARVEST:
{company}/ppe/PUBLISHES/msd/features/commit_files.json (434 KB, 307 commits cached)
Entries are keyed BY REPO and carry per-file path + line count + change type:
"alchemy:ac86efda..." -> [["alc/config/production_tasks.py", 19, "M"], ...]
"cookbook:685a6eab..." -> [["alchemy/art/mdl/ScupltModel.py", 55, "A"], ...]
So file -> commits is an inversion of a cache that already exists and already spans
both repos. Build the reverse index (path -> [sha]) from it; extend coverage using
the existing /cache-step walker for commits not yet cached.
CROSS-REPO IS NOT OPTIONAL: the motivating example (BuildUeRig.py) lives in the COOKBOOK
repo while its supporting plumbing lives in alchemy. A file search that only covers one
repo answers the question wrong. commit_files.json is already repo-keyed -- keep that.
RESULT SHAPE: matching commits grouped by existing feature where tagged, plus an explicit
UNTAGGED group (the actionable pile), each row showing sha, subject, repo, date, author,
files touched, and derived effort from the analytics sessionizer. Sort untagged first --
that is the work the search exists to enable.
SCOPE: read-only filter + result view. Tagging the results is #NEXT_TAG; the executive
report over them is #NEXT_DISPLAY. This ticket only has to FIND the work reliably.
ACCEPTANCE: searching "BuildUeRig.py" returns its cookbook commits AND alchemy commits
touching the UE-rig path; searching the UI label "Build UE Rig" resolves to the same set
via the files that render it; results separate tagged from untagged; a partial path and
an absolute path return the same commits.
RELATED: #3491/#3492/#3493 measure time per TASK; this line measures it per FEATURE.
Port the MagiCam Lighting section natively: an HDRI picker with thumbnail grid reading the JSON HDRI library (name -> exr/hdr paths + yrot), Apply builds the environment (skylight/HDRI backdrop equivalent of the Blender world setup) with Exposure and Rotation controls; plus the Weta-style aim-rig spot light - a control point at the lit spot with the light offset up its axis, so rotating the control is one-click directional lighting, with Distance and Create + snap-to-selection.
#671Beat board: SCRIBBLE the layout — blank plate with entity-bound mask layersNew featureApp · transcriptclaudeScript
·App · transcript·Alchemy·3 months ago
Drawing beats generating when what you need is FRAMING and LAYOUT — but the artist is not tracing a panel. The fast move is notation: a scribble for the figure, a horizon line, a box for the cup, or flat RGB masks saying who stands where. That scribble IS the framing input to the generate. It is never artwork, and it never becomes the beat's picture.
WHAT IT SHOULD DO
1. CLICKING THE EMPTY LAYOUT CELL STARTS THE DRAWING. That is the entry point — no extra pencil affordance to find. The empty slot today opens a file picker on click; drawing takes that click instead, which gives the slot one rule per gesture: DROP an image to bring one in, CLICK to draw one. Browse stays reachable as a secondary affordance on the slot (hover icon or context menu) rather than the primary click, and drag-and-drop is untouched. The empty cell should read as an invitation to draw, not as an upload target.
The click opens Alchemy's image editor on that beat's LAYOUT plate, creating a blank canvas when there isn't one. It writes to the plate path that already exists (layout_ plus the visual id, see _plate_dest) and sets plates['layout'] — so the thing drawn is immediately the reference the generate consumes, with no promote step. It must NOT touch the beat's media. When a plate is already in the slot — a dropped photo or an earlier scribble — clicking opens THAT for markup instead of blanking it: scribbling placement over a real frame is the same gesture, and a click must never destroy work.
2. THE CANVAS IS THE RIGHT SHAPE. Size from the video's own aspect — AspectRatio.dims(short_side) in alc.models.style returns encoder-even dimensions for every aspect the pipeline accepts. A 9:16 video must never open a landscape canvas.
3. IT IS NOTATION, AND THE PROMPT MUST SAY SO. A scribble plate needs a different callout from a photographic one. #533 ships a layout-only callout for a real frame; a scribble needs stronger, distinct wording: the marks are PLACEMENT NOTATION defining position, scale, eyeline and horizon only, and no line, colour or texture from the sketch may appear in the render. Getting this wrong produces a rendered scribble, which is the one failure mode that would make the feature useless.
4. MASK-TO-ASSET — most of the plumbing exists, finish it. layer_doc manifests already carry entity_type and entity_id per layer (character / prop / environment). The editor's add-entity-layer route already binds a layer to an asset AND back-fills the block's cast when it is missing. editor_overlay.js already round-trips both fields through the live layer stack. What is missing is an EMPTY entity-bound layer to draw into: that route today renders the asset's published design and SAM-cuts it. Add a blank layer bound to an asset, offered from the beat's existing chips so Jack and the Coffee Cup are one click each.
5. COLOUR IS DERIVED, NEVER REMEMBERED. Each entity-bound layer gets a stable colour, the editor shows the legend, and the prompt NAMES the regions — the red region is where JACK stands, the blue region is the Coffee Cup. The artist must never have to recall which colour meant which character, and the legend must be generated from the same binding the layers carry, so it cannot drift.
6. A SCRIBBLE MUST BE DISTINGUISHABLE FROM A PLATE. plates is dict[role, path] today; the callout and the colour legend depend on knowing which kind of plate this is. Decide it explicitly — a parallel kind map, or richer plate values — and do not infer it from the filename.
7. A SCRIBBLE IS A SOURCE, NOT A ROLE. The slot is LAYOUT (composition — where the figure stands, where the horizon sits); framing is the camera's and lives on the beat's camera beside angle and movement. A scribble does not need a word of its own: what varies is the SOURCE, which the reference panel already reports per row (published dsn design / dropped plate / framing term guide). A scribble adds one more value there — drawn — and that is the same fact the prompt reads to pick its callout, so the panel and the model cannot disagree about what kind of plate this is.
OUT OF SCOPE: mask-CONDITIONED model pipelines (controlnet, regional prompting) — v1 names the regions in the prompt; brush and tool work inside the editor; any drawing UI inside the transcript tool.
DONE WHEN: the pencil on an empty framing slot opens a correctly-shaped blank canvas; a blank layer can be bound to any of the beat's characters or props and scribbled into; saving updates the beat's framing plate in place and the tile and panel show it without a reload; the panel reads it as a scribble; and a generate names each coloured region as the asset it is bound to.
#3499Features: bulk-tag search results to a human-titled feature spanning areas and reposNew featureAnalytics & Effort Attributionclaude
·App · features·3 months ago
Depends on #3498 (the search). Once a search returns the commits behind a button or a file, the next action must be ONE tag operation over the whole result set -- that is what turns 1.1% coverage into real coverage.
1. BULK TAG FROM RESULTS
Select all / select some -> assign to an existing feature or mint a new one inline.
The existing per-commit /assign route is the primitive; this is the batch verb over it.
Show what the batch will do before it commits (n commits, which repos, date span).
2. HUMAN-READABLE TITLES ARE THE POINT
Auto-seeded names are unusable as receipts -- the registry currently contains
"Claude fervent jennings fe341e" (derived from a branch name). Executives read this.
Titles must be curated and hierarchical, matching how the work is actually described:
"Design -> UE Rig"
"Rig Caddy -> Attach/Detach"
"Facial Expressions"
Keep the derived name as provenance; display the curated title. Feature.source already
distinguishes derived | pr | manual | ai -- extend, do not replace.
3. MODEL GAPS THIS EXPOSES (alc/models/feature.py)
- Feature.area_id is a single area, but real features are CROSS-AREA. The facial-
expressions work spans the design shelf AND the transcript tool. Needs many-areas
(keep a primary for sorting if useful, but stop losing the rest).
- Feature.repo is a single repo label, but features span alchemy AND cookbook
(Design -> UE Rig is exactly this). commit_shas already carry repo-qualified keys
in commit_files.json -- let the feature span them.
- 368 auto-derived areas is noise at report time. Add a curated flag or a depth cap so
the executive view groups on a handful of real domains, not every directory.
None of this needs a migration that drops data: widen the fields, keep readers tolerant.
4. TAGGING MUST GET EASIER OVER TIME, NOT STAY MANUAL
Every tag is training data for the existing proposer (/propose). After a bulk tag,
re-propose over the untagged remainder -- tagging one cluster should make the next
cluster cheaper. The proposals store already exists and is resumable.
ACCEPTANCE: a search for "Build UE Rig" can be tagged in one action to a new feature titled
"Design -> UE Rig" spanning both repos and more than one area; the registry round-trips it;
the curated title is what renders, with the derived name kept as provenance; re-running the
proposer afterwards suggests related untagged commits.
RELATED: #3498 (search, blocks this), and the display layer that consumes it.
#453Cookbook step deep link 500s when the step name is unknownBug fixclaude
·App · cookbook·Alchemy·3 months ago
GET /c/{cid}/project/{pid}/cookbook/task/{sw}/{task}/{category}/{recipe_type}/{step_name} with a step_name that isn't in the recipe should answer a clear 404 (like the IDE route at app/routes/pages.py fast_ide_entry does), not a 500.
#534Visual beat: one-click 'use the previous beat's image as my layout'New featureApp · transcriptclaude
·App · transcript·3 months ago
DEPENDS ON #533, which adds the layout plate slot (plates['layout']) to a visual beat. This ticket is the shortcut that makes that slot cheap to fill in the case it exists for.
The common move on the beat board is continuity: one beat establishes a composition (a closer shot on the cabin doorway), and the NEXT beat is the same frame with something new happening in it (Jack walks into that doorway). Today the artist re-drags the thumbnail beat to beat; with #533 landed they would drag it onto the framing slot instead. Both are a drag across the board, and the intent - 'same frame as the beat before this one' - is a single idea that deserves a single click.
WHAT IT SHOULD DO
A control on the visual tile's framing slot that fills it from the PREVIOUS beat in board order, in one click. Specifically:
1. Previous = the nearest preceding visual beat that HAS media, walking backwards across rows (not just within the current row/block) - a beat whose square is still empty is skipped rather than ending the search. If none exists, the control is absent or disabled with a title saying why.
2. It fills framing_media, never media - the beat keeps its own picture (or stays empty and generates one). Same COPY semantics #533 establishes: the file is copied to this beat's deterministic framing_<visual_id> name, never shared by path, because the image editor saves back in place (#528).
3. It copies the SOURCE's current picture at click time. If the source is later re-generated or retouched, this beat's framing guide does not follow - that is the intended behaviour (the guide is a receipt of what was composed against), and the control can simply be clicked again to re-take it.
4. Discoverable from the empty framing slot: a small affordance on the slot itself (icon + title like 'Use the previous beat's image as the framing guide'), not a menu item somewhere else. It should also read correctly in the #533 disclosure panel afterwards - the FRAMING row says it came from a dropped/copied plate, same as a manual drop.
5. Optional stretch, only if it falls out cheaply: shift-click or a modifier to take framing from the previous beat AND inherit its camera Shot term, so a run of beats can be pinned to one framing in a few clicks. Skip it if it complicates the primary path.
WHY: continuity across adjacent beats is the single most common framing intent on this board, and it is currently a manual drag that the app cannot tell apart from 'here is a picture'.
#3500Features: executive receipt view — effort and schedule per human-titled featureNew featureAnalytics & Effort Attributionclaude
·App · features·3 months ago
Depends on #3498 (search) and #3499 (tagging). This is the display layer -- the surface an
executive reads to see the receipts for what Alchemy delivered. Same data as the Actuals and
Architect tabs, but grouped by FEATURE instead of by task or commit, because "Design -> UE Rig"
is the unit a stakeholder recognises and "ac86efda" is not.
PER FEATURE, SHOW:
- Curated title and description ("Design -> UE Rig")
- Artist time, derived from its commits via the analytics sessionizer (never stored --
alc/models/feature.py is explicit that actuals stay owned by the event stream)
- SCHEDULE: first commit -> last commit, distinct active days, number of work sittings.
Same three-unit treatment as #3491 -- elapsed calendar time is what a stakeholder feels,
and it is exact.
- Commit count, repos touched, areas touched, contributors
- The evidence trail: expandable list of commits with subjects and files, so any number on
the page can be drilled to the code that backs it. A receipt nobody can audit is a claim.
TWO VIEWS OFF THE SAME DATA:
- Executive roll-up: features sorted by effort, grouped by curated area, date-filtered.
Readable by someone who has never opened the repo.
- Delivery detail: the drill-down above.
HONESTY REQUIREMENTS (these are the difference between a receipt and a sales slide):
- Show COVERAGE on the page -- "these features account for N of M commits (X%)". At filing
time that is 1.1%. A roll-up that silently omits 98.9% of the work reads as authoritative
and is not. Coverage must be visible and must go up as tagging proceeds.
- Label derived vs measured effort (see #3492) -- an estimate must never render like a
measurement.
- Untagged work gets its own explicit bucket, never dropped.
WHY IT MATTERS: this is the artifact a client or investor has most likely never had -- a
per-capability breakdown of what was built, what it cost in human time, and over what calendar
window, traceable to the commits. It is also the honest counterweight to throughput claims,
which read as inflated without evidence attached.
ACCEPTANCE: the view lists curated features with artist time, active days, calendar span and
commit count; each drills to its commits and files; coverage percentage is displayed; untagged
work appears as its own bucket; date filtering works.
RELATED: #3491 (task effort table -- same three-unit treatment, task grain), #3498, #3499.
#511[cookbook] [alchemy] [dsn] Implement the Generate Model From Tpose buttonNew featurealchemystub
·Cookbook · alchemy · build · dsn·Alchemy·3 months ago
Generate a meshy model from shared_data tpose if it exists.
#687Beat board: bring back grab-to-slide — a video's SOURCE in-point, not the clip's positionNew featureApp · transcriptclaudeScript
·App · transcript·Alchemy·3 months ago
Grab-to-slide was removed from the visual tile with the reasoning that a beat is its row's span, so there is nothing to slide. That is true of the clip's POSITION and false of its SOURCE. The moment beats start carrying generated video it matters: video comes back at whatever timing the model felt like, and the usable seconds are somewhere inside it — sliding is how you choose which part plays. It was only safe to remove because nobody had tested video yet.
THE DISTINCTION THAT WAS LOST
A beat's screen time is set by its block and is not up for negotiation on this board (see #670 for editing it). What a slide changes is WHICH FRAMES of the beat's media play during that fixed window — the source in-point. Two different clocks: lane time and source time. The removal collapsed them.
WHAT TO BUILD
1. RESTORE THE HANDLE, for video media only. A still has no frames to choose between, so it keeps no handle. The mechanic itself survives: app/static/js/slide.js (AlcSlide) is intact and transcript_editor.js still loads it eagerly for exactly this gesture — what went away is the handle in the tile and the commit path (removed in the dead-code sweep, commit 8b8da9c; the original is Feat #96, commit 4174866).
2. IT COMMITS A SOURCE IN-POINT. The beat has nowhere to put one today: the doc-level in_point is the whole source recording's, and a beat carries only a media path. Add a per-beat source in-point beside media, cleared whenever the media itself changes — a new clip's in-point is not the old clip's.
3. PLAYBACK MUST AGREE. The previz screen seeks video by offset into the clip (updateScreen and syncScreenVideo compute local time as playhead minus clip start); that becomes playhead minus clip start PLUS the in-point. The create-mode virtual transport needs the same. The timeline JSON carries the in-point so both read it from one place.
4. CLAMP, AND SAY WHAT HAPPENS AT THE END. In-point plus the beat's span cannot exceed the media's duration. Decide and state the behaviour when the video is SHORTER than the beat's screen time — hold the last frame, or mark the clip as short — rather than letting it silently run out.
5. The filmstrip in #98 is this gesture's companion: while sliding, show the frames you are landing on. Sliding blind is guessing.
OUT OF SCOPE: trimming the beat's screen time (#670); moving a clip to another position (the lane is 1:1 and sequential); any Seedance generation work.
DONE WHEN: a beat holding a video shows a grab handle; dragging it changes which part of the video plays without moving the beat or changing its duration; the preview screen and the transport both play from the new in-point; and the in-point resets when the media is replaced.
#3501Kanban: name the feature at ticket-filing time so tagging stops being retroactiveNew featureAnalytics & Effort Attributionclaude
·App · kanban·3 months ago
#3498/#3499/#3500 recover feature attribution AFTER the fact, by searching commits and tagging
them. That backfill is necessary -- 5,973 commits are already on the ground -- but it should not
be the permanent mechanism. The cheapest moment to know which feature a commit belongs to is
when the ticket is filed, because a person is already describing the work.
WHAT TO BUILD:
1. An optional feature facet on a ticket -- pick an existing curated feature or name a new one.
Mirrors how --area-path and --ui-keyword already route a ticket to its code; this routes it
to its CAPABILITY.
2. Surface it in both filing paths: the in-app Request pill and dev/ticket_cli.py submit
(a --feature flag, same shape as --sprint).
3. On wrap-up, when a ticket's branch merges, tag that branch's commits to the named feature
automatically. The ticket already knows its branch and the wrap-up flow already runs at merge
-- this is a hook on an existing event, not a new process.
4. Leave it optional. A required field would get filled with noise, which is worse than empty:
a wrong feature tag is harder to detect than a missing one.
WHY IT CLOSES THE LOOP: retroactive tagging is O(commits) forever and needs a human to remember
what they built months ago. Naming the feature at filing time is O(1) per ticket, captured while
the context is fresh, and it makes the executive receipt (#3500) self-maintaining rather than a
periodic archaeology exercise.
SEQUENCING NOTE: this is deliberately p3 and last. It depends on #3499 having established what a
curated feature IS (human title, cross-area, cross-repo) -- building the capture field before the
model settles would bake in the wrong shape.
ACCEPTANCE: a ticket can carry a feature from the Request pill and from ticket_cli submit --feature;
a merged ticket's commits appear under that feature in the receipt view without manual tagging;
the field is optional and a ticket without it behaves exactly as today.
RELATED: #3499 (curated feature model -- blocks this), #3500 (the view this keeps fed).
#454Design Deliverable Slots widget on the dsn shelfNew featureclaude
·App · routes·3 months ago
PROBLEM: the dsn tag is inferred from the filename, so a one-token miss silently diverges with no signal. Live case: chr_jack_tpose.png (missing the 'dsn' task token) parsed as an ad-hoc tag instead of 'tpose', so build/dsn CheckForTpose found no T-pose and spent a Gemini generation making a new one, while a perfectly good T-pose sat in Source Files. You cannot see what will be used for mesh generation until after the recipe runs.
COMMIT 1 (do first, useful alone) - near-miss tag guard in alc/services/ai/design.py (_parse_tag / scan_design_candidates, the ONE scanner both the app and the cookbook read). A stem containing a vocabulary word that does not parse to that tag is a naming mistake, not an ad-hoc group: classify it as a near-miss. CheckForTpose then fails with 'found chr_jack_tpose.png - looks like a T-pose but is not named to convention; tag it or rename it' instead of silently generating. Also covers legacy names (freddie_tpose_front_main.png, chr_x_dsn_t_pose.png).
COMMIT 2 - Design Deliverable Slots widget on the dsn shelf. Add a shelves/dsn cookbook step declaring WIDGET = 'design_slots' (same contract Height.py uses; see get_task_shelf_buttons in app/routes/version.py) rendering an app-side partial that reuses _deliverable_slots() from app/routes/assets.py - so chr gets Main/Anchor/Headshot/T-pose/Back/Side A/Side B and env/prp get main+anchor, from the existing _DELIVERABLE_DEFS vocabulary. Each slot accepts a drag from a Source Files row and offers the existing generate URL. Drag-to-slot precedent: app/tools/script/templates/partials/scene_prep_band.html.
KEY DESIGN POINTS: (1) The drop must NOT rename the file. pick_main checks primary_designs[tag] before filenames and the pointer is just a filename in that dir, so dropping a misnamed file writes primary_designs['tpose'] = 'chr_jack_tpose.png' and the recipe resolves it as-is. Renaming stays an optional 'normalize names' action. (2) The widget must read/write the SAME pointers as the character dashboard (primary_designs / reference_design on the asset) - NOT a parallel per-version store, or the dashboard and the shelf can disagree about what the T-pose is, which is worse than today. (3) primary_designs is asset-global but source files are per-version: when the pointed-at file is not in the version being viewed, the slot must render UNRESOLVED rather than looking healthy (pick_main's is_file() check falls back to the name bucket there). (4) Mark the T-pose slot as the mesh source on characters, so 'what feeds Generate Model' is a labelled slot with a thumbnail before you press Run All.
#455on the right click for files within source files and render files add "rename" and "delete" as otpions in that context menuNew featureTom
·Cookbook · alchemy · context-menus·Alchemy·3 months ago
#496i need to be able to choose multiple asset categories.for a step.New featureTom
·App · cookbook·Alchemy·3 months ago
#333[cookbook] [alchemy] [dsn] Implement the Check for T-Pose buttonNew featurealchemystub
·Cookbook · alchemy · build · dsn·Alchemy·3 months ago
Check if there's a file with "tpose" in the name. if there's not create one from the "anchor" image if it exists, or the main image if it doesn't.
#497Cookbook steps read the active project's registry, not the path'sBug fixclaude
·Service · project·3 months ago
Roughly 20 cookbook steps resolve production registries with PROJECT_REGISTRY.current, which is GLOBAL UI STATE (whatever project the app has open) rather than the project of the PathObject being processed. A step run against cgl/loj while the app sits on bse/imm silently reads the wrong registry: lookup_asset returns None, primary_designs is invisible, and the step reports the asset as missing from the registries.
LIVE CASE that motivated this: build/dsn CheckForTpose on cgl/loj chr/jack failed to see primary_designs={'tpose': 'chr_jack_tpose.png'} because the app was on bse/imm. The tag had been set correctly in the UI; the recipe simply could not see it. Worse, it is INTERMITTENT - the same step passes or fails depending on which project happens to be open, which makes it look like flaky data rather than a resolution bug.
FIX: resolve from the address. PROJECT_REGISTRY.get_project(po.project, po.company) already exists and is correct - PathObject carries .company and .project. Two steps are already converted as the pattern to follow: cookbook/alchemy/build/dsn/CheckForTpose.py and GenerateModelFromTpose.py.
REMAINING (~20 files, from grep -rn 'PROJECT_REGISTRY.current' cookbook/ --include=*.py): build/svz EnsureTimeline, GenerateDialogue, GenerateStoryboards; publish/dsn CreateThumbnails, EnsureAnchor, EnsureCharPoses, EnsureMainImage, PopulateProfileImages, PrepImages; publish/scr AnalyzeScript, DeriveCharacterLooks, DeriveLocationStates, Publish; render/svz ExportOtio, ExportPremiereTimeline, RenderAnimatic; review/edt ScriptComplianceReport; shelves/dsn DesignSlots.
CONSIDER a helper so each step is not hand-rolling the lookup - e.g. store_for(po) in alc/services/project.py, or lookup_asset_for(po) in alc/services/ai/design.py - and then a guard so a step can never reach for .current by accident. Same family as the existing active-context global-state bug (#290): global company/project clobbering per-address work.
#525In our phase details area of the Phases info panel - we should have a console that shows error messages etc.. so when a step fails it's obvious why.New featureTomDetails
·Alchemy·3 months ago
#528when i drag an image from one media to the next - it links to the exact one - with images we should create a copy so that i can edit the copy without effecting the originalNew featureTom
·App · transcript·Alchemy·3 months ago
#529Dropping the same image file into two beats makes them share one fileBug fixclaudeScript
·App · transcript·Alchemy·3 months ago
upload_visual_media (app/tools/transcript/transcript.py) names the saved file after the SOURCE filename: name = get_valid_filename(Path(file.filename).name). Drop homestead.png onto beat A and again onto beat B and both beats point at the same images/homestead.png. Each beat should own its own file: key the saved name to the TARGET beat so a re-drop overwrites only its own copy.
#530In the preview area of the transcript editor - we need a way of seeing compositing guidelines - and a way of defining the aspect ratio for the thing we're working on - we could start a toolbar at the top with options like "Aspect Ratio" and a toggle for "Show Guide" or something.New featureTomCompositing
·App · transcript·Alchemy·3 months ago
#532when i have a beat set to "visual" i can't select it - and have the playhead change - neither does the changing playhead move selection to the beat in the scriptBug fixTomScript
·App · transcript·Alchemy·3 months ago
#588on the transcript page - show the path of the .json file we are editing in the editor.New featureTomEditing
·App · transcript·Alchemy·3 months ago
#644i want a button that analyzes all todo items and groups them into sprints where code paths are shared.New featureTom
·App · kanban·Alchemy·3 months ago
#241in the unreal version view - i need to see what location i'm connected to right by the phases in the right panel so i'm always aware of the locationNew featureTom
·Cookbook · unreal·Alchemy·3 months ago
#274Rename cookbook_root -> cookbook_import_root and consolidate the cookbook sys.path anchorChoreclaude
·Core·3 months ago
The cookbook is a Python package literally named 'cookbook' (cgl-cookbook/cookbook/__init__.py). To 'import cookbook.*', the PARENT dir (cgl-cookbook) must be on sys.path -- not the cookbook folder. That parent is currently exposed as CFG.cookbook_root / company.cookbook_root, a name that reads as 'the root of the cookbook' and confuses everyone (and agents) about which folder is the git repo. The container also exists to hold potentially MULTIPLE cookbook checkouts per company in future.
DO:
1) Rename the parent concept cookbook_root -> cookbook_import_root everywhere: CFG property (alc/config/config.py:165), company.py:70 property, alc/config/env.py set_cookbook_root(), the COOKBOOK_ROOT env var references, and consumers: alc/services/deadline/cookbook_runner.py:40, mesh_runner.py:45, app/tools/cookbook/recipe_steps.py:417, route.py, software.py:238-240, and the readme/manifest JSONs that list 'cookbook_root'. Keep cookbook_root as a deprecated alias property if needed for external callers.
2) Consolidate the import anchor: ONE canonical env var (value = the parent = cookbook_import_root) that alchemy sets AND a single helper that prepends it to sys.path/PYTHONPATH. Repoint the sites that wrongly inject the GIT ROOT (cookbook_dir) instead of the parent -- alc/plugins/menus.py:37 and alc/services/deadline/deadline.py:72/156/208 (PYTHONPATH=...;{cookbook_dir}) -- to the parent anchor. Putting the git root on path is what spawned the phantom nested cookbook/cookbook folder (since deleted).
3) COOKBOOK_ROOT env var is currently set by set_cookbook_root() but NEVER read -- make it the canonical anchor (renamed) and actually consume it, or remove the dead setter. Also note set_cookbook_root imports 'from cgl.config import CFG' which may be stale.
4) Back out the redundant stopgap: cookbook_git_root field on Company (company.py), the CFG.cookbook_git_root accessor (config.py), and the value in alchemy.json -- for a single cookbook, cookbook_dir already IS the git root, so this field is redundant once the naming is clear. (A CLAUDE.md was added at the cookbook git root explaining the layout; keep or fold its content into the new naming.)
ACCEPTANCE: import cookbook.* resolves in base venv, DCC subprocesses, and Deadline jobs via ONE named anchor; no code inserts the git root onto sys.path; grep for cookbook_root returns only the deprecated alias (if kept); no phantom cookbook/cookbook can be recreated.
#290Active company/project is global mutable state — can't have two projects open at onceBug fixclaude
·Core·3 months ago
SYMPTOM: switching the active project/company in one browser tab changes core settings for every other tab AND for background/headless tasks. Two projects cannot be open simultaneously. Observed live: an app boot + a header switch flipped the persisted active company cgl->bse, which moved the Kanban board out from under a running ticket CLI session (dev/ticket_cli.py 'no ticket #274' until company_id was restored).
ROOT CAUSE: 'active company' and 'active project' are a single GLOBAL mutable value — CFG.company_id and company.last_project_id — persisted to one shared alchemy.json via CFG.commit(). The header switchers write that global default to disk on every explicit switch: app/routes/pages.py select_project (~L75-85: set_current_company/set_current_project) and select_company (~L115-129). alc/config/config.py set_current_project (L290-293) / set_current_company (L305-308) both call commit() (L374-393) which rewrites the whole alchemy.json.
The READ path is already per-request-correct: app/context.py AlcContextMiddleware activates a contextvar from the /c/{cid} URL, and get_dashboard_context is pure-read. The URL already carries company (/c/{cid}) and project (?project=). So the per-tab selection mostly EXISTS in the URL already; the bug is that switching still writes a GLOBAL persisted default, and non-request consumers read that global default instead of an explicit selection.
GLOBAL-DEFAULT CONSUMERS to make explicit/session-scoped: WorkBoard.path() reads CFG.project.path_object (app/tools/kanban/kanban.py) so the board follows the global active company; dev/ticket_cli.py + any headless/background worker keys off persisted CFG; app/main.py lifespan appears to commit config on shutdown (persists last global state).
FIGURE-OUT (design decision this ticket must resolve, THEN implement):
1) Make active company/project per-session/per-tab, not global. Likely: source the 'default' from a session cookie (per browser), keep the URL as the source of truth within a tab, and STOP calling set_current_company/set_current_project+commit() on ordinary navigation/switch.
2) Decide what 'active' means for headless consumers (ticket CLI, farm workers, WorkBoard): pass explicit --company/--project (already supported by ticket_cli) and have WorkBoard.path take an explicit context rather than reading CFG.project.
3) Audit every CFG.commit() on a GET/navigation path — config writes should be reserved for genuine config edits (settings save, add company/project), never for 'which project am I looking at'.
ACCEPTANCE: two browser tabs can hold two different projects at once with no cross-talk; switching project in one tab does not modify alchemy.json or affect the other tab or a running ticket CLI; headless/board consumers resolve their target from explicit args or session, not the global persisted default.
#389[cookbook] [alchemy] [dsn] Implement the Height buttonNew featurealchemystub
·Cookbook · alchemy · shelves · dsn·Alchemy·3 months ago
We may need a different Button Kind of "widget" for this. I essentially want a way to see/edit the height of the character here since it's so central to everything after the design publish.
#314[cookbook] [premiere] [amc] Implement the Ensure Shot Names buttonNew featurestub
·Adobe Premiere Pro·3 months ago
This will go through and ensure that each "shot" is a nested sequence with an approved SEQ_SHOT naming convention
use this previous alchemy amc publish as a guide - we have many of the tools for this here already although these steps are from an older version of the cookbook:
E:/Alchemy/cgl/config/cgl-cookbook/cookbook/alchemy/publish/amc
#315[cookbook] [premiere] [amc] Implement the Export timeline mp4 buttonNew featurestub
·Adobe Premiere Pro·3 months ago
Render the timeline to the render director of the pathObject with default alchemy naming.
[sequence_shot] or [po.parent_po.child] po.parent comes from the path of the premiere project that is opened. po.child (the shot numbers) is sequential. 0010, 0020, 0030,
use this previous alchemy amc publish as a guide - we have many of the tools for this here already although these steps are from an older version of the cookbook:
E:/Alchemy/cgl/config/cgl-cookbook/cookbook/alchemy/publish/amc
#316[cookbook] [premiere] [amc] Implement the Export OTIO buttonNew featurestub
·Adobe Premiere Pro·3 months ago
Export an otio file of the edit.
use this previous alchemy amc publish as a guide - we have many of the tools for this here already although these steps are from an older version of the cookbook:
E:/Alchemy/cgl/config/cgl-cookbook/cookbook/alchemy/publish/amc
#317[cookbook] [premiere] [amc] Implement the Extract Audio buttonNew featurestub
·Adobe Premiere Pro·3 months ago
Extract audio from the mp4 file to a wav file.
use this previous alchemy amc publish as a guide - we have many of the tools for this here already although these steps are from an older version of the cookbook:
E:/Alchemy/cgl/config/cgl-cookbook/cookbook/alchemy/publish/amc
#270[cookbook] [alchemy] [dsn] Implement the Ensure Height buttonNew featurealchemystub
·Cookbook · alchemy · publish · dsn·Alchemy·3 months ago
Ensure that there is a set Height for this asset before moving forward - all scale calculations depend on this down the line. Important to get right in the design phase.
#318[cookbook] [premiere] [amc] Implement the Create Shot Previews buttonNew featurestubPrevis
·Adobe Premiere Pro·3 months ago
For each shot in the timeline create an mp4 file in the /shots directory of the po.render_dir. Use OTIO file as a guide for where to create the cuts.
use this previous alchemy amc publish as a guide - we have many of the tools for this here already although these steps are from an older version of the cookbook:
E:/Alchemy/cgl/config/cgl-cookbook/cookbook/alchemy/publish/amc
#319[cookbook] [premiere] [amc] Implement the Create image Sequences for shots buttonNew featurestub
·Adobe Premiere Pro·3 months ago
For various applications we need an image sequence (4 digit padding) of each shot. create the image sequence at: /shots/[shot_name]/[shot_name].####.png
use this previous alchemy amc publish as a guide - we have many of the tools for this here already although these steps are from an older version of the cookbook:
E:/Alchemy/cgl/config/cgl-cookbook/cookbook/alchemy/publish/amc
#329Tracker: thumbnail column on the far leftNew featureclaude
·App · routes·3 months ago
Add a leading thumbnail column (far-left, fixed ~40-48px) to the Tracker grid. Assets already expose AlchemyAsset.asset_thumb ({exists, path} root-relative for /alcroot). Scenes (SceneAsset) have no equivalent and need a scene thumbnail resolver (off PathObject .preview/.thumb per the review-media-paths convention, resolved from the scene address). Graceful placeholder when no thumb. Sits before the name column; may participate in the data-col hide/show machinery.
#331Tracker: create a missing asset inline from the scene pill editorNew featureclaude
·App · web ui·3 months ago
In the scene link pill widget (characters/props/location), when the typed text matches no existing option, offer a '+ Create <text>' action (and/or on Enter). It should open the SAME create dialog used in the asset grid (GET /create/{asset_type} -> create_modal.html into #modal-container) prefilled with the typed name and the right category (env for location, chr for characters, prp for props). On submit, instead of the asset-grid re-render that /create/{parent} does today, run a tracker-aware create-and-link: create the asset via alc.plugins.alchemy.create.asset, then set the scene link (reuse edit_scene_link logic in app/routes/tracker.py), and return the updated tracker_pill_cell.html swapped back into the originating <td> (plus close the modal). Needs: create_modal.html to accept a prefilled name + optional hidden link-context fields (scene_id, field), and a new POST endpoint (e.g. /tracker/scene/{scene_id}/create-link/{field}). Location maps to the Environments/env category; a hand-authored scene like INT should be able to spin up a 'village' environment without leaving the tracker.
#341Edit Transcript (on things where we start with audio, and get a transcript) should be updated with the layout of the script viewNew featureTomLayout
·App · transcript·Alchemy·3 months ago
#342Apply the one-visual-per-line beat-board rule in Edit TranscriptNew featureclaudeScript
·Ontology · models·Alchemy·3 months ago
Edit Transcript mode is exempt from the beat-board invariant (one row = one visual = one timeline clip): TranscriptEdit._ensure_row_visual and enforce_one_visual_per_row both early-return on self.is_transcript. Drop both gates so transcript rows are born with their one whole-phrase-anchored visual, refuse a second, and heal on open (stale t_start/t_end cleared) exactly as create-mode rows do. Follow-on from #341, which gave Edit Transcript the Create Script layout.
#343Visual-only blocks should carry a real visual tile, not a left-hand note inputAdjust existingclaudeScript
·Ontology · models·Alchemy·3 months ago
A visual-only block (block_type='visual', wordless music/instrumental span) expresses its visual as a plain 'visual_note' string rendered as a full-width text input on the LEFT, and is explicitly denied the visuals column (transcript_rows.html: 'if _video and not _is_visual'). That makes it a SECOND, weaker kind of visual: no VisualDescription, so no cast/location chips, no media drop, no Generate. Give a visual block the same one VisualDescription every other row gets, rendered as a tile on the right, and leave its left side empty (just the VISUAL chip). Migrate existing visual_note text into that visual so nothing is lost.
#344Clearing a visual's text should blank the card, not delete the visualBug fixclaudeScript
·Ontology · models·Alchemy·3 months ago
TranscriptEdit.set_visual_text removes the visual entirely when given empty text (the old 'abandoned add' convention, from when visuals were added by hand). Under the beat-board invariant every row is BORN with its one visual, so there is no abandoned add - and clearing a description now silently breaks the one-visual-per-row 1:1 until the next open heals it. Empty text should blank the description and keep the card. The tile's trash button (remove_visual) has the same problem: it deletes the row's only visual and leaves the row bare, so it should clear the card in place instead.
#347when i set the location on the visual for the beat board row, it doesn't stickBug fixTom
·App · transcript·Alchemy·3 months ago
#348i want to simplify the style panel - we should really potentially be starting with either using the reference from the assets we're referencing or using our "storyboard" style that we use for prompting elsewhere in the app.New featureTomReference
·App · transcript·Alchemy·3 months ago
for now i really only want three options:
"Prompt", "Reference", and "Storyboard"
Prompt - means i'll provide a prompt for the style
Reference means we'll be using published design reference for the location characters, props
Storyboard means we'll be using published design reference but rendering in storyboard aesthetic.
C:\Users\tmiko\PycharmProjects\alchemy\app\tools\script\script_viewer.py we have a render storyboards button here and i know we have a great prompt for this- i don't remember where we're defining that prompt though, but we should be able use the same mechanism ultimtaely
#362on the version view - if i'm on an entity_type = "assets" i want to see an editable "height" field on the "shelf"New featureTomDesign
·App · web ui·Alchemy·3 months ago
this is on the version view. see the reference. We should draw this widget before any cookbook shelf buttons - i should have a "height" label and the ability to edit the text.
we can use the same widget/logic we use on the character dashboard where it will accept things like 6' 1" or 6ft 1in, or 185cm resulting in us having both metric and imperial height for these items (we use this later when we ensure scale etc... this only applies to the design task for now. if there's a way to do this as a button in the cookbook it might even be better - i just haven't attempted to embed a "widget" this way before. I'm adding a place holder here: so that we can experiment with that idea. E:/Alchemy/cgl/config/cgl-cookbook/cookbook/alchemy/shelves/dsn/Height.py
#390Locations rail: define a video's location once and have every beat inherit itNew featureTomScript
·App · transcript·Alchemy·3 months ago
Add a LOCATIONS section to the transcript rail, under Characters, mirroring the Characters panel. A location declared there is the video default: every beat inherits it unless that beat pins its own. Multi-scene videos declare additional locations and pin them per beat. Panel rows show the environment's reference thumb, beat count, a DEFAULT badge on the first, 'Make default' on the others, and a remove control. On a beat tile an inherited location renders as a dashed ghost chip that KEEPS an override input beside it, so a second scene stays pinnable.
#419I need the concept of "Playlists" for Media / Review MediaNew featureTomConcept
·App · media·Alchemy·3 months ago
we're goign to need a playlist model that is essentially an ordered list of "reviews". then we're going to need a way of creating, and selecting playlists - i'm thinking that we have a left panel that we can use to create/display/select playlists. if we doulbe click a playlist it'll go into the review room with those items for review.
#421[cookbook] [alchemy] [default] Implement the Add To Dailies Playlist buttonNew featurealchemystub
·Cookbook · alchemy · review · default·Alchemy·3 months ago
Ensure a Dailies playlist for the current task exists for today's date, ("Model Dailies 8-1-2026") and add the review(s) to the playlist.
#248when i'm in the unreal version view and version up, refresh should keep me in that version view - it currently takes me to the default version viewBug fixTom
Author the Unreal lookdev BUILD phase (recipe unreal/build/ldv), mirroring the proven mdl build pattern (ImportLatestModel + BuildModelTurntable, PR #36). ldv build: import the latest model, apply the published lookdev materials/textures (alc/plugins/unreal/tasks/ldv/create.py build_material/assign + tex import), then build the turntable scaffold via common.create.build_turntable (level + LevelSequence + framed camera + HDRI backdrop, save_current_level). Reuses the connected-session routing + turntable framework already shipped for mdl. Differs from mdl build (gray shaders): ldv is the beauty/material look. Steps live in cgl-cookbook/unreal/build/ldv/.
#251[unreal][ldv] Render recipe — lookdev turntable via MRQNew featureclaude
·Unreal Engine·3 months ago
Author the Unreal lookdev RENDER phase (recipe unreal/render/ldv), a near-copy of unreal/render/mdl (SaveScene -> CheckForSequencer -> SetRenderSettings -> RenderTurntable -> ProcessRenders) pointed at ldv coordinates. Reuses common/create.render_sequence + setup/headless.render_sequence_headless (the -game MRQ render) UNCHANGED — render_sequence is task-agnostic. Set deadline: true so it farms by default via the app-side cli_runner path (PR #36). Output = lit lookdev turntable mp4 to the version render dir. Steps live in cgl-cookbook/unreal/render/ldv/.
Make the two failing cross-DCC parity cases pass: (1) unreal mdl/create.py import_file signature drift — the Unreal impl adds game_folder/asset_name kwargs the ModelCreate contract (alc/plugins/smart_task/mdl.py) does not declare; add them to the contract or make them internal. (2) unreal prv/query.py is missing PrevisQuery contract ops ['get_shot_cameras','list_sequences'] (alc/plugins/smart_task/prv.py) — implement them or reconcile the contract.
#263Camera body library — pick a physical camera (filmback/lens presets) for a shot cameraNew featureclaude
·Plugin · unreal·Unreal Engine·3 months ago
A shared library of real-world camera bodies (Arri Alexa 35 / Mini LF, RED V-Raptor, Sony Venice 2, etc.) selectable on a shot camera - so instead of hardcoding a 36mm filmback, an artist picks a body and sensor width/height (and native lens options) come from the preset. Design as a cross-DCC data registry (same pattern as the HDRI library / status registry): one source of truth - e.g. alc/config/cameras.py or a JSON - {body_name: {sensor_width, sensor_height, notes, native_lenses:[mm]}}. Consumers: (UE) cam.edit.set_camera_body(name) sets the CineCamera Filmback (UE already has Filmback presets - seed the library from them + custom bodies); Camera Caddy UMG gets a Camera Body dropdown above the Lens dropdown. (Blender, follow-up) same registry drives cam.data.sensor_width/height on the MagiCam camera. Resolves the BP_MagiCam filmback caveat (#258) properly: body choice makes sensor + FOV correct by construction. Add get_camera_bodies() query + set_camera_body verb to the cam contract so it is portable.
#265UE MagiCam: frustum-fit plane scaling (planes fill the frame at their depth)New featureclaude
·Plugin · unreal·Unreal Engine·3 months ago
The 10 multiplane planes on BP_MagiCam (#259) currently keep a fixed 1:1 scale and do NOT resize to the camera frustum - so a plane at depth D doesn't fill frame. Add frustum-fit: each plane scales to fill (fit) the CineCamera's frame at its own depth, so it acts as a full-frame multiplane layer; parallax then comes from the depth separation as the camera dollies. MATH (CineCamera): at distance d along camera forward, frustum height H = d * sensor_height / focal_length, width W = d * sensor_width / focal_length (d = plane distance from the rig camera; sensor from the body/filmback #263, focal from the lens). Engine Plane is 100 units, so plane scale = H/100 (Z) x W/100 (Y) in its upright orientation; combine with image aspect from the auto-size leg (#260) - fill height, match aspect for width (or letterbox). UPDATE TIMING (design): frustum-fit is computed at the plane's REST depth, recomputed only when depth/lens/body changes - NOT per camera-move (classic multiplane: planes are fixed-size at setup, the shot's camera move then produces parallax). So fold the recompute into the cam.edit ops set_plane_depth / set_lens / set_camera_body (#260/#263), which already fire on exactly those changes; no BP tick needed. Verify in the live editor: a plane at any depth fills the CineCamera frame, and dollying reveals parallax between layers.
#278[unreal] [rig] Rebuild stale IK Rig / Retargeter instead of reusing a bad first buildBug fixclaudeRig
·Unreal Engine·3 months ago
_ensure_ik_rig and _ensure_retargeter in alc/plugins/unreal/tasks/mtn/create.py are load-or-create: if a first turntable build produces a bad IK Rig / Retargeter (e.g. an interrupted or mis-placed run), every subsequent build reuses the broken asset and the failure silently persists. Add a way to force a clean rebuild — e.g. a force_rebuild flag threaded from build_calisthenics_turntable (and BuildRigTurntable) that deletes + recreates the rig's IK Rig / Retargeter (and optionally the baked/ dir) before retargeting.
#327[cookbook] [unreal] [prv] Implement the Create Cameras from amc shots buttonNew featureunrealstub
·Cookbook · unreal · build · prv·Unreal Engine·3 months ago
Create a sequencer camera (with magicCam rigs) for each shot in the published animatic. Use the animatic's msd to find the right images for the magicCam's image plane(s)
Build the missing UE motion-retarget backend + a reviewable 'rig calisthenics' turntable that proves the UE-native previs pipeline (Tier A) end-to-end, 1:1 with the Blender calisthenics turntable.
(1) alc/plugins/unreal/tasks/mtn/ — the UE motion task, mirroring blender/tasks/mtn/ and conforming to smart_task/mtn.py (MotionCreate/MotionQuery). Core op: retarget a clip onto the rigged character via UE IK Retargeter — create IK Retargeter (source IK rig -> target character IK rig), auto_map_chains/auto_align_all_bones, bake to AnimSequence (IKRetargetBatchOperation). Uses the STABLE spike-confirmed IKRetargeter API (NOT Control Rig, which crashes — see alc/plugins/unreal/CONTROL_RIG_SCRIPTABILITY.md). Consumes import_rigged_fbx + setup_ik_rig + the bone ontology (alc/models/skeleton.py) for name conform so chains map.
(2) A rig-calisthenics turntable build recipe (cookbook unreal/build/, mirror mdl/ldv turntable): level + ~300f LevelSequence with several motion clips retargeted back-to-back (range-of-motion QC), framed camera (reuse common.create.build_turntable), MRQ render -> reviewable mp4.
Dependency: source motions. Project currently has only 2 tutorial anims (Tutorial_Idle, Tutorial_Walk_Fwd) + TutorialTPP_Skeleton — needs a real clip set imported for UE (Blender motion-library FBX onto a source skeleton, or a Mixamo/mocap set). Ontology conforms both source+target naming.
Groundwork (this sprint, proven): conform + import_rigged_fbx + setup_ik_rig + rig-task marks; tani Meshy rig live at /Game/assets/chr/tani/rig/default/.
#272in the right click menu for source/render files items - i need a "copy file path" and copy folder path button right above "view in explorer"New featureTom
·App · web ui·Alchemy·3 months ago
#269/cookbook/alchemy/build/dsn/GenerateModel.py we need to adjust image_to_mesh to generate a rig as well.New featureTom
·App · cookbook·Alchemy·3 months ago
this is really about the image_to_mesh function this path is pulling in.
/cookbook/alchemy/build/dsn/GenerateModel.py
we either need a flag in that function or a "image_to_rig" function so that character meshes we create come with rigs by default - we may have to put some extra tooling into our character metadata so that we ensure that we know if it's a biped character for example (or humanoid) before running this, i don't know that meshy can give us rigs for non humanoid characters. the other thing is if meshy can give us default rigs (single bone) on props we can also use it there. some of this ticket will be discovery
#275[cookbook] [unreal] [rig] Implement the Create Rig from Design GLB buttonNew featureunrealstub
·Cookbook · unreal · build · rig·Unreal Engine·3 months ago
Create a meshy rig from the published design .glb if there is one.
#276[cookbook] [unreal] [rig] Implement the Import GLB rig buttonNew featureunrealstub
·Cookbook · unreal · build · rig·Unreal Engine·3 months ago
Import the GLB Rig into the PathObject location for the asset/ rig within UE
#277[unreal] [turntable] Disable depth-of-field on the mdl + ldv turntable camerasChoreclaudeRig
·Plugin · unreal·Unreal Engine·3 months ago
The rig turntable's CineCamera now disables depth-of-field (alc/plugins/unreal/tasks/mtn/create.py calisthenics_turntable) so the framed subject renders sharp. Apply the same DoF-off to the mdl and ldv turntable camera builders for consistency, so model and lookdev review turntables aren't soft/fuzzy either.
#73Ontology/taxonomy monitoring page in the Pipeline tabNew featureclaude
·App · features·3 months ago
Add a dedicated ontology/taxonomy page (in the Pipeline tab) that gives a visual, at-a-glance way to monitor the app's own taxonomy - tools, pages/routes, areas, features - and flag entries that are named poorly or not uniquely enough. Purpose: keep the ontology honest as the app grows and surface 'litter' (duplicate/near-duplicate pages, ambiguous tool names like Script vs Transcript, stale routes). Each registered page/tool should be listed with its route, owning area, and a visual confirmation thumbnail (via the existing screengrab service), with lint-style flags for name collisions, near-duplicate labels, and pages that render nearly identically but are different tools. Builds on the feature-ontology tool (app/tools/features) and the navigation registry (app/navigation.py). Sister ticket to #61 (which clarifies the specific Script-vs-Transcript naming and adds per-page thumbnails to the tool picker); this ticket is the broader monitoring surface.
Exploration spike: in the LIVE UE 5.8 editor via the Alchemy connected-session bridge (PR #36), map which 5.8 rigging APIs are Python-scriptable vs editor-UI-only, so we know where to build Alchemy shortcut tools. Confirmed scriptable: unreal.SkeletonModifier (Static Mesh->Skeletal Mesh, add_bones, parent_bone, commit_skeleton_to_skeletal_mesh). Verify hands-on: (a) is Bind Skin auto-weighting (Direct Distance / Geodesic Voxel) callable from Python (unreal.SkinWeightModifier)? (b) can Mesh-to-MetaHuman body conform be triggered scripted, or is it Creator-UI-only? Editor-interactive is FINE — goal is a scriptability map (fully/semi/manual per step), not headless purity. Output scopes the tool tickets below.
Author a spawnable BP_MagiCam Blueprint that is the 1:1 UE prefab of the Blender MagiCam rig (magicam-blender's camera_rig_multiplane_simple2.blend). Nested Scene Component hierarchy Base -> Height -> Dolly -> Tilt with a CineCameraComponent on the end (matches MagiCam's armature bone chain base->height->dolly->camera). Filmback sensor width set to match MagiCam's camera (Blender 36mm default) so the 16/24/35/50/85/120/200mm lens presets give identical FOV. Expose the four controls as selectable sub-objects/gizmo actors so an artist can grab-and-drag each (Base/Height/Dolly/Tilt), mirroring MagiCam's visible dolly/tilt_arrow gizmo objects. Spawned per shot, not assembled each time. NOTE: MagiCam exposes NO DOF/aperture/focus/shake/roll/pan - do not add them (CineCamera gives them free but keep the port 1:1).
#243Farm-render Unreal on Deadline (flip deadline:true on the render recipe)New featureclaude
·Service · deadline·Alchemy·3 months ago
The headless UE recipe run (run_recipe_headless: spawn UnrealEditor-Cmd, run the recipe, quit) is exactly what a Deadline Worker runs — SmartScene._run_recipe already routes deadline:true recipes through run_cookbook to a Worker that spawns the DCC headless. So farm-rendering the Unreal turntable/lighting should be a deadline:true flag on the render recipe. Validate end-to-end: submit the unreal render recipe to Deadline, a Worker syncs the (configure_unreal_python_env'd) project, spawns headless UE, renders via MRQ, writes frames. Provision/verify a GPU Worker with UE installed + the version-scoped site-packages deps.
#253[unreal][rig] MetaHuman auto-rig workflow for any humanoid modelNew featureclaude
·Plugin · unreal·Unreal Engine·3 months ago
Flagship humanoid rig feature. Build an Alchemy workflow around UE 5.8 Mesh-to-MetaHuman (conforms full bodies of ANY topology -> a fully-rigged MetaHuman; Epic explicitly supports AI-generated meshes). Target: any humanoid model (incl. our design->mesh outputs) -> a solid, animation-ready MetaHuman rig. Interactive-editor acceptable if conform isn't scriptable — wrap it with Alchemy tooling driven from the connected session: resolve the input mesh from the PathObject, run the conform, land the rigged MetaHuman in the rig task. Humanoid-only; note MetaHuman topology + licensing caveats. Complementary cloud path: the dormant MeshyClient.rig_humanoid (alc/services/mesh/meshy.py).
The one genuinely-new piece of the MagiCam port. Build the image-plane component used by BP_MagiCam (#258): a static-mesh plane + translucent unlit material showing shot artwork, with (1) construction-script auto-size reading Texture.GetSizeX/Y so the plane fits its image aspect (ports MagiCam's driver-scaled planes), (2) an opacity scalar param on a dynamic material instance (replaces MagiCam's magicam_opacity multiply node feeding Principled alpha), (3) image-sequence playback for hold-then-step drawings (MagiCam keys texture frame_offset; UE uses a flipbook / media texture - previs-good-enough, refine later). BP_MagiCam carries a stack of 10 of these; depth/parallax is purely the plane's local distance from the camera (no parallax property). Per-plane visibility windowed to its shot via Level Sequence keyed visibility (shot ranges from cam.query.get_shots).
#75we don't need the thumbnail on the character tag on the trs-vis widgetNew featureTom
Add the missing Unreal rig ontology + a shortcut toolkit for a 'good enough for previs, fast' rig. (1) alc/plugins/unreal/tasks/rig/ — the rig SmartTask create/edit/query, mirroring alc/plugins/blender/tasks/rig/, picking UE's root-control convention (contract alc/plugins/smart_task/rig.py names ARP c_pos / mGear world_ctl). (2) alc/plugins/unreal/tools/skeletal_editor.py — wrap UE 5.8 SkeletonModifier + Bind Skin as the shortcut ops every rigging system has (place bones from a template/landmarks, auto-skin via Geodesic Voxel, mirror, bone-count reduce) so we don't have to learn each system. Fill the empty BuildControlRig.py stub (cookbook/unreal/start/rig/). Follows the mdl tasks/tools pattern; editor-interactive OK.
#260UE MagiCam: extend cam.edit with rig verbsNew featureclaude
·Unreal Engine·3 months ago
Extend alc/plugins/unreal/tasks/cam/edit.py with the MagiCam control verbs (name-in / dict-out, bridge-drivable, parity-checked against the cam contract). Ports the Camera Caddy actions from magicam-blender's caddy_blender.py onto BP_MagiCam (#258): select_control(name in base|height|dolly|tilt) -> select/pilot that gizmo for the transform tool; set_lens(mm) -> CineCameraComponent CurrentFocalLength; key_camera() -> key the four control transforms in the current Level Sequence at the playhead; copy_pose()/paste_pose() -> read/write control transforms keyed by control name so a pose maps across shot rigs (exclude image planes); set_plane_depth(n,dist), set_plane_opacity(n,v), set_plane_visible(n,bool), plane_make_static(n). Contract-portable ops promote to smart_task/cam.py; MagiCam-only ops stay in the Unreal module (contract subset-of concrete, per the cam etymology note). Marker-driven multi-cam cuts already covered by existing edit.set_active + query.get_shots.
#80when a sprint is empty - clear it out from the sprint column.
·App · kanban·Alchemy·3 months ago
automatically clear out empty sprints on the sprint column
Add the Unreal rig BUILD phase on the mdl build/render pattern (PR #36): cookbook/unreal/build/rig/ (rig.json + ImportModel + AutoRig steps), optionally unreal/publish/rig. Wire the ALREADY-WRITTEN but never-called MeshyClient.rig_humanoid (alc/services/mesh/meshy.py — PNG-mesh -> rigged FBX via Meshy cloud rigging API) into a headless design->rigged-FBX->UE-import flow, reusing dsn.create.submit_mesh_gen as the submit pattern and mdl/create.py AssetImportTask for the /Game import, then IK Retargeter -> Mannequin. Depends on the rig task package. Target: fast previs rig via the farm, same framework as mdl render.
Reproduce MagiCam's Camera Caddy N-panel as an in-editor UMG Editor Utility Widget, docked in the Unreal editor - the artist-facing face on the cam.edit rig verbs (#260). Controls, 1:1 with caddy_blender.py: Camera dropdown (one shot = one camera); Camera-at-Playhead button; Start/End/View-Range row; Camera Controls section with Base/Height/Dolly/Tilt buttons (each calls select_control); Lens dropdown (16/24/35/50/85/120/200mm); Key Camera button; Copy/Paste transform buttons with a 'Copied: <source>' label; Image Planes list - one row per plane with an eye visibility toggle + a select button (depresses when active), and when a plane is active a box with an Opacity slider (0-1) + Create Static button. Widget calls the cam.edit ops (in-process or over the Alchemy unreal bridge). This is the same Qt->native-panel move MagiCam itself already made, one engine over.
#81Make the Generate Buton Functional on the Visual block for the script row.New featureTomScript
·App · transcript·Alchemy·3 months ago
For the first pass i just want you to generate the payload for the ai / nano-banana service we have - we already have a lot of great image generation code.
On these - we want the main published design file for any flagged asset as reference, for example if "oswald" the character is flagged find it's main file in it's published pathObject msd. we can use that as reference. same with locations. We can also pull the style escription from the model and ensure that's in the prompt. At first all i want the "Generate" button to do is print out (or copy to the clipboard) what the prompt would look like and the paths to the files it's using as reference, this will allow me to essentially see how we're building our payload before executing the ai call.
#82Unpublished cast/location pills go red + click to the design version viewNew featureclaudeScript
·App · transcript·Alchemy·3 months ago
On the Visual block's cast/prop/location chips (script row), colour a chip RED when the tagged asset has no published design yet, and make the chip label a link to that asset's design version view so it can be published or created on the spot without leaving the flow.
#83PHase button behavior on the version view info panelNew featureTomDetails
·App · web ui·Alchemy·3 months ago
the System / Area should be version view - but we don't have that as a choice in the taxonomy right now. (we should make a ticket for that)
Thisis about what happens in the right info panel when you click a phase button:
Build: Show Phase details
Render: PHase Details
Review: Phase Details (if notes exist - show notes)
Art: SHow "To-do"
Publish: Phase Details
#85on the generation payload modal - it'd be very cool to be able to copy images to the clip boardNew featureTom
·App · transcript·Alchemy·3 months ago
a copy button next to each image path to copy it to the clipboard.
#87generate button is working great, we should connect that to our nano-banana serviceNew featureTom
·App · transcript·Alchemy·3 months ago
#88on the transcript page i want to see the media in the playback area that corresponds to the clilp, just like i currently see the textNew featureTomTranscript
·App · transcript·Alchemy·3 months ago
the NLE / timeline should play back images and video as it plays
#89when i select text to anchor it to an image - then hit "connect selected" - i lose focus and the scroll area scrolls toward the bottom of the page.Bug fixTom
·App · transcript·Alchemy·3 months ago
#90on the kanban ticket modal - id like a way to draw on the attached imagesNew feature
·App · kanban·Alchemy·3 months ago
it seems like if we routed the clicking on the image to the image editor in the app this would allow us to use the draw tool to draw on the reference image.
As long as saving (which i think already does this) simply saves over the file this should work well.
#91add a "paste" icon option to the reference files section of the kanban ticket modalDesignReference
·App · kanban·Alchemy·3 months ago
I want to be able to paste an image currently in the clipboard in as a reference file for the ticket i'm editing, the button needs to be smart enough to know if we have something in the clipboard and if it's an image. It should only be enabled if we meet those conditions.
#93Mark pipelines 'live on public task map' from the cookbookNew featureclaude
·App · cookbook·3 months ago
Today, which software x task pipelines appear on the public task-map embed (embed.alchemypipe.com/task-map) is controlled by a hand-maintained JSON allowlist at cloud/maps/pipelines.json, which sits outside the cookbook. The cookbook gives no indication of whether a pipeline is live on the marketing site, and adding one means editing that JSON by hand (e.g. maya/prv is missing purely because it's not listed). Move liveness INTO the cookbook: (1) add a 'public: bool = False' field to the Recipe model (alc/cookbook/recipe.py, mirroring the existing 'deadline' bool) - backward-compatible; (2) surface a checkbox in the cookbook editor (app/tools/cookbook/) to toggle it, plus a per-pipeline live/not-live indicator so you can see at a glance what's on the marketing site; (3) change the task-map exporter (cloud/maps/export_web.py) to DERIVE its allowlist by scanning cookbooks for the flag, retiring/replacing cloud/maps/pipelines.json as the source of truth. Grain decision for triage: the task map renders a whole software x task pipeline (all phases aggregated), so pipeline-level liveness is the cleaner unit; a per-recipe checkbox can still drive it (pipeline live = its tasks/publish recipe flagged). After marking, publishing is one run of cloud/maps/deploy.ps1.
#95in the transcript tool i need a headerNew featureTomTranscript
·App · transcript·Alchemy·3 months ago
We'll need a Close button that closes the tool and returns to the version view for the version we're on.
We'll also need a mini "phases" widget - this doesn't need to be a full widget, but i do need the ability to see the "build", "art", "render", "review", "publish" buttons on it - in all honesty we may want most of the features we have in the widget on the typical version view info panel - it's more about how/when we display the information. In this implementation i'd rather have the panel drop down over the top of the tool - if i'm in dev mode for example or if there are notest to see on the review button, or todo items to see on the art button. and if i click off of it i'm back to the tool. The main reasoning here is that i don't want to lose horizontal space on the tool. This could be a common implementation for other tools as well if we make it modular enough.
#96when i'm dealing with a movie clip - give me the abilit to move the clip forward and back within the timeline rangeNew featureTom
·App · transcript·Alchemy·3 months ago
I want this to work like resolve's "cut" mode on a clip.
1) i click the [-] icon and drag to enter "slide" mode (Reference file 1)
2) as i slide i get green indicators on the left/right border of the widget indicating an edit is happeing
3) as i slide i get an indicator of how many frames/timecode i'm sliding +-
3) as i slide i can see an approximation of frames visually (this is a stretch goal - if it's too complicated let's just make a new ticket describing it.
The desired result is that i can move a clip back/forth easily on the timeline after dragging it into the visual widget.
#103when i create a "step" in the cookbook - it should be creating a ticked in the kanban backlogBug fixTom
·App · cookbook·Alchemy·3 months ago
this feature was one i thought we already wrote but maybe it went stale, or we never got it quite finished.
#142add more relevant source files to cookbook workItemsNew feature
·App · kanban·Alchemy·3 months ago
C:\Users\tmiko\PycharmProjects\alchemy\alc\cookbook\cookbook.py
(we should add C:\Users\tmiko\PycharmProjects\alchemy\alc\cookbook) to releveant files on cookbook tasks if we con't currently have it.
Cookbook features don't reqiure a UI keyword - so we can remove that from the requirements, we still want to select the file for the target code scope, and want to have cookbook related files for the target code scope as well.
I'd also include smart task and plugin information if it exists.
C:\Users\tmiko\PycharmProjects\alchemy\alc\plugins\blender\tasks\ldv
C:\Users\tmiko\PycharmProjects\alchemy\alc\plugins\{software}\tasks\{task}
these smart tasks give us the ontology for the task and the software.
if the task/software doesn't exist we can often look to other tasks/software for patterns in order to build the plugin / task. All these data points are important to include with a cookbook task to give agents full context.
SmarTask lives here:
C:\Users\tmiko\PycharmProjects\alchemy\alc\plugins\smart_scene.py
Agnostic menus live here:
C:\Users\tmiko\PycharmProjects\alchemy\alc\plugins\menus.py
these are all building blocks of cookbook recipes that make them fairly easy to write code for.
#228[cookbook] [blender] [rig] Implement the Create HDRI buttonNew featurestub
·Cookbook · blender · render · rig·Blender·3 months ago
Create a Blender HDRI in the scene. use the default for the function from alc\plugins\blender\create.hdri() library to create the environment light. setting the environment to the default studio setup.
#229on the cookbook departments list i need to be able to select "Default" as a departmentNew featureTom
·App · cookbook·Alchemy·3 months ago
We need to add default as a selectable option to match the "default" department we have in most software folders within the cookbook structure .
#230I need to have a Toggle to make a cookbook step Active vs inactive.New featureTom
·App · cookbook·Alchemy·3 months ago
I want a a toggle for recipe steps so that i can "turn them off" or turn them on. this could touch a lot of things for example if a step is off it shouldn't even show up as a step to the system, it should be ignored/skipped. this allows me to ignore a step but not delete it completely.
#231[cookbook] [alchemy] [default] Implement the Mark for Review buttonNew featurealchemystub
·Cookbook · alchemy · review · default·Alchemy·3 months ago
mark the mp4 file we've created in this task for review within Alchemy.
#232I need an Unreal Engine Tab in the main tabs for alchemyNew featurealchemyTom
·App · web ui·Alchemy·3 months ago
for starters we will want some unreal specific lines on the menu. The first thing we're going to want is the equivalent of the "locations" widget we had in Unreal Engine. I think it's probably a good idea to still call them "locations" although i think we may be using the term "world" in PathObject. i honestly can't remember if we brought all of the UE features over in the refactor, so we just need to start buy analyzing this widget: C:\Users\tmiko\PycharmProjects\cglumberjack\cgl\apps\alchemy\widgets\unreal_widget.py and attempting to salvage the features - we don't need to reproduce the code just look at the features we had going and build off that - there's a lot of perforce syncing visuals in the UI, etc... PathObject handles UE paths different as well - according to where the perforce_root is set in the company config . here's a path with a valid perforce directory that has items in it: C:\alc\p4v\cgl\JLT\PERFORCE so we can use that to figure out the display - that's the first goal - see if we can set up those links and simply show the ue worlds/locations/environments - whatever we want to call them. I use "locations" because that's typically how we think of them in film - we're going to this "location" to shoot - so sequences related to that Location will be found there.
Introduce a per-project UnrealSession primitive: which world/location is 'connected', whether a UE instance is attached, and the UE version. Double-clicking a location in the Unreal > Locations panel connects to it (record the active world; port the old PySide entry points open_project_in_unreal / configure_unreal_python_env from cglumberjack unreal_widget.py / alc/plugins/unreal to launch or attach UE with that project + inject the alc python env). Render a persistent 'Unreal header' across the whole Unreal workspace showing connection state: '● Connected to <world> · UE <ver> · [Open] [Disconnect]', and an obvious disconnected state. This is the spine every other Unreal-lens behaviour hangs off (build dispatch, live-scene view).
#234Route ldv Build to the connected Unreal instance (+ Unreal scene gateway)New featureclaude
·Alchemy·3 months ago
In the Unreal lens, clicking Build on a character's ldv task should run the UE lookdev recipe against the currently-connected location and land the turntable/render back as that task's version media. Two parts: (1) Add the missing Unreal scene gateway alc/plugins/unreal/alchemy.py (class Scene(SmartScene), software='unreal') so run_task / SmartTask.scene can open the connected world, save, and commit_version — every other DCC has one, Unreal doesn't. (2) Dispatch: the version/dashboard Build, when dcc=unreal + a connected world (from the UnrealSession, ticket #233), resolves unreal.tasks.ldv and executes in the connected UE python, then commits the turntable output as the task version. No PySide — UI is Alchemy-web, execution is UE-python.
#240in the unreal version view - i need to see the "Unreal Source Files" as well as the "renders" for the version i'm in. (we also have to sort out how to ensure there's a new version to work with for UE)New featureTom
·Cookbook · alchemy · context-menus · files·Alchemy·3 months ago
#37when i hit enter on a block to create a new line i have to refresh the page to see it [bug] [kanban video]Bug fixTomScript
·App · script·Alchemy·3 months ago
when i hit enter on a block to create a new line i have to refresh the page to see it [bug] [kanban video]
#74When working in the transcript tool, i want a "Style" section where i can define the style of AI images generated within the video i'm working on.New featureTomTranscript
·App · transcript·Alchemy·3 months ago
#86after interacting with a visual card focus is lost and the scroll area scrolls to the bottom, i've noticed this when editing text and other aspects of the visual widget.Bug fixTomEditing
·App · transcript·Alchemy·3 months ago
I've seent his on multiple behaviors - text edit then tab,
#94Cookbook 'Push Live' action — publish task maps to the marketing site (CGL only)New featureclaude
·App · cookbook·3 months ago
Add a low-prominence 'Push Live' action in the cookbook UI that publishes the currently live-marked pipelines to the public task-map embed (embed.alchemypipe.com/task-map) — i.e. runs the same export + S3 sync + CloudFront invalidation that cloud/maps/deploy.ps1 does today, but from the app. Placement (per Tom, keep it non-prominent for now): a right-click context menu on the Cookbook icon in the tool header, item labelled 'Push Live' (or a clearer variant). CRITICAL UX: it must read as 'publish to the marketing site', NOT 'push cookbook changes to the repo' — show a confirm dialog naming the destination (embed.alchemypipe.com) so it's never confused with a git push; finish with a success toast linking the live URL. SCOPE: only show this for the CGL cookbook — gate on the active company being cgl (or on the presence of an embed target, e.g. cloud/maps/infra.json). Normal studio cookbooks must NOT show it. IMPLEMENTATION: implement the publish as a Python routine using boto3 (s3 put_object/upload + cloudfront create_invalidation), reading bucket + distributionId from cloud/maps/infra.json, callable from the web handler and run async — do NOT shell out to PowerShell, since the app runs on Linux EC2 and deploy.ps1 is Windows-only. Reuse export_web.py's build step to produce the bundle. Pairs with #93 (which adds the per-pipeline 'live on task map' flag); #93 decides WHAT is live, this pushes it.
#141when creating cookbook items use text tags like [cookbook] [software] [task code] in the title - will make auto-filtering extremely easy.New featureTom
·App · kanban·Alchemy·3 months ago
[cookbook] [maya] [model] for example - we should just insert that into the task title when creating it.
#246[cookbook] [unreal] [mdl] Implement the Check for sequencer buttonNew featurestub
·Cookbook · unreal · render · mdl·Unreal Engine·3 months ago
Find the sequencer for the model we're working with in the UE scene.
#42when i type a data-phrase in the data-row on these blocks and hit tab - i exepct to exit "edit" mode, but the app jumps toward the bottom of the scroll widget for all the rows.Bug fixTomModel
·App · transcript·Alchemy·3 months ago
expected behavior on tab here is to simply exit edit mode while keeping position in the scroll in tact.
#76the P1 Visual track should show text for items that have text and images for visual widgets that have images. It seems that after the design refactor the text items are no longer visible on the timelineNew featureTomDesign
·App · transcript·Alchemy·3 months ago
this is a mix bedtween a legit bug and a feature request.
#139Make 'cookbook files' a first-class path category (absolute-path root) in feature/path mappingNew featureclaude:alc_do_ticket
·Ontology · models·Alchemy·3 months ago
Add cookbook_root as an accepted root in the path-mapping / feature classification layer, alongside the code (repo) root, so cookbook-authored files are addressed as a distinct 'cookbook files' category that assumes absolute paths. Today candidate-file resolution and area classification assume every source file lives under the repo root; cookbook files live under CFG.cookbook_dir (often a different drive). #103 fixed the crash pragmatically by keeping cookbook paths absolute in resolve_candidate_files, but the concept isn't first-class: cookbook AREA path_prefixes are still joined to the repo root (so sibling-file discovery yields nothing), and PathObject has no helper to return a cookbook-relative path. Formalize it: (1) a PathObject/classify_path notion of cookbook-root vs code-root; (2) resolve cookbook-area prefixes against cookbook_root so sibling cookbook files are discovered as candidates; (3) a helper to return cookbook-relative or absolute forms as needed.
#144as an artist i want to be able to move a visual card from one row to another.New featureTom
·App · transcript·Alchemy·3 months ago
#247[cookbook] [unreal] [mdl] Implement the Set Render Settings buttonNew featurestub
·Cookbook · unreal · render · mdl·Unreal Engine·3 months ago
HD resolution, set the output path to the current version's render directory,
#43when i create a ticket from the "Request" widget on the header of the app, we need to do a better job of matching url to application and system/areaBug fix
·App · kanban·Alchemy·3 months ago
i recently created one here: http://127.0.0.1:8080/c/cgl/project/ppe/transcript/at/videos/kanbanworkflow/SEQ/scr?u=tom&v=000.000 and the kanban workItme matched itself to the cookbook for some reason if though it should be able to do a simple query and find that this route is connected to the transcript/ script editor. we need to check what we're querying and that system and fix that bug.
#97when i edit an image on the transcript view, i need to go back to the transcript view when i close and we need to clear the image's cache and reload.New featureTomTranscript
·App · transcript·Alchemy·3 months ago
#113I need a search bar on the kanban board so i can filter tickets by strings.New featureTom
·App · kanban·Alchemy·3 months ago
we can place it on the same row as the lanes, compact and focused buttons - in the middle of that row.
#242Unreal render phase — render the turntable sequence via Movie Render QueueNew featureclaude
·Plugin · unreal·Alchemy·3 months ago
The mdl BUILD phase now builds the turntable scaffold (level + LevelSequence + orbiting model + framed camera + HDRI backdrop) at strict coordinates (chr_jack_mdl_TT_Sequence). RENDER phase: render that LevelSequence through Movie Render Queue (MoviePipelineQueueSubsystem — the tail of ldv.render_turntable already shows the MRQ setup: output setting, resolution, PNG/EXR image output, deferred pass) to the version's render dir, then hand the frames to the shared review/OIIO path so the turntable comes back as reviewable media in Alchemy. Add a common/ldv render op (render_sequence), a render-phase step, and the unreal/render/mdl recipe. Resolve the render output dir from the PathObject (get_render_path) app-side and pass it in (route engine isn't grounded in UE). Should run headless (the proven primitive) or in-session.
#140Implement the Quick Create Rig button (alchemy)New featurestubDesign
·Cookbook · alchemy · shelves · dsn·Alchemy·3 months ago
This will publish the design , then run thorugh the model (build, publish) recipes and the rig (build, publish) recipes (skipping render and review for the "Quick Create".
C:\Users\tmiko\PycharmProjects\alchemy\alc\cookbook\cookbook.py
Cookbook.run() has options for only running named phases.
Problem:
A prepared ticket (#38) was silently replaced by a different ticket carrying the same number; the original was lost. It presents as "replaced, not duplicated" — afterward the board shows no duplicate, just the wrong ticket under that number and a missing one.
Root cause (not the numbering logic):
WorkBoard._next_number() is correct — it returns max(number_seq, all live item numbers) + 1, so within a single process a live number can never be reused. The real defect is a lockless cross-process read-modify-write: every writer does WorkBoard.load() → mutate → board.save() (atomic tmp+replace) with no lock. When a second process writes during the overlap — dev/ticket_cli.py (claim/done, run by /alc_do_ticket) while the web app is live, or a second app instance on :8092+ — it loaded before the first save landed, mints the same number, and its save clobbers the other. Whoever saves last wins; the other ticket vanishes. The single web process alone is safe (add_item has no await between load and save, so browser clicks can't interleave).
Fix:
Add WorkBoard.mutate(fn) that wraps load → change → save in a cross-process lock (filelock, already installed in venv), keyed on the board file. Route every writer through it — a partial lock gives false confidence, since an unlocked writer can still clobber a locked one. This also protects concurrent status/text edits, not just creates.
Relevant files:
alc/models/work_item.py — WorkBoard.load / save / _next_number / add (add mutate() here)
app/tools/kanban/service.py — add_item (all 5 creation mouths funnel here)
app/tools/kanban/kanban.py — ~17 write routes
dev/ticket_cli.py, dev/sprint_cli.py — the CLI writers (the second process in the race)
app/tools/walkthroughs/walkthroughs.py — flag/save writers
Board file: E:\Alchemy\cgl\ppe\PUBLISHES\msd\work_board.json
(~23 board.save() sites total across those files)
Acceptance:
Two processes creating/editing tickets simultaneously never lose an item or reuse a live number. Reproduce/verify by writing from the CLI and the web app in the same instant (a board-file watcher fingerprinting count/number_seq/id-set/dupes per write confirms no dropped id and no duplicate number).
#143I should be able to re-generate an image after i've created one - we currently hide the generate button if an image exists.New featureTom
Parallel agent sessions (alc_do_ticket, alc_do_sprint) share ONE working tree, so in-flight WIP from one session collides with another: overlapping commits that need manual cleanup, entangled diffs, and one session's uncommitted WIP breaking another's imports mid-task. Observed twice in one session: overlapping commits Tom had to hand-untangle, and a broken LOCK_TIMEOUT edit in alc/models/work_item.py (un-annotated pydantic attr) that took down a live alc_do_ticket session's app + ticket CLI while it worked an unrelated ticket. Adopt and encode a standard protocol. (1) ISOLATION: each alc_do_ticket / alc_do_sprint run executes in its own git worktree + branch off main (harness supports isolation:worktree; auto-cleaned). Hard isolation so no two sessions ever share a checkout; the board claim/worker stays the soft coordination signal layered on top. (2) PR UNIT: a completed sprint opens ONE pull request to main (one review, one CI run, one merge surface); the operator keeps judgment to PR a single ticket early for a hotfix or clearly unrelated work. (3) BRANCH NAMING standard: ticket/<n>-<slug> for a ticket, sprint/<name> for a sprint; sprint tickets land on the sprint branch, which PRs to main. Deliverable: update both skills (.claude/commands/alc_do_ticket.md and the alc_do_sprint skill) to encode worktree isolation + branch naming + the PR policy, and extend dev/ticket_cli.py / dev/sprint_cli.py where they need to create/track the branch or gate the PR step.
#102Can't trim the edges of visual clips on the timeline (overlapping beats / dashed border)Bug fixclaudeScript
·App · transcript·Alchemy·3 months ago
Visual beats on the P1 lane are meant to be editable (drag an edge to trim, drag the body to slide), but right now their EDGES can't be adjusted. The clips show a dashed/dotted border. Restore edge-trim for visual beats.
#47Split the work board into per-item files (board = index only)Adjust existingclaude
·App · kanban·3 months ago
Re-architect board storage so each WorkItem serializes to its own file and the board.json holds only an index: item ids, lane order, sprints, priorities, number_seq. The index is written only on add/remove/reorder/number-mint; item edits (status, text, spec, autosave, candidate_files, attachments) touch only that item's file, so concurrent edits to DIFFERENT items never contend. Complements the board lock added in #44 (index still locked; per-item edits mostly lock-free). The code already anticipates this (work_item.py docstring: 'a multi-writer board would need per-item files like the event stream'). Needs: per-item file load/save, board_rows loading N item files (or a cache), and a one-time migration exploding the existing work_board.json.
#100Auto-connect a lone visual to its block's phrase (skip manual Connect)Bug fixclaudeScript
·App · transcript·Alchemy·3 months ago
When a block has exactly ONE visual card and the block has spoken words (a phrase), the visual should auto-anchor to that phrase (the whole block's words) on creation / media drop, so the user never has to click 'Connect selected'. Today a single visual on a block stays 'unanchored - select text to connect' and only gets a timeline position via the floating fallback (the row span), which is not a real anchor: no slide handle (#96), decoupled from the phrase, and it re-reads as unanchored. One phrase + one visual is unambiguous - connect it automatically.
#13Add an In Progress Swim lane to the alchemy kanban board in the alchemy kanbanAdjust existing
·Alchemy·3 months ago
The In Progress Swim lane will be where Items with status of In Progress go while Agents are working on them.
We will want a "Done" lane as well.
And to accomplsih this work items will need a status attr in their model and manifests.
#99when i have text selected on a row, and click the "+" button for a visual, that visual should automatically be connected to the text.New featureTom
·App · transcript·Alchemy·3 months ago
#45we should auto-populate the characters section of the transcript tool with characters tagged on the Visual blocks of each row.New featureTomTranscript
·App · transcript·Alchemy·3 months ago
#101Visual clips on one row overlap on the timeline - enforce no-overlap on connectBug fixclaudeScript
·App · transcript·Alchemy·3 months ago
When a block has multiple visuals, their clips frequently OVERLAP each other on the P1 Visual lane. Overlapping visual clips should not be possible. When a visual is connected to text (anchored), account for overlaps against the other visual beats on the lane and adjust the surrounding clips (push/reflow to butt) instead of letting them stack - the same no-overlap discipline set_visual_time already uses on a MOVE.
#14I need to be able to autogenerate a walkthrough based on a cookbook pipelineNew featurePipeline
·App · walkthroughs·Alchemy·3 months ago
A cookbook pipeline goes through all task phases, so in theory we should be able to autogenerate a walkthrough (or at least build its scaffolding) based on the production task and software. I want a button that allows me to auto-generate that scaffolding by choosing task and software.
#46I should be able to hide/show any of the sections in the left panel of the script editor. for better visual focus.New featureTomScript
·App · script·Alchemy·3 months ago
#16refactor _backlog to kanban for easier ontology organizationNew feature
·App · walkthroughs·Alchemy·3 months ago
_backlog_response() this and other backlog functions are currently buried in
C:\Users\tmiko\PycharmProjects\alchemy\app\tools\walkthroughs\walkthroughs.py
we need a tools\kanban\kanban.py most likely and we should refactor what was previously 'baklog' as kanban where applicable.
#38When i select a row on the script editor - one of the blocks - the playhead indicator should move to the beginning of that "data-block-text"New featureTomScript
·App · script·Alchemy·3 months ago
playhead to move on block / row selection
#18on the alchemy kanban board "+add" button - the details pane disappears when you cilick it.Details
·Cookbook · alchemy · menus·Alchemy·3 months ago
#19alchemy kanban board - every item has a "TODO" tag - we should not show that
·Cookbook · alchemy · menus·Alchemy·3 months ago
#20alchemy kanban - track timestamp when we move a cardNew feature
·App · kanban·Alchemy·3 months ago
ensure we have timestamp on the model for the cards, ensure we have a status history on the model (we have this on tasks and versions i think we can reuse that) this also seems like an "event" we have creative "todo" events - i don't know if we just piggyback this onto those, and give them a type "code" or "development" or what the best course is, we can figure that out together as you dig into it.
#21kanban task digalogue - Alchemy System / Area dialogue "+ new area button" bugBug fix
·App · analytics·Alchemy·3 months ago
Right now i was able to create an "Alc Kanban" item in the taxonomy - which ends up not following the syntax of the rest of the taxonomy - when i click new area - it should derive the System from the Application setting (or another drop down) so that i only have to type one word and it will then mimic the standard naming convention. For example Alc Kanban should be: "App · analytics" but asking a user to go and find the emoji for that dot and to remember what the short code is for the system will guarantee errors in naming. We need to make it impossible to create an invalid entry in the taxonomy like this - there are currently two kanban and Alc Kanban - that was me trying to figure out how the tool worked.
#7Script Message-Clarity Linting PassScript
·App · script·Google Docs·3 months ago
A script-review preflight tool (could live in Google Docs or as a standalone linter) that checks the VO/script against a maintained 'banned phrases / diluting qualifiers' list and a 'core message pillars' brief. It highlights any sentence that introduces a concept not in the approved message pillars, or contains flagged filler phrases, and suggests a trimmed alternative. Run before any new script draft goes to record.
#25alchemy kanban cards - add image by draggingNew feature
·App · kanban·Alchemy·3 months ago
We need an attachment area on the kanban todo creation dialogue so we can add a reference image to it. This reference image needs to be able to be used by claude or other agents, so a path to the image on disk is likely what we'll want to store.
#23alchemy kanban - i should be able to reorder items in a sprintDesign
·App · kanban·Alchemy·3 months ago
When i drag/drop a sprint item i should be able to reorder it in it's sprint box.
#24alchemy kanban - claude skill integrationNew feature
·App · kanban·Alchemy·3 months ago
I want to create a claude skill - something like "/alc_sprint that will automatically grab then next sprint and split out each task into sessions.
#15i want to be able to autogenerate a walkthrough scaffolding based on an alchemy pageNew feature
·App · walkthroughs·Alchemy·3 months ago
I should be able choose something like "Alchemy" as the application and "App * Walkthroughs" and have the system generate me a scaffolding of what it knows the steps would be as a starting point - for example with the walk through the first two steps would be obvious: 1) Click Pipeline 2) Click Walkthroughs) 3) Click Create Walkthrough (forgot the actual text of the button but you get the idea That should be able to be guessed - at minimum the first 2 should be obvious. We want to auto-populate whereever we can and the more walkthroughs we build the better we'll get at it. This is just a starting point.
#22Pin the kanban lane headers so they stay visible when scrolling a long laneNew featureTom
·App · kanban·Alchemy·3 months ago
#27I should be able to reorder Sprint "buckets" in the kanban viewDesign
·App · kanban·Alchemy·3 months ago
in the sprint swim lane i should be able to reorder sprints - in the sprint order the top one in the column will be the next "current" sprint.
#17i should be able to ctrl+click on every menu item in alchemy and open it in a new tab
·Cookbook · alchemy · menus·Alchemy·3 months ago
#12Create an Adobe After Effects PluginNew feature
·Adobe After Effects·3 months ago
Create an After Effects plugin tied to the cookbook like the premiere one, do it on a video project where we want to push motion graphics, will be better to do it when someone like ELI is around to help us. Potentially reach out to ELI to see if he wants to USE alchemy in exchange for giving us features to add.
#36I want a "Priorities" line on the Kanban Board - top 3 things I care about on any given dayNew feature
·App · kanban·Alchemy·3 months ago
It'll add a big, easy to edit section of the day's top 3 priorities. With Agentic coding we often have sprints of a morning so what's most important today could be the equivalent of what used to happen in 2-3 sprints. It means remembering what your focus is on is paramount. This is a list of strings essentially - mainly for the operator. The will be in order of priority left to right - i should be able to drag / drop to reorder them and have that order be reflected in the Workboard C:\Users\tmiko\PycharmProjects\alchemy\alc\models\work_item.py model.
#9Image–Message Alignment Review TemplateDesign
·App · image_editor·Adobe Photoshop·3 months ago
A per-slide image brief template (one row per key message moment) that requires the designer to fill in: the single idea the image must communicate, the emotional tone, and a 'distraction check' (no secondary animations, no watermarks, no text overlay conflicts). Paired with a preflight step that flags slides where the image brief field is empty or where known banned sources (public domain watermarks, specific meme formats) are detected by filename/metadata before the deck goes to review.