• v1.5.0 5f7703dcf6

    jkaindl released this 2026-05-27 12:02:49 +00:00 | 53 commits to main since this release

    Brush trains for --total-steps (default 30 000) regardless of whether the splat already converged. v1.5.0 adds an opt-in patience-stop: hold out ~10 % of frames, monitor PSNR while training, and SIGTERM Brush when the curve flattens. On a typical converging capture this can save 30-50 % of the Brush stage.

    Added

    • PlateauMonitor in train.py — pure-logic class that polls <output_dir>/eval_<step>/ directories, computes mean PSNR via compute_eval_psnr, maintains the (step, psnr) history, and decides should_stop once the last patience consecutive Δ-PSNR values are all below min_delta_psnr and the last step is ≥ min_steps. Idempotent; missing/in-progress eval dirs are handled gracefully.
    • compute_eval_psnr(eval_dir, frames_dir) — cv2-based mean PSNR across rendered eval images vs originals. Pairs by filename stem so Brush's <orig>.png lines up with our frames/<orig>.jpg. Originals get downscaled (cv2.INTER_AREA) to the render resolution before MSE.
    • Six new [brush] config fields: plateau_enabled (bool, default false), plateau_eval_split_every (2-50, default 10), plateau_eval_every (100-10000, default 1000), plateau_min_steps (default 5000), plateau_patience (1-20, default 3), plateau_min_delta_psnr (0 < x ≤ 5, default 0.05). Cross-field validator rejects plateau_min_steps > max_steps only when the feature is enabled (so a CI-config lowering max_steps doesn't have to touch plateau fields).
    • build_brush_command extension: when plateau_enabled, appends --eval-split-every, --eval-every, --eval-save-to-disk, and ties --export-every to plateau_eval_every so every eval checkpoint has a fresh PLY for the SIGTERM-safe stop mechanism.
    • run_brush orchestration: spawns a daemon PlateauMonitor thread, polls every 5 s, sends proc.terminate() on should_stop. The non-zero returncode from our own SIGTERM is recognised and not re-raised as CalledProcessError. Edge case: SIGTERM before any export → RuntimeError with an actionable hint.

    Discovery (in spec doc)

    A 250-step Brush probe against max_strasse/brush_dataset confirmed three load-bearing assumptions, all documented in docs/superpowers/specs/2026-05-27-v15-train-till-plateau-design.md:

    1. Brush emits no stdout/stderr in subprocess mode (TUI suppressed) — stdout-PSNR parsing is not viable, hence the filesystem-watch approach.
    2. --eval-save-to-disk is reliable and writes eval_<step>/<orig_filename>.png.
    3. --eval-split-every N is deterministic — every Nth frame, sorted by filename.

    Tests

    • 13 new unit tests (5 in test_config.py, 6 in test_train.py for PSNR helper, 6 in test_train.py for PlateauMonitor, 2 in test_train.py for build_brush_command branches). All purely synthetic — no Brush invocation, no real-world frames.
    • 347 tests passing (up from 328 at v1.4.6), ruff clean, mypy clean on train.py.

    UX

    In the default config (plateau_enabled = false), v1.5.0 changes nothing. Enable via your user config:

    [brush]
    plateau_enabled = true
    # defaults are fine; tune patience / min_delta_psnr if you want aggressive stops
    

    When triggered, pipeline.log will contain a train.plateau_detected event with the full history of train.eval events leading up to the stop. The final scene.ply is whichever export was written most recently before SIGTERM (one per plateau_eval_every steps).

    Default-on candidacy

    v1.5.0 ships opt-in. After real-world validation on 2-3 captures, a follow-up release will likely flip the default to true.

    Downloads
  • v1.4.6 83ea5583ea

    jkaindl released this 2026-05-27 11:35:09 +00:00 | 60 commits to main since this release

    Closes the v1.4 line: small coverage gaps + early-warning UX + repo hygiene, so v1.5 starts from a clean baseline.

    Added

    • Pre-flight viewer-config check (cli._warn_if_viewer_misconfigured). When [viewer] auto_open=true + target="supersplat-local" but the dist isn't built, the warning fires at the start of process / resume / add-video / rescue — not after the ~5 h run finishes and the auto-open silently falls back. Doctor still catches the same case independently. 4 unit tests.
    • Test coverage for the v1.4.5 deprecation warning at target="supersplat" (remote). Verifies the viewer.remote_supersplat_deprecated logger.warning fires for the remote path and stays silent for the local one. 2 new tests in test_viewer.py.

    Removed

    • Orphaned v1.3 hero assetsdocs/assets/max_strasse_hero.{gif,mp4,webm,jpg} (≈9 MB). The README has pointed at max_strasse_autobisect_hero.* since v1.4.4; the v1.3 originals are still reachable via the v1.3.0 git tag for historical purposes.

    Fixed

    • v1.4.0 Codeberg release-page body PATCH'd to remove the stale "no real-world smoke against a non-trivial structurally-failing capture" line — refuted by the v1.4.4 max_strasse end-to-end success (490/493 cams, 5 h 36 min).

    Tests

    • 328 tests passing (up from 322 at v1.4.5), ruff clean, mypy clean on the four core modules.
    Downloads
  • v1.4.5 4841ff70fb

    jkaindl released this 2026-05-27 09:13:11 +00:00 | 61 commits to main since this release

    Follow-up to the v1.4.0–v1.4.4 burst — non-feature improvements that accumulated during the v1.4 work: code hygiene, observability, disk reclaim, and a docs refresh that catches up with the local-viewer default.

    Added

    • autosplat cleanup-rescue <capture_dir> — reclaim the per-probe rescue/probes/<clip_id>/ workspaces (typically ~1-3 GB per successful rescue). --keep-clips/--remove-clips (default keep, so resume/add-video keep working), --dry-run to preview. 4 CliRunner tests.
    • train.heartbeat events in pipeline.log every 5 min during Brush training. The ~1-2 h Brush stage previously emitted nothing between train.brush.start and train.done; non-TTY runs (watch daemon, WebUI, ssh) now have a visible pulse. Pure helper _make_train_heartbeat_emitter() so the throttling is unit-tested without a real Brush run (3 tests).
    • Deprecation warning when [viewer] target = "supersplat" (remote). The remote editor is blocked by browsers' Mixed-Content policy (HTTPS page can't fetch HTTP localhost PLY) — the warning points users at the v1.4.4 supersplat-local default.

    Changed

    • cli.serve --with-supersplat shares _serve_local_and_block with the auto-open path. The helper grew an open_browser=False kwarg so --no-open-browser still works.

    Fixed

    • All remaining mypy strict noise in watcher.py and viewer.py — 18 errors in watcher (untyped dict, bytes|str event paths, Observer not-a-type) + 2 in viewer (_Handler.__init__/log_message annotations). The Observer issue fixed by typing as watchdog.observers.api.BaseObserver; the bytes/str by a tiny _as_str() helper. mypy src/autosplat/viewer.py src/autosplat/watcher.py is now clean.

    Docs

    • GETTING-STARTED.md Section "Look at the result" rewritten to reflect the v1.4.4 local-viewer default (auto-open at 127.0.0.1:3000, blocking server with Ctrl-C, setup_supersplat.sh as required first-time step).
    • CONFIGURATION.md [viewer] section expanded — all five keys listed, the auto-open lifecycle described step-by-step, daemon/WebUI exception noted.
    • CAPTURE-GUIDE.md gains a "Reclaiming disk after a successful rescue" subsection pointing at the new cleanup-rescue command.
    • ARCHITECTURE.md gains a "Documentation convention" subsection: v1.4+ uses docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md instead of post-hoc PHASE-N-*.md reports. Reasons recorded.

    Tests

    • 322 tests passing (up from 315 at v1.4.4), ruff clean, mypy clean on the two modules touched.
    Downloads
  • v1.4.4 96bd7d4a6d

    jkaindl released this 2026-05-27 07:32:33 +00:00 | 62 commits to main since this release

    After the v1.4.2/v1.4.3 viewer hotfixes, the default target="supersplat" (remote editor at playcanvas.com) still produced an empty editor for users on modern browsers: HTTPS pages cannot fetch HTTP localhost resources (Mixed-Content blocking). The user could work around it with --with-supersplat; v1.4.4 just makes that the default.

    Changed

    • [viewer] target default flipped to "supersplat-local". When the dist is built (bash scripts/setup_supersplat.sh — already part of repo setup), autosplat process / rescue finish by starting both a local SuperSplat HTTP server and a local PLY server, opening http://127.0.0.1:3000?load=http://127.0.0.1:8765/scene.ply in the browser, and blocking until Ctrl-C. Everything is HTTP-on-localhost — no mixed-content blocking, no .ply download dialog, no manual drag-and-drop.
    • Graceful fallback when the dist is missing. If target="supersplat-local" but target/supersplat/dist/index.html doesn't exist, the viewer prints a clear console hint to run the setup script and falls back to a no-op (the existing autosplat doctor warning already covers this).

    Added

    • viewer._serve_local_and_block() — shared helper that wraps serve_supersplat_local() + browser-open + signal-handled block. Re-used by the auto-open path; cli.serve --with-supersplat keeps its inline equivalent for now.

    Tests

    • test_open_in_viewer_supersplat_local_no_browser rewritten to assert the dist-missing fallback (no browser, hint message).
    • New test_open_in_viewer_supersplat_local_opens_local_servers_when_dist_present verifies the dist-present path: fake dist with index.html, both servers start, browser receives a http://127.0.0.1:*?load=http://127.0.0.1:*/scene.ply URL, stop_event keeps the test from blocking.
    • test_load_default_config_parses_cleanly updated to expect the new default target.
    • 315 tests passing.

    Real-world validation

    First end-to-end smoke against max_strasse.MP4 — the same 5:35 drone pass that v1.2.0 left at 5/244 cameras and that v1.3.0 only rescued after a manual 4-clip cut. autosplat rescue max_strasse.MP4 produced a 2.0 GB scene.ply with 490/493 cameras registered (99.4 %) in 5 h 36 min on M5, fully automatic. Pipeline trail in the logs:

    bisection.start            duration_s=335.6
    bisection.probe  clip=0    cameras_registered=120  passed=true
    bisection.probe  clip=1    cameras_registered=115  passed=true
    bisection.combine_start    leaves=[0, 1]
    pipeline.adaptive_retry    reason="low_camera_ratio: 0.03 < 0.5"  matcher=sequential
    sfm.done                   cams=490  points=368582
    quality_gate.passed        ratio=0.9939  matcher=exhaustive
    train.done                 duration_s=8398
    pipeline.done              duration_s=20168
    

    Fly-through: YouTube. Updated hero asset in docs/assets/max_strasse_autobisect_hero.*.

    Downloads
  • v1.4.3 869fbbbede

    jkaindl released this 2026-05-27 06:44:11 +00:00 | 63 commits to main since this release

    Follow-up to v1.4.2 — the auto-open path was fixed for process / rescue, but autosplat serve (without --with-supersplat) still opened the raw http://127.0.0.1:8765/scene.ply URL in the browser. Browsers have no MIME handler for .ply, so the result was a download prompt instead of a rendered splat.

    Fixed

    • autosplat serve (no --with-supersplat) opens the remote SuperSplat editor, not the raw PLY URL. New _remote_supersplat_url_for(ply_url) helper wraps the local PLY URL in https://playcanvas.com/supersplat/editor?load=<encoded-ply-url> — the same pattern viewer.open_in_viewer uses for target="supersplat". The local PLY server keeps running so the remote editor can fetch the file. --no-open-browser still suppresses the browser launch for headless / CI use.

    Tests

    • 2 new tests in test_cli_serve.py cover URL construction (default port, high port). End-to-end CliRunner tests of the serve loop are intentionally avoided because they involve threading + signals + sockets and add fragility for no extra coverage — the helper is pure.
    • 314 tests passing.
    Downloads
  • v1.4.2 418b4a75e8

    jkaindl released this 2026-05-27 06:25:24 +00:00 | 64 commits to main since this release

    Pre-v1.4.2, autosplat process opened SuperSplat in the browser with ?load=http://127.0.0.1:8765/scene.ply but never actually started a server on port 8765 — SuperSplat tried to fetch, silently failed, and the editor opened blank. Drag-and-drop the file manually was the only workaround. The pattern existed since the earliest CLI but was first surfaced by the real-world v1.4.0 max_strasse rescue run (5 h 36 min total, 490/493 cams = 99.4 %, then SuperSplat opened empty).

    Fixed

    • viewer.open_in_viewer now actually serves the PLY. For remote targets (supersplat / playcanvas) the function wraps serve_directory() around the browser-open and blocks on a threading.Event that a SIGINT/SIGTERM handler sets — so the local HTTP server stays up as long as the user wants to look at the splat, then shuts down cleanly when they press Ctrl-C. Console prints the viewer URL + PLY URL upfront so the user knows what's running.

    Changed

    • pipeline.run_pipeline no longer calls the viewer. Blocking inside the pipeline orchestrator would stall the watch-folder daemon between captures and prevent the WebUI's JobRunner from marking jobs done. CLI commands (process, resume, add-video, rescue) now invoke a new _open_viewer_if_configured() helper after their Done summary, so the user sees the result first and then learns the server URL. Daemon and WebUI already had the same broken viewer path — removing it there is a no-op for users (it was never opening anything anyway).

    Tests

    • 2 new end-to-end viewer tests: one verifies serve_directory is actually called inside open_in_viewer; the other fetches the PLY over HTTP from the running server to prove the bytes are reachable.
    • test_cli_rescue gets test_rescue_invokes_viewer_after_done verifying the wiring at the CLI layer.
    • Removed three obsolete patch("autosplat.pipeline.viewer_mod.open_in_viewer") in test_pipeline (the attribute no longer exists).
    • 311 tests passing, ruff clean, mypy clean on viewer.py + pipeline.py.

    UX note

    autosplat process video.mp4 now blocks at the very end with a console message:

    Viewer: https://playcanvas.com/supersplat/editor?load=…
    Serving PLY at http://127.0.0.1:8765/scene.ply. Press Ctrl-C when finished to stop the local server.
    

    To opt out — e.g. for CI runs or when SuperSplat is started manually — set [viewer] auto_open = false or [viewer] target = "none" in the config.

    Downloads
  • v1.4.1 6fe180b081

    jkaindl released this 2026-05-26 15:36:26 +00:00 | 66 commits to main since this release

    Follow-up to v1.4.0 — a probe-performance cap, a manual rescue CLI command, per-clip progress visibility in the WebUI, and an optional smart-split at the motion peak. Plus the pre-existing dict | None mypy noise in pipeline.py is finally cleared.

    Added

    • autosplat rescue <video | capture_dir> — manual trigger for the bisection path. Bypasses sequential/exhaustive matching when you already know a video is structurally hostile. Two modes (fresh-video / existing-capture); multi-video captures need an explicit --video because bisection is single-video only. 5 CliRunner tests cover both modes plus the rejection branches.
    • Probe-stage target_frames cap — new [retry] bisect_probe_target_frames config (default 120, range 30-1000). Exhaustive matcher cost scales as n²/2, so the override cuts probe-stage compute by ~4× compared to the pipeline-wide default of 250. Applied automatically inside probe_clip alongside the existing matcher=exhaustive override.
    • WebUI per-clip bisection progressWatcherState.InProgress.detail (optional, backwards-compat), update_stage(stage, detail) API, end-to-end propagation through webui.state.list_captures and the dashboard's active-job line: "Active job · bisect · probing clip 0_1 (depth 2/3)". Stage transitions reset the detail; legacy state.json without the field still loads cleanly.
    • Smart-split at motion peak — opt-in [retry] bisect_smart_split (default false). When enabled, find_motion_peak samples 30 frames per cut range, runs dense Farneback optical flow (downsampled to 320 px wide for speed), and places the cut at the strongest motion event instead of at the midpoint. Output is clamped to [min_clip_s, duration - min_clip_s] so a peak near an edge can't produce a sub-min sub-clip. Falls back to midpoint cleanly when OpenCV can't open the file or the motion signal is too flat (<10 % peak-to-trough ratio).

    Fixed

    • bisect_recursively per-branch ffmpeg resilience (from the late-v1.4 fix sweep)cut_video raising subprocess.CalledProcessError on a corrupt sub-range is now caught, logged as bisection.cut_aborted_branch, and treated as a failed probe; the sibling branch still runs.
    • read_source_video_from_log now returns the most recent pipeline.start (from the late-v1.4 fix sweep) — after bisection appends a fresh pipeline.start with the leaf-clip list, autosplat resume and autosplat add-video on a bisected capture see the current state instead of silently re-feeding the original failed input.
    • run_pipeline / run_pipeline_with_adaptive_retry dict type-args — pre-existing mypy strict findings (lines 143 and 493, both dict | None) finally typed as dict[str, Any] | None. mypy src/autosplat/{pipeline,bisection}.py is now clean.

    Tests

    • 22 new tests across test_bisection.py (smart-split + probe-perf + per-clip-state), test_pipeline.py (multi-pipeline.start reader), test_watcher.py (InProgress.detail), test_cli_rescue.py (the new command), and tests/webui/test_captures.py (detail propagation). Total 310 unit tests passing (uv run pytest -q), up from 291 in v1.4.0.

    Docs

    • README status line, mermaid, and release table updated to reflect v1.4.1.
    • CAPTURE-GUIDE, CONFIGURATION, TROUBLESHOOTING already covered the bisection path; nothing new needed there for this point release.
    Downloads
  • v1.4.0 8eba91ea18

    jkaindl released this 2026-05-26 15:20:25 +00:00 | 72 commits to main since this release

    When sequential → exhaustive adaptive-retry exhausts itself (retry_hint=None), the pipeline now automatically binary-subdivides the source video, probes each leaf clip with a cheap preprocess+SfM-only run, and combines the surviving leaves through the existing multi-video path. Automates the manual 4-clip workflow that rescued max_strasse in v1.3.0 — no new command, no new flag, just a longer run when the matcher swap isn't enough.

    Added

    • Auto-bisection-rescue — third escalation in run_pipeline_with_adaptive_retry, gated on retry_hint=None, single-video input, and cfg.retry.bisect_enabled (default true).
    • New module src/autosplat/bisection.pyBisectionClip frozen dataclass, build_ffmpeg_cut_command + cut_video (stream-copy, no re-encode), probe_clip (forces colmap.matcher=exhaustive because sequential is unreliable on shorts), bisect_recursively (DFS halt-on-success-per-branch), and rescue_via_bisection orchestrator.
    • Three new [retry] config fields with conservative defaults: bisect_enabled=true, bisect_min_clip_s=60.0 (range 10–600), bisect_max_depth=3 (range 1–6 — depth 3 = ≤ 8 leaves). Disable bisect_enabled for fast-fail in CI.
    • Bisection artefacts persist under <capture_dir>/rescue/clips/<stem>_part_<clip_id>.mp4 (physical sub-clips) and <capture_dir>/rescue/probes/<clip_id>/{frames,colmap} (per-clip SfM workspace) so a partial run is debuggable. clip_id is depth-encoded (0, 0_1, 0_1_0) — leading digit is the first/second half, deeper underscores trace each recursion step.
    • 20 new tests in tests/test_bisection.py + 4 in tests/test_pipeline.py. Two opt-in real-binary integration tests sit behind AUTOSPLAT_BISECTION_E2E=1 plus the existing needs_ffmpeg / needs_colmap markers. Total 293 unit tests passing (uv run pytest -q, up from 265 in v1.3.0).
    • Docs: CAPTURE-GUIDE.md gains an "Auto-bisection internals (v1.4+)" section with the rescue/ layout and clip_id semantics; CONFIGURATION.md [retry] table grows three rows; TROUBLESHOOTING.md replaces the old "if exhaustive also fails, you're stuck" branch with the actual escalation path and structured log events to watch for.

    Fixed

    • bisect_recursively per-branch ffmpeg resiliencecut_video raising subprocess.CalledProcessError on a corrupt sub-range (broken keyframe, container quirk) is now caught, logged as bisection.cut_aborted_branch, and treated as a failed probe; the sibling branch still runs. Previously the whole rescue would abort with a raw CalledProcessError.
    • read_source_video_from_log now returns the most recent pipeline.start — after bisection appends a fresh pipeline.start with the leaf-clip list, autosplat resume and autosplat add-video on a bisected capture see the current state (the leaves) instead of silently re-feeding the original failed input. Pre-v1.4 captures with only one start event behave identically.

    Design Notes

    • Why a separate module? pipeline.py was already 600 LOC; bisection's ~330 LOC of recursion + ffmpeg-cut + probe logic gets isolated in bisection.py and exposes only the orchestrator to pipeline.py. Each unit is testable without the others (the recursion is monkeypatch-friendly via the _probe_fn injection point).
    • Why no WebUI per-clip progress in v1.4? Bisection runs inside the existing sfm stage from the state-machine's perspective; events surface via structured logs only. A WebUI bisect_probe stage with per-clip progress is a v1.4.1 candidate.
    • Why no standalone autosplat rescue command? The user-chosen trigger is the auto-escalation path — a normal autosplat process … just keeps running. A standalone command can be added in v1.4.1 if the auto-path proves clumsy.
    • Why no smart-split? Midpoint binary cuts are the v1.4 strategy. Smart-split at motion-change (OpenCV optical flow) is a v1.4.1 candidate and would only help when the rotation event is concentrated rather than smeared across the clip.

    Follow-ups (resolved post-v1.4.0)

    • Probe-stage compute cost (250 frames × exhaustive matcher)resolved in v1.4.1 via [retry] bisect_probe_target_frames=120 (~4× faster probes).
    • No real-world smoke against a non-trivial structurally-failing captureresolved in v1.4.4 with the max_strasse.MP4 end-to-end run: 490/493 cameras (99.4 %), 2.0 GB scene.ply, 5 h 36 min on M5. Fly-through video.
    Downloads
  • v1.3.0 c338fa7695

    jkaindl released this 2026-05-24 16:22:03 +00:00 | 89 commits to main since this release

    v1.3.0 — Multi-Video Rescue

    When a single drone pass can't be reconstructed (180° turn, 360° spin,
    rotation-heavy footage), v1.2.0 told you to re-shoot. v1.3.0 lets you
    combine multiple passes into one capture — and ships the shoot rules
    that say when this works.

    Added

    • Multi-video capturesautosplat process v1.mp4 v2.mp4 [...]
      combines frames from N videos into one capture; COLMAP solves the
      combined set. Each video gets a per-source frame-naming prefix
      (<stem>_frame_NNNNN.jpg) so passes don't clobber each other. WebUI's
      new-capture form accepts a multi-line textarea.
    • autosplat add-video <capture_dir> <video> — append another pass
      to an existing capture and rebuild frames + SfM + training with the
      larger set. Mirrored in the WebUI as an "Add video" form on the
      capture detail page.
    • --target-frames N CLI flag (on process + resume) — per-run
      override for preprocess.target_frames. Useful for long videos where
      the default cap of 250 subsamples too aggressively (a 30-min
      walkthrough otherwise drops to 1 frame every 7s).
    • docs/CAPTURE-GUIDE.md — empirical shoot rules from v1.2.0
      smoke testing. Translation > rotation, smooth turns, ≥80% overlap,
      textured surfaces, even lighting. Includes case data from real
      failed captures so users can interpret their own quality-gate output.

    Fixed / improved

    • Path | list[Path] union type throughout run_pipeline,
      JobRunner, and the adaptive-retry wrapper. Every existing
      single-video caller stays unchanged.
    • No special-case for "add to single-video capture." When you add a
      video, all frames re-extract with the consistent per-source naming.
      Trades one re-preprocess run (~1-2 min) for zero migration logic.
    • No multi-video schema for pipeline.log-readers older than v1.3.0
      — the new videos: [...] field is additive; readers expecting
      video: str still see the legacy form when only one video was used.

    Tests

    265 unit tests (263 passed, 2 opt-in E2E skipped) — +19 over v1.2.0.
    ruff check src/ tests/ → "All checks passed!".

    Downloads
  • v1.2.0 b9b0865aa7

    jkaindl released this 2026-05-24 14:26:46 +00:00 | 93 commits to main since this release

    Resume & Recovery

    Pipeline failures used to be terminal: every botched run had to be re-started from scratch (re-extracting frames, re-running COLMAP) and the user had to remember to swap the matcher by hand. v1.2.0 makes the pipeline self-healing — a partial capture can pick up where it died, the WebUI exposes a Resume button, and a low-camera SfM result auto-retries with the exhaustive matcher even after a process crash.

    Added

    • Adaptive matcher retry in CLI + WebUI — the watcher daemon's Phase-3 retry mechanism (QualityGateFailure → retry_hint → swap matcher) was only wired into the long-running watch daemon. autosplat process and the WebUI's JobRunner now share it via a new in-process run_pipeline_with_adaptive_retry wrapper. A 180° drone turn that breaks the sequential matcher with 3/244 cameras self-rescues with exhaustive, frames intact.
    • autosplat resume <capture_dir> CLI command — continues a previous capture from on-disk state. detect_completed_stages inspects frames/, colmap/sparse/0/, training/*.ply, output/scene.ply and computes the skip-set; read_source_video_from_log scrapes the original video path from pipeline.log (no new manifest file — works for every existing capture). New capture_dir_override parameter on run_pipeline so the resumed run keeps its original date-stamped directory instead of forking <today>_<stem>.
    • WebUI Resume button — failed captures show a Resume button (replacing the broken-for-real-captures Retry that routed through /process). POST /captures/{id}/resume calls a new JobRunner.start_resume_job that spins up a worker thread running resume_capture(...). Shares the existing cancellation/persistence/runs.jsonl path.
    • New-capture run-start flow (V12-1) — the dashboard's "New capture" button now actually starts a pipeline run from a video path (it previously linked back to the captures list). GET/POST /captures/new with path validation (.mp4/.mov/.m4v, file-exists check).
    • Persistent job history via runs.jsonl (V12-2) — the JobRunner appends one record per finalized job to <captures_dir>/<id>/runs.jsonl (status, start/finish timestamps, error). On startup, load_history() rehydrates the in-memory history so a WebUI restart no longer wipes the "Recent jobs" view. Append-only, crash-safe.
    • Responsive WebUI layout + mobile sidebar (V12-3) — three breakpoints (Desktop ≥1024 px / Tablet ≤1023 px / Mobile ≤767 px). Sidebar narrows then slides off-canvas; stat-tiles collapse 4→2 columns; hamburger button toggles .mobile-sidebar-open on body with tap-to-close backdrop; tables scroll horizontally on mobile.
    • URL-param developer mode (V12-4) — reactivates the latent aspect subthemes (?aspect=shugo|gunshi|kantoku|sensei) and the HTMX-annot debug overlay (?dev=1) without adding a settings UI. Both persist in localStorage; URL-params set/clear them.

    Fixed

    • Quality gate runs on resumed SfM — resuming a capture whose previous SfM stage produced a bad sparse model (e.g. only 3/244 cameras) used to silently skip the quality gate and let Brush train on garbage — the gate lived inside the SfM else branch. run_pipeline now re-parses sparse/0 via _parse_mapper_stats when sfm is in skip_stages and feeds the synthesized SfmResult into check_sfm_quality. The adaptive-retry wrapper also drops "sfm" from skip_stages on retry so the matcher swap actually re-runs SfM.
    • Stale-job liveness reconciliation — jobs whose worker thread died mid-run (e.g. host sleep) used to hang as phantom running entries forever. JobRunner._reconcile now flips them to failed with interrupted — the run ended without producing a result on the next read.
    • Version-source consistencypyproject.toml and src/autosplat/__init__.py now agree (both 1.2.0).

    Design Notes

    • Adaptive retry vs. resume — orthogonal mechanisms. Adaptive retry (in-process) catches QualityGateFailure during a live pipeline and retries the next stage with the hint set; frames stay in memory. Resume (cross-process) is the answer when the process is fully gone (sleep / crash / Ctrl-C): it reads disk artifacts and the original log to reconstruct the run.
    • No manifest file for resume. Source video is scraped from the pipeline.start JSON event in pipeline.log. Works for every existing capture without any migration.
    • Resume refuses on a complete capture. If output/scene.ply exists, resume_capture raises immediately instead of silently redoing the whole pipeline.

    Tests

    246 unit tests (244 passed, 2 opt-in E2E skipped) — +40 over v1.1.2.

    For the full changelog see CHANGELOG.md.

    Downloads