Skip to content

Allow devDependency imports in TypeScript spec files - #1844

Merged
BryonLewis merged 2 commits into
mainfrom
eslint-unused-directives
Aug 18, 2026
Merged

BryonLewis merged 2 commits into
mainfrom
eslint-unused-directives

Conversation

@PaulHax

@PaulHax PaulHax commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Allow devDependency imports in TypeScript spec files

The client lint command did not report unused ESLint directives. As a result, obsolete directives
could remain after the related lint errors were fixed.

The Airbnb import/no-extraneous-dependencies rule also permits test dependencies only in
JavaScript test files. TypeScript spec files needed a manual directive when they imported vitest
or @vue/test-utils.

Changes

  • Make the client lint command report unused ESLint directives.
  • Remove directives that no longer suppress an error.
  • Permit devDependency imports in **/*.spec.ts files.
  • Remove the related directives from 18 TypeScript spec files.
  • Correct two existing server lint findings: a test docstring and a redundant exception type.

This PR does not change application or test behavior. It changes the lint gate and removes obsolete
lint comments.

Hierarchy prerequisite

The hierarchy PRs add TypeScript spec files that import devDependencies, so this change lets them
pass the stricter lint gate without manual suppressions.

Stack

This is PR 1 of 9. Next: preserve Desktop import warnings.

  1. Current — Allow devDependency imports in TypeScript spec files
  2. Preserve warnings from every Desktop import file
  3. Copy source metadata when creating a single-camera soft clone
  4. Add hierarchical track classification
  5. Centralize hierarchical classification changes
  6. Replace mutable merged tracks with read-only projections
  7. Make track lifecycle operations classification-safe
  8. Add lossless DIVE KWCOCO classification support
  9. Define raw and resolved classification boundaries

This PR contains 2 commits on origin/main:

  • 455c1524 Remove stale eslint-disable directives
  • 637984a3 Allow devDependency imports in spec files

Overall: 64 files, +18/-89.

@PaulHax
PaulHax force-pushed the eslint-unused-directives branch from 637984a to f2eb233 Compare August 18, 2026 18:29
Enable --report-unused-disable-directives in the client lint script so
unnecessary suppressions fail the gate, and drop the directives that no
longer suppress anything.
Airbnb's import/no-extraneous-dependencies allowlist only covers .js and
.jsx test files, so every TypeScript spec needed a hand-written disable
comment. Scope the rule to permit devDependencies in *.spec.ts and drop
the now-redundant suppressions.
@PaulHax
PaulHax force-pushed the eslint-unused-directives branch from f2eb233 to 2563e23 Compare August 18, 2026 19:47
@BryonLewis
BryonLewis merged commit c5e72d4 into main Aug 18, 2026
3 checks passed
@BryonLewis
BryonLewis deleted the eslint-unused-directives branch August 18, 2026 20:06
romleiaj added a commit that referenced this pull request Aug 19, 2026
Client (eslint with main's new --report-unused-disable-directives, and
vue-tsc, both now clean):
- Drop import/no-extraneous-dependencies directives in spec files that
  main's #1844 made redundant, and no-await-in-loop ones that no longer
  fire.
- function-paren-newline: collapse or one-arg-per-line the offending
  calls.
- autoRegisterSelection: remove the dead empty-bin guard (bins <= usable
  guarantees every bin spans a frame) instead of using `continue`.
- AutoRegisterDialog: unnest the pairCount ternary.
- Justify the two reactive-touch `void` expressions with a scoped disable.
- ViewerLoader: remove loadMultiCamMetadata, orphaned when the in-process
  auto-register service path was dropped; guard the null multiCamMedia
  branch in populateMultiCamImagePathGetters.
- useMediaController: per-camera controllers now carry translateCameraFrame
  and cameraFrameToSlot, which the MediaController interface requires.
- common.spec: seed and type the registration fixtures at format v2.

Server (flake8/black/isort/mypy, now at parity with main):
- black/isort formatting in registration_output.py and run_pipeline.py,
  wrap an over-long message in multicam_pipeline.py, D209 docstring in
  models.py.
- _invert3 accepts Optional input so the leftToRight/rightToLeft
  fallback typechecks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012JeU2DKMZAN2d4TSymbJyz
romleiaj added a commit that referenced this pull request Aug 31, 2026
Client (eslint with main's new --report-unused-disable-directives, and
vue-tsc, both now clean):
- Drop import/no-extraneous-dependencies directives in spec files that
  main's #1844 made redundant, and no-await-in-loop ones that no longer
  fire.
- function-paren-newline: collapse or one-arg-per-line the offending
  calls.
- autoRegisterSelection: remove the dead empty-bin guard (bins <= usable
  guarantees every bin spans a frame) instead of using `continue`.
- AutoRegisterDialog: unnest the pairCount ternary.
- Justify the two reactive-touch `void` expressions with a scoped disable.
- ViewerLoader: remove loadMultiCamMetadata, orphaned when the in-process
  auto-register service path was dropped; guard the null multiCamMedia
  branch in populateMultiCamImagePathGetters.
- useMediaController: per-camera controllers now carry translateCameraFrame
  and cameraFrameToSlot, which the MediaController interface requires.
- common.spec: seed and type the registration fixtures at format v2.

Server (flake8/black/isort/mypy, now at parity with main):
- black/isort formatting in registration_output.py and run_pipeline.py,
  wrap an over-long message in multicam_pipeline.py, D209 docstring in
  models.py.
- _invert3 accepts Optional input so the leftToRight/rightToLeft
  fallback typechecks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012JeU2DKMZAN2d4TSymbJyz
BryonLewis added a commit that referenced this pull request Sep 2, 2026
* Add Auto Align (deep-feature matching) to the Camera Registration panel

New 'Auto Align (current frame)' button computes the camera-pair
alignment automatically using the MINIMA-LoFTR matcher hosted by the
VIAME interactive service (companion VIAME branch dev/auto-align), a
LoFTR fine-tuned for cross-modality matching (EO<->IR).

Flow: the panel asks the platform bridge for an alignment of the
selected pair on the current frame; desktop ViewerLoader resolves each
camera's image path (same per-camera getters stereo uses) and sends
both to the interactive service, which returns a homography plus ~24
spatially-spread inlier correspondences in native pixels. Those are
injected as ordinary point pairs (CameraRegistrationStore.
applyAutoAlignment): they appear in the correspondences table, are
hand-refinable like picked points, fit through the normal homography
path, and persist through the existing save and Export-menu downloads.
Provenance (autoAlignModel, autoAlignInlierRatio) is stamped into the
registration source.

Desktop-only, mirroring the text-query wiring end to end:
interactive.ts autoAlign -> ipcService alignment-auto-align /
alignment-available (matcher weights present in the VIAME install) ->
frontend api -> Viewer's provide/inject auto-align bridge
(dive-common/use/useAutoAlign) -> RegistrationTools. Web (and installs
without the weights) never see the button. Unmatchable scenes surface
the service's actionable error (e.g. 'try a frame with more
structure') instead of applying a bad warp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Document Auto Align feature plan and Phase 0 benchmark results

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Let Auto Align accept any multi-camera dataset, not just stereoscopic

handleAutoAlign gated on loadStereoMetadata(), which rejects anything that
isn't a stereoscopic (subType === 'stereo') dataset. A plain multicam rig --
e.g. an EO/IR pair (subType === 'multicam') -- therefore failed with
"Auto-align requires a multi-camera dataset" even though deep-feature
alignment needs only two cameras' images for the current frame, not a
calibrated stereo pair.

Factor the (stereo-agnostic) per-camera image-path-getter population out of
loadStereoMetadata into populateMultiCamImagePathGetters, and add
loadMultiCamMetadata that gates on isMultiCamDatasetMeta (stereo OR multicam).
handleAutoAlign now uses the latter. loadStereoMetadata keeps its
isStereoscopicDatasetMeta gate unchanged, so the stereo service still never
loads on a non-stereo dataset; the getter-building code is moved verbatim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Rename Auto Align to Auto Register

The button creates a camera-pair REGISTRATION -- it automates exactly
what the Camera Registration panel does by hand -- so name it with the
registration vocabulary (registration = creating the transform, Align
View = consuming it). Renames the UI strings, the store method
(applyAutoRegistration), the provide/inject bridge
(dive-common/use/useAutoRegister), the apispec types
(AutoRegisterRequest/Response), the desktop API and IPC channels
(auto-register / auto-register-available), the provenance keys stamped
into the registration source (autoRegisterModel,
autoRegisterInlierRatio), and the feature-plan doc.

The interactive service's wire name (command: 'auto_align', VIAME
branch dev/auto-align) is deliberately unchanged -- it renames in step
with the service side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Move from 'auto_align' to 'register_images' naming

* Scrub alignment language, move to register elsewhere

* Turn on point picking when Auto Register injects its matches

The injected correspondences were invisible after a successful Auto
Register unless the user separately flipped the "Pick points" toggle,
because the keypoint layer draws nothing while picking is off. Enable
picking in applyAutoRegistration so the points appear immediately for
review, matching the summary text's "Review the points" instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4dW5stcXQBj89d5Kc35bw

* Rename the "Pick points" toggle to "Edit points"

Since Auto Register and registration files can both populate the
correspondence table, the toggle's job is broader than placing new
points: it gates seeing, dragging, and deleting them too. Comments that
referenced the toggle by name follow suit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4dW5stcXQBj89d5Kc35bw

* Stop Auto Register from restamping the rig-global source

applyAutoRegistration merged its matcher provenance (model, inlier
ratio) into cameraRegistrationSource, but that stamp is rig-global and
written into EVERY per-camera registration file. Auto-registering (or
then refining) one pair therefore changed every other camera's file
body too, so saving after touching only UV points warned it would
overwrite the IR file as well, and the Source readout blended the
producer's lineage with matcher details.

Drop the stamp: the pair's divergence from a loaded producer
registration already surfaces through the refined-from-source warning,
like any in-app refit. Persisted per-pair matcher provenance is
deferred until the file format has a pair-level source field; the plan
doc now records that decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4dW5stcXQBj89d5Kc35bw

* Drop the stale "Save, then" from the refined-from-source note once saved

The divergence note is a persistent status by design (the pair really
has diverged from the producer registration, before and after saving),
but its copy always opened with "Save, then download...", which reads
as a pending to-do even right after a save. Key the action hint on the
dirty flag: while unsaved it still says save first; once saved it goes
straight to the export instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J4dW5stcXQBj89d5Kc35bw

* Remove the Auto Register feature plan doc

Its design rationale now lives in code comments and the PR description;
the status/history sections were already stale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MgbsJaWzbWbntMwAt47Wa

* Type the register_images IPC boundary

Replace the Promise<any> return and duplicated inline request shapes with
AutoRegisterRequest/AutoRegisterResponse from apispec, plus a local
interface for the service's snake_case wire payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MgbsJaWzbWbntMwAt47Wa

* Shorten the Auto Register button label so it fits the panel

The current-frame qualifier moves to a tooltip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MgbsJaWzbWbntMwAt47Wa

* Say "cameras ready" instead of "cameras registered" in rig status

The count includes the reference camera, which is ready by definition but
was never registered to anything -- a fresh 3-camera rig read "1/3
cameras registered" before any work happened. Applies to the panel
summary, the Align View tooltip, and the import menu hint, which share
the same registrationProgress semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MgbsJaWzbWbntMwAt47Wa

* Give correspondences an image-pair observation dimension (format v2)

A correspondence now belongs to a (camera pair, image pair), not just a
camera pair. The store's canonical state becomes per-pair lists of
observations -- image-pair identity (image names; frame indices resolved
from them at load time via a viewer-published resolver), an enabled flag,
per-observation producer provenance ('manual' vs a matcher id, closing
the gap applyAutoRegistration documented), optional producer stats, and
the points themselves. Fits pool points from every enabled observation
instead of trusting one frame, and new selectors (framesForPair,
correspondencesForFrame/ForCameraFrame, pairFitStats, setFrameEnabled,
clearFrame) expose the per-frame structure and per-frame reprojection
RMS against the pooled fit.

The registration file format moves to version 2: points live only in
per-observation rows (imageLeft/imageRight identity, enabled, source,
stats) -- no flattened duplicate -- and every loader rejects any other
version outright, because a pre-v2 file would otherwise load as a
matrix-only pair with its points silently dropped. All three persistence
surfaces move together: the client parsers (store + desktop backend's
fromRegistrationPairs), the server cameraCorrespondences meta schema,
and the web/desktop import, export, and seed paths.
mergeRegistrationValues now merges at observation granularity, so a
fresh pipeline result updates the image pairs it covers without
discarding hand-picked observations it doesn't.

The keypoint layer renders only the observations belonging to the image
pair each pane currently shows (each side resolved in its own camera's
frame space), fixing points floating over features that aren't there --
including for hand-picking across frames, which was broken independent
of auto-register. No migration path: v1.9.12's flat format never reached
users, and KAMERA adopts v2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcZ1d5tifDtgzx3QFbaaFf

* Rebuild the registration panel around frames; add timeline markers

The panel becomes two-tier: a Registration Frames list (one row per
observation with its point count, per-frame reprojection RMS against the
pooled fit as a colored agreement dot, an enable checkbox that excludes
a frame from the fit without deleting its points, jump-to-frame,
per-frame delete, and prev/next-frame navigation) above a correspondence
table scoped to the frame currently being viewed. The transform status
reads 'fit from N frames / M point pairs — rms X px', so a frame that
disagrees with the consensus is visible and removable instead of quietly
dragging the solution. Unresolved observations (images not in this
dataset) list with a warning row; producer-skipped candidates show their
machine-readable reason.

The Timeline work-area gains a marker row for the active pair's
registration frames -- positioned with the exact pixel mapping the
playhead uses, clickable to seek, dimmed when excluded -- rendered as
siblings of the playhead so it stays visible in all four chart modes.
ControlsContainer feeds it from the injected registration store, gated
on the Camera Registration panel being open, in the Timeline's own
selected-camera-local frame space. Opening the panel no longer
soft-collapses the bottom controls (the marker row is most useful
exactly then); the left sidebar still collapses as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcZ1d5tifDtgzx3QFbaaFf

* Update client specs for the observation model and format v2

CameraRegistrationStore.spec exercises the new spine: observation
identity stamping (image pair + manual provenance, frame:// pseudo-names
without a resolver), per-frame grouping, resolver-driven frame
resolution (names win over stale indices; unresolvable images go null,
not guessed), per-side correspondencesForCameraFrame lookups, pooled
fitting across frames, enable/disable toggles refitting without deleting
points, pairFitStats flagging a corrupted frame's disagreement,
observation-granular applyRegistrationResult merging (a matcher re-run
replaces its own prior result, manual work survives), v2 round trips
carrying enabled/source/stats, and the version guard rejecting anything
that isn't version 2.

cameraRegistrationFiles.spec covers the v2 serialization (observations
only, no flattened points, advisory frame omitted when unresolved) and
observation-granular merging. Fixtures in the seed/import/discovery
specs move to v2 bodies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcZ1d5tifDtgzx3QFbaaFf

* Run auto-registration as a multi-frame pipeline job; drop the service path

The Auto Register button becomes 'Auto Register Frames…': a dialog with
the knobs worth changing (time-bin budget, candidates per bin, min
inliers, pairs topology on a triplet, replace-vs-merge for prior matcher
results) that launches utility_align_cameras_{2,3}-cam over a stratified
candidate spread. DIVE picks for diversity and synchronization -- equal
time bins across the flight, ranked within each bin by inter-camera
timestamp skew when per-frame timestamps exist (100 ms of desync at
survey speed is ~5 m of ground motion RANSAC cannot reject) -- and the
VIAME process picks for image quality within each bin, so oversampling
then pruning keeps the flow single-pass.

Plumbing: runtimeParams.imagePairs carries the per-camera frame subset
as image names (frame://N pseudo-names for video); the desktop multicam
arg writer honors it when writing input{i}_images.txt, keeping the row
ordering contract -- and applies frameRange for multicam image lists,
fixing a pre-existing silent no-op. Video cameras get their selected
frames extracted to stills first, so the register pipes never need
vidl_ffmpeg. On completion, viame.ts sniffs *registration*.json in the
job work dir (substring match, like the calibration hook), maps
extracted-still names back to frame://N identities, and merges the
result into the dataset's registration at observation granularity via
the shared writers -- rejected candidates arrive disabled with their
skip reason, so the frame list doubles as the post-run review. The panel
polls the persisted meta for the result (no cross-platform job event),
never clobbering unsaved edits without confirmation, and shows a
client-computed triplet loop-closure readout in the rig status block.

Availability is now 'is the align pipe in the pipeline list' -- truthful
by construction since the add-on pack installs pipes and weights
together -- replacing the ckpt-presence IPC probe. The single-frame
service path is deleted end to end: useAutoRegister, the AutoRegister*
apispec types, the register-images IPC pair, and
InteractiveServiceManager.autoRegister. The VIAME stdio backend itself
stays (it shares alignment_core).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcZ1d5tifDtgzx3QFbaaFf

* Web-girder parity for the auto-register pipeline

build_multicam_kwiver_settings honors runtimeParams.imagePairs: when a
camera has a registration frame subset, only those images are written to
its input list, in order, preserving the row-pairing contract
(image-sequence cameras; a video subset raises a clear error rather than
silently processing the whole clip). run_pipeline pushes the rig's
camera names to the register process, and on completion uploads the
registration JSON to the dataset folder for provenance and merges it
into the dataset's saved registration meta server-side --
registration_output.py mirrors the client's observation-granularity
merge (same-identity observations replaced, hand-picked ones kept,
matrix-only pairs replacing wholly) and PATCHes the allowlisted meta
fields, so the panel's poll-for-result picks it up exactly like on
desktop. The client web runPipeline path passes runtimeParams through
unchanged; no new endpoint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcZ1d5tifDtgzx3QFbaaFf

* Update backend spec fixtures to registration format v2

fromRegistrationPairs, the per-camera file round trip, the multicam
transform seed, and the discovery fixtures all move to v2 bodies
(observations with image-pair identity instead of flat points).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PcZ1d5tifDtgzx3QFbaaFf

* Add cross-camera frame translation to the aggregate controller

Cameras drop frames independently, so their local index spaces
diverge: a frame number produced against one camera does not name the
same capture on another. Anything that reads such a number and then
renders or seeks it in a different camera's space is off by however
many frames the two have dropped between them.

Expose the two translations the aligned timeline already supports:
translateCameraFrame maps one camera's local frame to another's for
the same capture, and cameraFrameToSlot maps it to the rig-wide slot
the frame readout and scrubber count in. Both return undefined when
the capture has no frame there, so callers can omit rather than draw
somewhere wrong, and both pass through when alignment is not active
and the spaces coincide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Draw registration timeline markers in the selected camera's space

Observation frames are camA-local, but the Timeline draws in the
SELECTED camera's local frame space. Those two spaces only coincide
when the rig drops no frames, or when camA happens to be the selected
camera: a rig whose cameras drop independently accumulates an offset,
putting every marker a frame or two off the capture it describes.

Translate each marker through the aligned timeline, and drop the ones
whose capture has no frame on the selected camera -- there is no
honest place to draw those.

The spec's fixture is the real KAMERA fl09 center_view shape: 277
captures at 1s with RGB missing three of them and IR/UV missing three
different ones, so each modality has 274 frames over a timeline that
disagrees with its neighbours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Propose auto-register candidates from aligned timeline slots

The proposal picked a frame index and reused it for every camera, so
on a rig whose cameras drop frames independently the matcher was
handed images from different instants and asked to register them. On
the KAMERA fl09 fixture -- 277 captures with RGB and IR/UV each
missing three different ones -- that mispairs from the first drop
onward, and nothing downstream can recover it.

Take the candidates from the aligned timeline instead, so "frame i"
means capture i across the whole rig, and translate each chosen
capture into every camera's own local index before resolving paths.
Only slots holding a frame from every camera are eligible: the matcher
needs the whole set and the pipeline reads image lists off disk, so a
gap has no path to send. Gaps stay in the dataset and stay visible in
the viewer, they are simply not candidates. Skew ranking reads its
timestamps through the same slots. Datasets with no aligned timeline
keep the old positional behaviour, which is all they can support.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Re-baseline the registration store after a replace-mode run

"Replace existing" drops the prior matcher observations so the fresh
run replaces rather than merges. Those removals are the run's own
bookkeeping, but they left the store reading dirty, so the completion
path stopped to ask whether to discard unsaved edits -- on every
single replace-mode run, about changes the user never made.

Mark the store saved again when it was clean before the run, so only
genuine hand edits still read dirty. A store that was already dirty
stays dirty: those really are the user's. This is the same rule frame
resolution follows in CameraRegistrationStore.setFrameResolver.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Say the run is waiting on confirmation, not still matching

The post-run confirm is modal and can sit unanswered indefinitely,
while the panel went on claiming the job was matching frames. Set the
status to what is actually true before awaiting the answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Seek and label registration frames in the right camera space

Two ends of the same mistake. Observation frames are camA-local, but
handler.seekFrame() interprets its argument in the SELECTED camera's
local space, so jumping to a registration frame from the panel landed
on the wrong capture whenever the rig's cameras drop frames
independently -- and the frame numbers printed in the list and in the
remove-confirmation disagreed with the frame readout and scrubber,
which count global slots, by the same drift.

Seek through the aggregate controller's seekCameraFrame with camA
named explicitly, so the translation happens in the one place that
knows the aligned timeline, and carry a separate displayFrame on each
row for the number shown. camA-local stays the identity every panel
action works in; only the presentation moves to slot space.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Measure loop closure over real frames, judged relative to width

The triplet-consistency readout was wrong at both ends. It sampled a
nominal 1000x1000 grid, which on a 12768x9564 EO frame covers a corner
and misses exactly the edge divergence that matters, and it compared
the result against a fixed 5 px. The residual lands in the last
camera's native pixels, so that fixed threshold silently means
something different per rig: the same rig error reads ~7.6x larger
against a 4864-wide UV camera than a 640-wide IR one, flagging a rig
inconsistent for nothing more than having a large last camera.

Sample over camera 1's actual frame and judge the residual as a
fraction of camera 3's width. 0.001 preserves what 5 px happened to
mean on a ~5000px camera; on a KAMERA calibration flight a converged
10-100 frame fit sits near 0.05% and a single-frame overfit rig at
0.15%. Both readouts now show the percentage alongside the pixels.

Report nothing while either annotator is still coming up: originalBounds
starts as a 1x1 placeholder, and read as a real size it collapses the
sample grid onto one corner pixel and yields a confident-looking wrong
verdict. Take the camera order from CameraStore.displayOrder too --
which camera is first and last is the direction this is measured in,
and camMap's insertion order does not reliably say.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Split the registration frame list into Auto and Manual sections

One flat list mixed matched frames with hand-picked ones and repeated
the producer name on every row, which is the least useful place to put
it: the distinction that matters is per-section, not per-row. Group
the rows under Auto and Manual headers, each carrying its own
enabled/total count and mean agreement with the pooled fit, and drop
the per-row source chip the header now says better.

Give each section a clear-all, since removing a matcher run one row at
a time was the only way out of a bad run. It removes every observation
of that section INCLUDING the skipped candidates the list hides: those
are still stored and still travel into the saved registration file, so
leaving them would leave invisible cruft the user cannot see or
remove. The confirmation says how many of those there are.

Skipped rows stay counted rather than listed -- overwhelmingly they
are "pruned", the oversampling remainder from proposing
candidatesPerBin per bin and keeping the best. They carry no points
and support no action.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Let the user queue specific captures for the matcher

The only way to run the matcher was the stratified proposal, which
spreads candidates across the whole sequence. That is the right
default, but it gives no way to say "these frames" -- the ones with
good overlap, or the stretch where the fit is visibly weak.

Queue captures from the panel and run the matcher on exactly those.
The queue is kept in global aligned-timeline slots, the same space the
frame readout shows, so what the user queues is what they were
looking at. Slots translate to each camera's local index at launch,
same as the proposal; captures a camera is missing are dropped,
because the pipeline reads image lists off disk and a gap has no path
to send, and a queue where none survive errors rather than silently
launching an empty job.

max_frames goes out as one bin per queued frame. It is the pipeline's
bin budget -- it keeps the best candidate per bin and prunes the rest
-- so leaving it at the proposal's budget would prune away the very
frames the user asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Say what frame count to pick, and warn where it overfits

The frame-count field offered no guidance, and the failure mode at the
low end is the kind that hides itself: one frame fits its own points
perfectly -- 100% inliers, near-zero RMS -- while producing the least
consistent rig of any setting. The quality readouts look their best
exactly when they are least trustworthy.

Add a hint for the useful range (the pooled fit converges by about 10
frames; beyond that extra frames mainly tighten triplet consistency,
at roughly linear cost) and warn below 5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8CVLjQDzaBGbCSKL7FiF

* Fix lint and typecheck failures after rebasing onto main

Client (eslint with main's new --report-unused-disable-directives, and
vue-tsc, both now clean):
- Drop import/no-extraneous-dependencies directives in spec files that
  main's #1844 made redundant, and no-await-in-loop ones that no longer
  fire.
- function-paren-newline: collapse or one-arg-per-line the offending
  calls.
- autoRegisterSelection: remove the dead empty-bin guard (bins <= usable
  guarantees every bin spans a frame) instead of using `continue`.
- AutoRegisterDialog: unnest the pairCount ternary.
- Justify the two reactive-touch `void` expressions with a scoped disable.
- ViewerLoader: remove loadMultiCamMetadata, orphaned when the in-process
  auto-register service path was dropped; guard the null multiCamMedia
  branch in populateMultiCamImagePathGetters.
- useMediaController: per-camera controllers now carry translateCameraFrame
  and cameraFrameToSlot, which the MediaController interface requires.
- common.spec: seed and type the registration fixtures at format v2.

Server (flake8/black/isort/mypy, now at parity with main):
- black/isort formatting in registration_output.py and run_pipeline.py,
  wrap an over-long message in multicam_pipeline.py, D209 docstring in
  models.py.
- _invert3 accepts Optional input so the leftToRight/rightToLeft
  fallback typechecks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012JeU2DKMZAN2d4TSymbJyz

* Pass -update 1 when ffmpeg extracts a single registration frame

The image2 muxer refuses a digit-bearing output filename without
-update, reading it as an ambiguous sequence pattern instead of one
literal file -- reproduced with the bundled ffmpeg (n6.1.2) on an
extracted <camera>.frame_<N>.png destination.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Check ffmpeg exit code, not stderr content, for extraction failure

result.error is accumulated stderr, which ffmpeg always writes (its own
progress/banner logging) even on a successful run -- the prior check
threw on every extraction despite a valid output file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Only mark timeline frames that carry registration points

A producer records the candidates it considered and discarded as well:
auto-register proposes more frames than it matches, then prunes the
weakest, writing those out with no points and enabled=false. Those were
drawing dimmed markers on the scrubber with nothing behind them and
nothing to toggle. The frame list still lists them with their skip
reason, which is the better place for that detail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Dev/auto register fixes (#1860)

* Hide the sidebar on registration open regardless of which side it is on

The collapse only fired for the left sidebar, so opening Camera
Registration with the sidebar in bottom mode left it up, competing for
the vertical room the two picking panes need. Collapse it either way;
the prior mode is still restored on close and the toggle still works
while registering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Don't offer the sidebar toggle while the registration panel is open

The sidebar isn't useful during picking and the bottom layout in
particular doesn't lay the picking panes out usably. The panel already
collapses the sidebar on open and restores it on close; hide the toggle
in between so it can't be turned back on there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Shorten Overlay Warp toggle labels to L/R direction arrows

Full camera names overflowed the button toggle; the full pair names
move to the buttons' native tooltips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Show L/R instead of full camera names in the point-picking cursor readout

Long camera names overflowed the monospace readout the same way they
did the Overlay Warp toggle; same L/R shorthand, same active-pair
cameras.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Truncate the per-camera registration export button labels

Long camera names overflowed the block button; the full label
(camera -> destination) is now an ellipsis-truncated line backed by
the button's native title tooltip on hover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Bump imageRevision on video seek so the aligned-view warp redraws

VideoAnnotator only bumped imageRevision once, right after
loadedmetadata; every later scrub left the aligned-view canvas warp
showing whatever the <video> element displayed mid-seek (often black)
because nothing told it to redraw once the seek actually landed. The
native (unwarped) pane never showed this since the browser keeps a
<video> element live on its own -- only the canvas snapshot needed
the signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Stop the aligned-view warp flickering on video scrub

The frame-number watch repainted immediately on every seek to blank a
per-slot gap -- a case video panes never hit (VideoAnnotator's seek()
treats it as unreachable by construction) -- and at that instant the
browser hasn't decoded the target frame yet, so the repaint only ever
caught the stale pre-seek frame. That stale paint, followed moments
later by the correct one once 'seeked' bumps imageRevision, read as a
flicker on every scrub step. Video panes now skip the immediate
repaint and rely solely on the imageRevision-triggered one; image
panes are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Truncate the registration export button labels on web too

Same unbounded label as the desktop export menu, same fix: ellipsis the
line and keep the full camera/destination in the button's title tooltip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Escape the observation-identity NUL instead of embedding it raw

Two literal NUL bytes sat in the template that builds an observation's
identity key, which made the whole source file register as binary: grep
skipped it silently unless given -a, and diff tooling degrades on it.

The escape produces the same string, so the key is unchanged. NUL is still
the right separator -- it is the one character a filename cannot contain,
so no combination of image names can collide across a delimiter -- and a
comment now says to keep writing it as an escape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9imLktmzCkAR3wmUybaEs

* Report a pipeline job from the moment it is accepted

A multi-camera registration run extracts its frame subset to stills before
spawning anything, which for a rig-wide job takes longer than the pipeline
itself. Nothing existed to report during that window: the job was created
after the spawn, keyed by the pid, so the Jobs tab stayed empty and the run
looked like it had not started.

Key the job by its work dir instead (already unique per run, and known before
any process is), and emit it as soon as that directory exists. Frame extraction
reports progress into the same entry, so the log fills in per camera while it
works, and a job that dies before spawning is closed out rather than left
running forever behind a spinning badge.

Post-run collection failing was swallowed to the main-process console, leaving
a job that reported success while its results never reached the dataset. Append
it to the job log and push it to the Jobs tab.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MwkzYmpLsfA46DArH3poyc

* Keep the Auto Register run visible in the registration panel

The only running indicator was `:loading` on two buttons, and the main one
renders only while no auto-registered frames exist. After a first successful
run there is a frame list, so the button is gone and a second run showed
nothing at all -- and leaving the Camera Registration tab and coming back left
the panel looking idle while the job was still going.

Put the state in the section instead, driven by the job service's own
`running` (which outlives this component), so it survives a tab switch
regardless of what the frame list holds. A linear bar also cannot overflow the
way a 14px circular spinner stuffed into an x-small button does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MwkzYmpLsfA46DArH3poyc

* End the Auto Register wait on job state, not on its output

Completion was inferred from the dataset's correspondences differing from a
pre-launch baseline. The matcher is deterministic, so re-running it over the
same frames merges byte-identical observations and that difference never
appears: the panel sat on "job running" until the 30-minute timeout. A job that
failed wrote no output at all and looked exactly the same.

Add an optional `watchPipelineJob` to the API and implement it on desktop
against the job store, matching a job for this dataset and pipe that started at
or after the call so an earlier run still in the history is not mistaken for
this one. The service now waits on the job and then reads the dataset once:
results changed adopts them as before, results identical says so, and a failed
job reports its exit code and points at the log. Platforms with no job feed
keep the artifact poll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MwkzYmpLsfA46DArH3poyc

* it was attmepting to use the source directory with project directory endpoints for images when aligning, this fixes that issue

* use runtimeParams.imageParis in writeMultiCamStereoPipelineArgs

* properly fail on post-run failures for desktop jobs

* updating client tests

* use same ordering mechanism for camera Names as web on desktop

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Bryon Lewis <Bryon.Lewis@kitware.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants