splitsmith 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {splitsmith-0.2.1 → splitsmith-0.3.0}/CHANGELOG.md +8 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/PKG-INFO +2 -2
- {splitsmith-0.2.1 → splitsmith-0.3.0}/README.md +1 -1
- {splitsmith-0.2.1 → splitsmith-0.3.0}/pyproject.toml +1 -1
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/__init__.py +1 -1
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/server.py +13 -5
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/AuditControls.tsx +8 -6
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/BeepSection.tsx +226 -57
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/HelpOverlay.tsx +7 -5
- splitsmith-0.3.0/src/splitsmith/ui_static/src/components/audit/BeepAnomalyBanner.tsx +71 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/BeepStatusChip.tsx +17 -38
- splitsmith-0.3.0/src/splitsmith/ui_static/src/components/audit/ShortcutHints.tsx +148 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/StageActionBar.tsx +8 -5
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/api.ts +11 -3
- splitsmith-0.3.0/src/splitsmith/ui_static/src/lib/keyboard.ts +45 -0
- splitsmith-0.3.0/src/splitsmith/ui_static/src/lib/platform.ts +50 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Audit.tsx +132 -368
- splitsmith-0.3.0/src/splitsmith/ui_static/src/pages/BeepReview.tsx +1049 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Coach.tsx +5 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Design.tsx +3 -2
- splitsmith-0.3.0/src/splitsmith/ui_static/tsconfig.app.tsbuildinfo +1 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/uv.lock +1 -1
- splitsmith-0.2.1/src/splitsmith/ui_static/src/components/audit/SyncBanner.tsx +0 -157
- splitsmith-0.2.1/src/splitsmith/ui_static/src/pages/BeepReview.tsx +0 -759
- splitsmith-0.2.1/src/splitsmith/ui_static/tsconfig.app.tsbuildinfo +0 -1
- {splitsmith-0.2.1 → splitsmith-0.3.0}/.gitignore +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/LICENSE +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/docs/local-slim/README.md +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/docs/saas-readiness/README.md +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/site/README.md +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/skills/splitsmith-match/README.md +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/audit.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/automation.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/backup.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/beep_calibration.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/beep_detect.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/cleanup.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/cli.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/coach.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/coach_distributions.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/compare/__init__.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/compare/cli.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/compare/emitter.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/compare/filler.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/compare/layout.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/compare/manifest.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/compare/project_loader.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/composition.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/config.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/cross_align.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/csv_gen.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/ensemble_calibration.json +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/fonts/Antonio-OFL.txt +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/fonts/Antonio-VariableFont.ttf +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/fonts/JetBrainsMono-Bold.ttf +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/fonts/JetBrainsMono-OFL.txt +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/overlay_theme.json +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/templates/action-cut.yaml +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/templates/match-recap.yaml +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/voter_c_gbdt.joblib +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/data/voter_e_visual_probe.joblib +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/__init__.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/agc_state.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/api.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/backend.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/calibration.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/clap_mel.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/features.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/fixtures.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/tta.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/visual.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ensemble/voters.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/fcp7xml_render.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/fcpxml_gen.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/fixture_schema.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/lab/__init__.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/lab/core.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/lab/promote.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/lab/snap_window.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/lab/sweeps.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/lab_cli.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/match_cli.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/match_model.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/match_registry.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mcp/__init__.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mcp/__main__.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mcp/detect_tools.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mcp/export_tools.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mcp/sandbox.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mcp/server.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mcp/tools.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mcp/write_tools.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/model_cli.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/models/__init__.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/models/cache.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/models/download.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/models/errors.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/models/manifest.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/models/registry.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/mp4_render.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/overlay_render.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/overlay_theme.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/relink.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/report.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/runtime.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/shot_detect.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/shot_refine.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/system_check.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/templates.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/thumbnail.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/trim.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/__init__.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/audio.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/embedded.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/exports.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/jobs.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/logging_setup.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/match_exports.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/project.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/scoreboard/__init__.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/scoreboard/cache.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/scoreboard/http.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/scoreboard/local.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/scoreboard/models.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui/scoreboard/protocol.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/.gitignore +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/index.html +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/package-lock.json +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/package.json +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/pnpm-lock.yaml +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/App.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/AddFootageModal.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/AppShell.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/CameraModelSelect.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/CleanupDialog.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/DirectoryPickerModal.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/FolderPicker.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/HitlQueuePanel.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/Jobs.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ListDrawer.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/MarkerGlyph.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/MarkerLayer.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/MountSelect.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/RelinkDialog.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/SettingProvenance.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ShooterScopedRoute.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ShotStepper.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/StageTimeSection.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/SweepsCard.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/VideoPanel.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/Waveform.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/AnomalyChips.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/AnomalyPins.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/CamGridModal.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/CamSyncPill.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/MultiCamColumn.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/PrereqGate.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/SessionSummary.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/audit/StageChipRail.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/developer/DeveloperShell.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/match/MatchShell.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/match/MatchSidebar.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/match/ShooterChipStrip.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/AvatarStack.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/Brand.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/ContextBar.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/DisplayHeading.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/IconButton.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/Kbd.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/Kicker.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/ModeSwitch.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/Readout.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/ShotTimerShell.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/StageDot.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/StatusPill.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/Tick.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/badge.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/button.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/card.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/index.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/ui/skeleton.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/anomalies.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/audit-input.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/audit-next-step.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/features.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/matchHref.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/mode.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/slugify.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/stageStatus.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/lib/utils.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/main.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Compare.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/CreateMatch.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Export.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Home.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Ingest.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Lab.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/MergeMatches.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Pick.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/PromoteReview.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Review.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/Shooters.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/dev/DevCorpus.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/dev/DevRetrain.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/dev/DevReviewQueue.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/pages/dev/DevValidate.tsx +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/styles/index.css +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/tsconfig.app.json +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/tsconfig.json +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/tsconfig.node.json +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/tsconfig.node.tsbuildinfo +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/vite.config.ts +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/user_config.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/video_match.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/video_probe.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/waveform.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/youtube_sidecar.py +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/tests/fixtures/beep_calibration/README.md +0 -0
- {splitsmith-0.2.1 → splitsmith-0.3.0}/tests/fixtures/schemas/README.md +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/mandakan/splitsmith/compare/v0.2.1...v0.3.0) (2026-05-25)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **beep-review:** single home for beep work; trim audit page ([#399](https://github.com/mandakan/splitsmith/issues/399)) ([9ecf999](https://github.com/mandakan/splitsmith/commit/9ecf9998f94edeb5420e3a982f97ae4edb2114f0))
|
|
9
|
+
* **brand:** hero + og:image, new tagline, audit shortcut hints ([#401](https://github.com/mandakan/splitsmith/issues/401)) ([40aa55a](https://github.com/mandakan/splitsmith/commit/40aa55ac56da0e9c81ae373554b9dc8816f4b2e0))
|
|
10
|
+
|
|
3
11
|
## [0.2.1](https://github.com/mandakan/splitsmith/compare/v0.2.0...v0.2.1) (2026-05-24)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splitsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Extract IPSC shot splits from head-mounted camera footage
|
|
5
5
|
Author: Mathias Axell
|
|
6
6
|
License: MIT
|
|
@@ -36,7 +36,7 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
|
|
37
37
|
Extract per-shot split times from head-mounted camera footage of IPSC matches and generate Final Cut Pro timelines with per-shot markers.
|
|
38
38
|
|
|
39
|
-

|
|
40
40
|
|
|
41
41
|
Built to do two things from a single stage video: get per-shot splits for analysis and coaching, and prepare frame-marked clips for match-footage review. Your head-mounted cam (Insta360 Go 3S in this case) already captures audio of every shot; the RO's timer only records your total stage time, so the splits live in the video and nowhere else. Splitsmith extracts them and turns them into a CSV plus an FCPXML timeline with per-shot markers you can step through in Final Cut Pro.
|
|
42
42
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
Extract per-shot split times from head-mounted camera footage of IPSC matches and generate Final Cut Pro timelines with per-shot markers.
|
|
9
9
|
|
|
10
|
-

|
|
11
11
|
|
|
12
12
|
Built to do two things from a single stage video: get per-shot splits for analysis and coaching, and prepare frame-marked clips for match-footage review. Your head-mounted cam (Insta360 Go 3S in this case) already captures audio of every shot; the RO's timer only records your total stage time, so the splits live in the video and nowhere else. Splitsmith extracts them and turns them into a CSV plus an FCPXML timeline with per-shot markers you can step through in Final Cut Pro.
|
|
13
13
|
|
|
@@ -1059,7 +1059,7 @@ class BeepQueueItem(BaseModel):
|
|
|
1059
1059
|
beep_time: float | None
|
|
1060
1060
|
beep_confidence: float | None
|
|
1061
1061
|
beep_reviewed: bool
|
|
1062
|
-
status: Literal["missing", "low_confidence", "unreviewed"]
|
|
1062
|
+
status: Literal["missing", "low_confidence", "unreviewed", "confirmed"]
|
|
1063
1063
|
alt_candidates: list[BeepQueueAltCandidate]
|
|
1064
1064
|
# Auto-computed cross-align suggestion for secondaries lives on the
|
|
1065
1065
|
# shooter's other videos; the SPA fetches them lazily if needed.
|
|
@@ -7257,16 +7257,21 @@ def create_app(
|
|
|
7257
7257
|
# ----------------------------------------------------------------------
|
|
7258
7258
|
|
|
7259
7259
|
@app.get("/api/match/beep-queue", response_model=BeepQueueResponse)
|
|
7260
|
-
def get_beep_queue() -> BeepQueueResponse:
|
|
7260
|
+
def get_beep_queue(include_confirmed: bool = Query(default=False)) -> BeepQueueResponse:
|
|
7261
7261
|
"""Pending beep items across every shooter in the bound match.
|
|
7262
7262
|
|
|
7263
|
-
Surfaces three states per primary video:
|
|
7263
|
+
Surfaces three pending states per primary video:
|
|
7264
7264
|
- ``missing``: detector hasn't run or didn't find a beep
|
|
7265
7265
|
- ``low_confidence``: detector found one but below the project's
|
|
7266
7266
|
auto-trust threshold
|
|
7267
7267
|
- ``unreviewed``: detector found one above threshold but the
|
|
7268
7268
|
user hasn't yet listened + approved
|
|
7269
7269
|
|
|
7270
|
+
When ``include_confirmed`` is true the response also includes
|
|
7271
|
+
items whose beep has been reviewed, with ``status="confirmed"``.
|
|
7272
|
+
The SPA uses this for the "Show confirmed" toggle so the user
|
|
7273
|
+
can revisit a settled beep without having to clear-and-redo it.
|
|
7274
|
+
|
|
7270
7275
|
Items are grouped by stage; per-stage shot detection is gated on
|
|
7271
7276
|
every shooter's primary in that stage being ``beep_reviewed``.
|
|
7272
7277
|
"""
|
|
@@ -7320,12 +7325,15 @@ def create_app(
|
|
|
7320
7325
|
elif primary.beep_reviewed:
|
|
7321
7326
|
grp.confirmed += 1
|
|
7322
7327
|
total_confirmed += 1
|
|
7323
|
-
|
|
7328
|
+
if not include_confirmed:
|
|
7329
|
+
continue
|
|
7330
|
+
status = "confirmed"
|
|
7324
7331
|
elif primary.beep_confidence is not None and primary.beep_confidence < threshold:
|
|
7325
7332
|
status = "low_confidence"
|
|
7326
7333
|
else:
|
|
7327
7334
|
status = "unreviewed"
|
|
7328
|
-
|
|
7335
|
+
if status != "confirmed":
|
|
7336
|
+
total_pending += 1
|
|
7329
7337
|
alts = [
|
|
7330
7338
|
BeepQueueAltCandidate(
|
|
7331
7339
|
time=cand.time,
|
{splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/AuditControls.tsx
RENAMED
|
@@ -12,6 +12,7 @@ import { Maximize2, Minus, Plus } from "lucide-react";
|
|
|
12
12
|
|
|
13
13
|
import { MarkerGlyph, type MarkerKind } from "@/components/MarkerGlyph";
|
|
14
14
|
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { modKeyLabel } from "@/lib/platform";
|
|
15
16
|
import { cn } from "@/lib/utils";
|
|
16
17
|
|
|
17
18
|
export interface MarkerFilters {
|
|
@@ -152,6 +153,7 @@ export function ZoomControls({ zoom, onZoomChange, className }: ZoomControlsProp
|
|
|
152
153
|
const next = base / ZOOM_STEP;
|
|
153
154
|
onZoomChange(next <= MIN_ZOOM ? null : next);
|
|
154
155
|
};
|
|
156
|
+
const mod = modKeyLabel();
|
|
155
157
|
|
|
156
158
|
return (
|
|
157
159
|
<div className={cn("inline-flex items-center gap-1", className)} role="group" aria-label="Zoom">
|
|
@@ -159,8 +161,8 @@ export function ZoomControls({ zoom, onZoomChange, className }: ZoomControlsProp
|
|
|
159
161
|
size="sm"
|
|
160
162
|
variant="ghost"
|
|
161
163
|
onClick={zoomOut}
|
|
162
|
-
aria-label=
|
|
163
|
-
title=
|
|
164
|
+
aria-label={`Zoom out (${mod}+3)`}
|
|
165
|
+
title={`Zoom out (${mod}+3)`}
|
|
164
166
|
>
|
|
165
167
|
<Minus className="size-3" />
|
|
166
168
|
</Button>
|
|
@@ -168,8 +170,8 @@ export function ZoomControls({ zoom, onZoomChange, className }: ZoomControlsProp
|
|
|
168
170
|
size="sm"
|
|
169
171
|
variant="ghost"
|
|
170
172
|
onClick={() => onZoomChange(null)}
|
|
171
|
-
aria-label=
|
|
172
|
-
title=
|
|
173
|
+
aria-label={`Fit waveform (${mod}+2)`}
|
|
174
|
+
title={`Fit waveform (${mod}+2)`}
|
|
173
175
|
aria-pressed={zoom == null}
|
|
174
176
|
>
|
|
175
177
|
<Maximize2 className="size-3" />
|
|
@@ -178,8 +180,8 @@ export function ZoomControls({ zoom, onZoomChange, className }: ZoomControlsProp
|
|
|
178
180
|
size="sm"
|
|
179
181
|
variant="ghost"
|
|
180
182
|
onClick={zoomIn}
|
|
181
|
-
aria-label=
|
|
182
|
-
title=
|
|
183
|
+
aria-label={`Zoom in (${mod}+1)`}
|
|
184
|
+
title={`Zoom in (${mod}+1)`}
|
|
183
185
|
>
|
|
184
186
|
<Plus className="size-3" />
|
|
185
187
|
</Button>
|
{splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/BeepSection.tsx
RENAMED
|
@@ -50,6 +50,8 @@ import {
|
|
|
50
50
|
import { Badge } from "@/components/ui/badge";
|
|
51
51
|
import { Button } from "@/components/ui/button";
|
|
52
52
|
import { Waveform } from "@/components/Waveform";
|
|
53
|
+
import { useSpacePlayPause } from "@/lib/keyboard";
|
|
54
|
+
import { modKeyGlyph } from "@/lib/platform";
|
|
53
55
|
import { cn, useReleaseMediaOnUnmount } from "@/lib/utils";
|
|
54
56
|
import {
|
|
55
57
|
ApiError,
|
|
@@ -695,6 +697,8 @@ export function BeepWaveformPicker({
|
|
|
695
697
|
showFallbackBeepMarker = true,
|
|
696
698
|
instructions,
|
|
697
699
|
ariaLabel,
|
|
700
|
+
externalMediaRef,
|
|
701
|
+
fillHeight = false,
|
|
698
702
|
}: {
|
|
699
703
|
slug: string;
|
|
700
704
|
stageNumber: number;
|
|
@@ -715,17 +719,56 @@ export function BeepWaveformPicker({
|
|
|
715
719
|
instructions?: string;
|
|
716
720
|
/** Override the canvas aria-label for screen readers. */
|
|
717
721
|
ariaLabel?: string;
|
|
722
|
+
/** Optional external media element (audio OR video) for the picker
|
|
723
|
+
* to drive. When provided, the picker skips rendering its own
|
|
724
|
+
* <audio> element -- the parent owns playback (same pattern as
|
|
725
|
+
* the audit page's MultiCamColumn <video>). Space, click-to-scrub,
|
|
726
|
+
* zoom, and the initial seek all act on this element. When not
|
|
727
|
+
* provided, the picker creates its own internal <audio src=
|
|
728
|
+
* videoAudioUrl(...)> as before -- StageTimeSection relies on
|
|
729
|
+
* that path. */
|
|
730
|
+
externalMediaRef?: { current: HTMLAudioElement | HTMLVideoElement | null };
|
|
731
|
+
/** Grow the waveform vertically (200 px instead of 80 px) so the
|
|
732
|
+
* picker box fills the row when paired with a taller sibling like
|
|
733
|
+
* BeepReview's <video aspect-video>. */
|
|
734
|
+
fillHeight?: boolean;
|
|
718
735
|
}) {
|
|
719
736
|
const [peaks, setPeaks] = useState<PeaksResult | null>(null);
|
|
720
737
|
const [loading, setLoading] = useState(true);
|
|
721
738
|
const [unavailable, setUnavailable] = useState(false);
|
|
722
739
|
const [localTime, setLocalTime] = useState<number>(0);
|
|
723
740
|
const [playing, setPlaying] = useState(false);
|
|
724
|
-
|
|
741
|
+
// Zoom is a multiplier relative to the viewport-fit baseline (null =
|
|
742
|
+
// fit-to-width). The audit canvas uses the same model -- it
|
|
743
|
+
// matters because absolute px/s blows up on long clips: a 104 s
|
|
744
|
+
// primary at 240 px/s renders 25k px wide, so even "first zoom"
|
|
745
|
+
// shoves the whole clip off-screen.
|
|
746
|
+
const [zoom, setZoom] = useState<number | null>(null);
|
|
747
|
+
const [viewportWidth, setViewportWidth] = useState(0);
|
|
748
|
+
const viewportRef = useRef<HTMLDivElement | null>(null);
|
|
749
|
+
const wrapperCallbackRef = useCallback((el: HTMLDivElement | null) => {
|
|
750
|
+
viewportRef.current = el;
|
|
751
|
+
if (!el) return;
|
|
752
|
+
setViewportWidth(Math.floor(el.getBoundingClientRect().width));
|
|
753
|
+
const observer = new ResizeObserver((entries) => {
|
|
754
|
+
for (const entry of entries) {
|
|
755
|
+
const w = Math.floor(entry.contentRect.width);
|
|
756
|
+
if (w > 0) setViewportWidth(w);
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
observer.observe(el);
|
|
760
|
+
return () => observer.disconnect();
|
|
761
|
+
}, []);
|
|
725
762
|
const [snapping, setSnapping] = useState(false);
|
|
726
763
|
const [proposal, setProposal] = useState<BeepSnapResult | null>(null);
|
|
727
|
-
|
|
728
|
-
|
|
764
|
+
// The picker's playback handle. When the parent owns the media
|
|
765
|
+
// element (BeepReview passes its <video> ref), we just alias the
|
|
766
|
+
// external ref; otherwise we create our own <audio> below. Either
|
|
767
|
+
// way, the rest of the picker (togglePlay, scrub, init seek,
|
|
768
|
+
// Space hook) reads / writes through ``mediaRef``.
|
|
769
|
+
const internalAudioRef = useRef<HTMLAudioElement | null>(null);
|
|
770
|
+
const mediaRef = externalMediaRef ?? internalAudioRef;
|
|
771
|
+
useReleaseMediaOnUnmount(internalAudioRef);
|
|
729
772
|
const rafRef = useRef<number | null>(null);
|
|
730
773
|
|
|
731
774
|
useEffect(() => {
|
|
@@ -737,7 +780,23 @@ export function BeepWaveformPicker({
|
|
|
737
780
|
.then((p) => {
|
|
738
781
|
if (cancelled) return;
|
|
739
782
|
setPeaks(p);
|
|
740
|
-
|
|
783
|
+
// Park the playhead at the detected beep so pressing Play
|
|
784
|
+
// immediately auditions what the detector picked, instead of
|
|
785
|
+
// making the user scrub from t=0 first. Also seek the
|
|
786
|
+
// underlying <audio> element when it's already loaded --
|
|
787
|
+
// otherwise localTime gets out of sync with the audio
|
|
788
|
+
// element's currentTime, and the first Play snaps the
|
|
789
|
+
// playhead back to whatever it was last at.
|
|
790
|
+
const t = p.beep_time ?? 0;
|
|
791
|
+
setLocalTime(t);
|
|
792
|
+
const el = mediaRef.current;
|
|
793
|
+
if (el) {
|
|
794
|
+
try {
|
|
795
|
+
el.currentTime = t;
|
|
796
|
+
} catch {
|
|
797
|
+
/* metadata not loaded yet -- onLoadedMetadata will retry */
|
|
798
|
+
}
|
|
799
|
+
}
|
|
741
800
|
})
|
|
742
801
|
.catch(() => {
|
|
743
802
|
if (!cancelled) setUnavailable(true);
|
|
@@ -776,7 +835,7 @@ export function BeepWaveformPicker({
|
|
|
776
835
|
useEffect(() => {
|
|
777
836
|
if (!playing) return;
|
|
778
837
|
const tick = () => {
|
|
779
|
-
const el =
|
|
838
|
+
const el = mediaRef.current;
|
|
780
839
|
if (el) setLocalTime(el.currentTime);
|
|
781
840
|
rafRef.current = requestAnimationFrame(tick);
|
|
782
841
|
};
|
|
@@ -788,7 +847,7 @@ export function BeepWaveformPicker({
|
|
|
788
847
|
}, [playing]);
|
|
789
848
|
|
|
790
849
|
const togglePlay = useCallback(() => {
|
|
791
|
-
const el =
|
|
850
|
+
const el = mediaRef.current;
|
|
792
851
|
if (!el) return;
|
|
793
852
|
if (el.paused) {
|
|
794
853
|
el.play().catch(() => {
|
|
@@ -797,7 +856,36 @@ export function BeepWaveformPicker({
|
|
|
797
856
|
} else {
|
|
798
857
|
el.pause();
|
|
799
858
|
}
|
|
800
|
-
}, []);
|
|
859
|
+
}, [mediaRef]);
|
|
860
|
+
|
|
861
|
+
// Window-level Space → toggle so the user can audition the beep
|
|
862
|
+
// even when focus is parked on a sidebar link or a queue row
|
|
863
|
+
// (the common path: click an item, hit Space). Gated by the picker
|
|
864
|
+
// actually having peaks loaded so Space falls through to the
|
|
865
|
+
// browser default while the picker is in its empty / loading state.
|
|
866
|
+
useSpacePlayPause(togglePlay, peaks != null);
|
|
867
|
+
|
|
868
|
+
// When the parent owns the media element (BeepReview), the
|
|
869
|
+
// ``playing`` / ``localTime`` state need a separate event bridge --
|
|
870
|
+
// the inline ``onPlay``/``onPause`` handlers below only fire on the
|
|
871
|
+
// internal <audio>. Listen on the external element so the picker's
|
|
872
|
+
// playhead + Play button label still reflect the truth.
|
|
873
|
+
useEffect(() => {
|
|
874
|
+
if (!externalMediaRef) return;
|
|
875
|
+
const el = externalMediaRef.current;
|
|
876
|
+
if (!el) return;
|
|
877
|
+
const onPlay = () => setPlaying(true);
|
|
878
|
+
const onPause = () => setPlaying(false);
|
|
879
|
+
const onEnded = () => setPlaying(false);
|
|
880
|
+
el.addEventListener("play", onPlay);
|
|
881
|
+
el.addEventListener("pause", onPause);
|
|
882
|
+
el.addEventListener("ended", onEnded);
|
|
883
|
+
return () => {
|
|
884
|
+
el.removeEventListener("play", onPlay);
|
|
885
|
+
el.removeEventListener("pause", onPause);
|
|
886
|
+
el.removeEventListener("ended", onEnded);
|
|
887
|
+
};
|
|
888
|
+
}, [externalMediaRef, peaks]);
|
|
801
889
|
|
|
802
890
|
// Click / drag = seek audio AND set the marker. Marker and the
|
|
803
891
|
// numeric input both read from the draft state in the parent, so they
|
|
@@ -806,7 +894,7 @@ export function BeepWaveformPicker({
|
|
|
806
894
|
const handleScrub = useCallback(
|
|
807
895
|
(t: number) => {
|
|
808
896
|
setLocalTime(t);
|
|
809
|
-
const el =
|
|
897
|
+
const el = mediaRef.current;
|
|
810
898
|
if (el) {
|
|
811
899
|
try {
|
|
812
900
|
el.currentTime = t;
|
|
@@ -853,19 +941,49 @@ export function BeepWaveformPicker({
|
|
|
853
941
|
|
|
854
942
|
const dismissProposal = useCallback(() => setProposal(null), []);
|
|
855
943
|
|
|
944
|
+
// Zoom math mirrors the audit canvas: 1.5x per click, capped at
|
|
945
|
+
// 16x in, 0.25x out (anything below = back to fit).
|
|
856
946
|
const zoomIn = useCallback(() => {
|
|
857
|
-
|
|
858
|
-
if (cur == null) return 240;
|
|
859
|
-
return Math.min(2000, cur * 2);
|
|
860
|
-
});
|
|
947
|
+
setZoom((z) => Math.min(16, (z ?? 1) * 1.5));
|
|
861
948
|
}, []);
|
|
862
949
|
const zoomOut = useCallback(() => {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
return next < 60 ? null : next;
|
|
950
|
+
setZoom((z) => {
|
|
951
|
+
const next = (z ?? 1) / 1.5;
|
|
952
|
+
return next <= 0.25 ? null : next;
|
|
867
953
|
});
|
|
868
954
|
}, []);
|
|
955
|
+
const zoomFit = useCallback(() => setZoom(null), []);
|
|
956
|
+
const pxPerSec = useMemo(() => {
|
|
957
|
+
if (zoom == null || !peaks) return null;
|
|
958
|
+
if (peaks.duration <= 0 || viewportWidth <= 0) return null;
|
|
959
|
+
const fitPps = viewportWidth / peaks.duration;
|
|
960
|
+
return Math.max(1, fitPps * zoom);
|
|
961
|
+
}, [zoom, viewportWidth, peaks]);
|
|
962
|
+
|
|
963
|
+
// Cmd/Ctrl + 1/2/3 -- same bindings as the audit canvas waveform so
|
|
964
|
+
// muscle memory carries across surfaces. Cmd+1 = zoom in, Cmd+2 =
|
|
965
|
+
// fit, Cmd+3 = zoom out. Ignored when typing in an input/textarea so
|
|
966
|
+
// we don't steal the browser's tab-cycling shortcut from form fields.
|
|
967
|
+
useEffect(() => {
|
|
968
|
+
function onKey(e: KeyboardEvent) {
|
|
969
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
970
|
+
if (e.key !== "1" && e.key !== "2" && e.key !== "3") return;
|
|
971
|
+
if (
|
|
972
|
+
e.target instanceof HTMLElement &&
|
|
973
|
+
(e.target.tagName === "INPUT" ||
|
|
974
|
+
e.target.tagName === "TEXTAREA" ||
|
|
975
|
+
e.target.isContentEditable)
|
|
976
|
+
) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
e.preventDefault();
|
|
980
|
+
if (e.key === "1") zoomIn();
|
|
981
|
+
else if (e.key === "2") zoomFit();
|
|
982
|
+
else zoomOut();
|
|
983
|
+
}
|
|
984
|
+
window.addEventListener("keydown", onKey);
|
|
985
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
986
|
+
}, [zoomIn, zoomOut, zoomFit]);
|
|
869
987
|
|
|
870
988
|
if (loading) {
|
|
871
989
|
return (
|
|
@@ -898,7 +1016,7 @@ export function BeepWaveformPicker({
|
|
|
898
1016
|
size="icon"
|
|
899
1017
|
variant="ghost"
|
|
900
1018
|
onClick={zoomOut}
|
|
901
|
-
disabled={
|
|
1019
|
+
disabled={zoom == null}
|
|
902
1020
|
aria-label="Zoom out"
|
|
903
1021
|
title="Zoom out"
|
|
904
1022
|
className="size-7"
|
|
@@ -917,50 +1035,101 @@ export function BeepWaveformPicker({
|
|
|
917
1035
|
</Button>
|
|
918
1036
|
</div>
|
|
919
1037
|
</div>
|
|
920
|
-
<
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
className="
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1038
|
+
<div ref={wrapperCallbackRef}>
|
|
1039
|
+
<Waveform
|
|
1040
|
+
peaks={peaks.peaks}
|
|
1041
|
+
duration={peaks.duration}
|
|
1042
|
+
currentTime={localTime}
|
|
1043
|
+
onScrub={handleScrub}
|
|
1044
|
+
onScrubEnd={handleScrubEnd}
|
|
1045
|
+
beepTime={markerLocal}
|
|
1046
|
+
pixelsPerSecond={pxPerSec}
|
|
1047
|
+
height={fillHeight ? 200 : 80}
|
|
1048
|
+
ariaLabel={ariaLabel ?? `Beep editor waveform for stage ${stageNumber}`}
|
|
1049
|
+
/>
|
|
1050
|
+
</div>
|
|
1051
|
+
{/* Zoom-shortcut chrome -- mirrors the time-ruler footer that
|
|
1052
|
+
lives under the audit canvas waveform. Same bindings
|
|
1053
|
+
(modKey+1/2/3) so muscle memory carries between surfaces.
|
|
1054
|
+
Right-aligned; the eyebrow on the left labels the row so the
|
|
1055
|
+
chord doesn't read as orphan keys. */}
|
|
1056
|
+
<div className="-mx-3 mt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 border-t border-rule bg-surface-3/60 px-4 py-2 font-mono text-[0.625rem] uppercase tracking-[0.08em] text-subtle">
|
|
1057
|
+
<span className="tracking-[0.14em] text-muted">Zoom</span>
|
|
1058
|
+
<span className="ml-auto inline-flex items-center gap-1.5">
|
|
1059
|
+
<kbd className="rounded border border-rule-strong bg-surface-2 px-1.5 py-px font-mono text-[0.625rem] font-semibold text-ink-2">
|
|
1060
|
+
{modKeyGlyph()}1
|
|
1061
|
+
</kbd>
|
|
1062
|
+
<span>in</span>
|
|
1063
|
+
</span>
|
|
1064
|
+
<span className="inline-flex items-center gap-1.5">
|
|
1065
|
+
<kbd className="rounded border border-rule-strong bg-surface-2 px-1.5 py-px font-mono text-[0.625rem] font-semibold text-ink-2">
|
|
1066
|
+
{modKeyGlyph()}2
|
|
1067
|
+
</kbd>
|
|
1068
|
+
<span>fit</span>
|
|
1069
|
+
</span>
|
|
1070
|
+
<span className="inline-flex items-center gap-1.5">
|
|
1071
|
+
<kbd className="rounded border border-rule-strong bg-surface-2 px-1.5 py-px font-mono text-[0.625rem] font-semibold text-ink-2">
|
|
1072
|
+
{modKeyGlyph()}3
|
|
1073
|
+
</kbd>
|
|
1074
|
+
<span>out</span>
|
|
1075
|
+
</span>
|
|
1076
|
+
</div>
|
|
1077
|
+
{externalMediaRef ? null : (
|
|
1078
|
+
<audio
|
|
1079
|
+
ref={internalAudioRef}
|
|
1080
|
+
src={api.videoAudioUrl(slug, stageNumber, videoId)}
|
|
1081
|
+
preload="metadata"
|
|
1082
|
+
onPlay={() => setPlaying(true)}
|
|
1083
|
+
onPause={() => setPlaying(false)}
|
|
1084
|
+
onEnded={() => setPlaying(false)}
|
|
1085
|
+
onLoadedMetadata={() => {
|
|
1086
|
+
// Retry the initial seek when audio metadata lands
|
|
1087
|
+
// after peaks did. Without this, the picker shows the
|
|
1088
|
+
// beep marker but Play starts at 0.
|
|
1089
|
+
const el = internalAudioRef.current;
|
|
1090
|
+
if (!el) return;
|
|
1091
|
+
if (Math.abs(el.currentTime - localTime) > 0.05) {
|
|
1092
|
+
try {
|
|
1093
|
+
el.currentTime = localTime;
|
|
1094
|
+
} catch {
|
|
1095
|
+
/* unreachable -- metadata loaded by definition */
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}}
|
|
1099
|
+
controls
|
|
1100
|
+
className="w-full"
|
|
1101
|
+
/>
|
|
1102
|
+
)}
|
|
1103
|
+
{/* Play / Snap buttons live in this row only when the picker
|
|
1104
|
+
owns its own <audio>. When the parent passes an external
|
|
1105
|
+
media element (e.g. BeepReview's <video controls>), the
|
|
1106
|
+
play affordance + Space toggle already exist on that
|
|
1107
|
+
element -- a redundant button row just eats vertical space. */}
|
|
1108
|
+
{externalMediaRef ? null : (
|
|
1109
|
+
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
952
1110
|
<Button
|
|
953
1111
|
size="sm"
|
|
954
|
-
variant="
|
|
955
|
-
onClick={
|
|
956
|
-
|
|
957
|
-
title="Snap the marker to the rise-foot of the nearest beep tone (±1.5s)"
|
|
1112
|
+
variant="outline"
|
|
1113
|
+
onClick={togglePlay}
|
|
1114
|
+
title="Play / pause the audio"
|
|
958
1115
|
>
|
|
959
|
-
{
|
|
960
|
-
|
|
1116
|
+
{playing ? <Pause /> : <Play />}
|
|
1117
|
+
{playing ? "Pause" : "Play"}
|
|
961
1118
|
</Button>
|
|
962
|
-
|
|
963
|
-
|
|
1119
|
+
{snapEnabled ? (
|
|
1120
|
+
<Button
|
|
1121
|
+
size="sm"
|
|
1122
|
+
variant="default"
|
|
1123
|
+
onClick={() => void requestSnap()}
|
|
1124
|
+
disabled={draftSourceTime == null || snapping}
|
|
1125
|
+
title="Snap the marker to the rise-foot of the nearest beep tone (±1.5s)"
|
|
1126
|
+
>
|
|
1127
|
+
{snapping ? <Loader2 className="animate-spin" /> : <Crosshair />}
|
|
1128
|
+
Snap to beep
|
|
1129
|
+
</Button>
|
|
1130
|
+
) : null}
|
|
1131
|
+
</div>
|
|
1132
|
+
)}
|
|
964
1133
|
{proposal != null ? (
|
|
965
1134
|
<SnapProposal
|
|
966
1135
|
slug={slug}
|
{splitsmith-0.2.1 → splitsmith-0.3.0}/src/splitsmith/ui_static/src/components/HelpOverlay.tsx
RENAMED
|
@@ -14,6 +14,7 @@ import { useEffect, useRef } from "react";
|
|
|
14
14
|
import { X } from "lucide-react";
|
|
15
15
|
|
|
16
16
|
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { modKeyGlyph } from "@/lib/platform";
|
|
17
18
|
import { cn } from "@/lib/utils";
|
|
18
19
|
|
|
19
20
|
export type HelpMode = "audit" | "review";
|
|
@@ -35,6 +36,7 @@ interface Section {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function sections(mode: HelpMode): Section[] {
|
|
39
|
+
const mod = modKeyGlyph();
|
|
38
40
|
const playback: ShortcutRow[] = [
|
|
39
41
|
{ keys: ["Space"], desc: "Play / pause" },
|
|
40
42
|
{ keys: ["←", "→"], desc: "Nudge playhead 250 ms" },
|
|
@@ -54,14 +56,14 @@ function sections(mode: HelpMode): Section[] {
|
|
|
54
56
|
];
|
|
55
57
|
const view: ShortcutRow[] = [
|
|
56
58
|
{ keys: ["L"], desc: "Toggle the marker list drawer" },
|
|
57
|
-
{ keys: [
|
|
58
|
-
{ keys: [
|
|
59
|
-
{ keys: [
|
|
59
|
+
{ keys: [mod, "1"], desc: "Zoom in" },
|
|
60
|
+
{ keys: [mod, "2"], desc: "Fit waveform to view" },
|
|
61
|
+
{ keys: [mod, "3"], desc: "Zoom out" },
|
|
60
62
|
];
|
|
61
63
|
const edit: ShortcutRow[] = [
|
|
62
|
-
{ keys: [
|
|
64
|
+
{ keys: [mod, "Z"], desc: "Undo last marker change" },
|
|
63
65
|
{
|
|
64
|
-
keys: [
|
|
66
|
+
keys: [mod, "S"],
|
|
65
67
|
desc:
|
|
66
68
|
mode === "audit"
|
|
67
69
|
? "Save audit JSON and jump to the next stage"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ArrowRight, Link2 } from "lucide-react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { useMatchHref } from "@/lib/matchHref";
|
|
5
|
+
|
|
6
|
+
export interface BeepAnomalyBannerProps {
|
|
7
|
+
/** The diagnostic produced by Audit's "first shot / overshoot" heuristic
|
|
8
|
+
* (Audit.tsx -> beepDiagnostic). Render-gated by the parent so this
|
|
9
|
+
* component only mounts when a real diagnostic exists. */
|
|
10
|
+
reason: string;
|
|
11
|
+
/** Shooter slug for the deep-link target. */
|
|
12
|
+
slug: string;
|
|
13
|
+
/** Stage number for the deep-link target. */
|
|
14
|
+
stageNumber: number;
|
|
15
|
+
/** Primary video id for the deep-link target. The /beep-review queue
|
|
16
|
+
* keys items by ``slug::stage::video_id``; we pass the same shape via
|
|
17
|
+
* ``?focus=...`` so the queue opens with the exact item active. */
|
|
18
|
+
videoId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Amber "beep looks wrong" banner that lives below the audit toolbar.
|
|
23
|
+
*
|
|
24
|
+
* Replaces the old in-page sync-mode entry (the chip's "Re-pick beep"
|
|
25
|
+
* button). Audit is now read-only with respect to the beep; the banner
|
|
26
|
+
* names the suspicion and ships the operator to /beep-review where the
|
|
27
|
+
* waveform picker + video preview live.
|
|
28
|
+
*
|
|
29
|
+
* Scoping: the banner fires only on the active shooter's video, never
|
|
30
|
+
* cross-shooter. Cross-shooter awareness ("3 beeps pending on stage 02")
|
|
31
|
+
* belongs in the queue, not in a per-stage warning surface here.
|
|
32
|
+
*/
|
|
33
|
+
export function BeepAnomalyBanner({
|
|
34
|
+
reason,
|
|
35
|
+
slug,
|
|
36
|
+
stageNumber,
|
|
37
|
+
videoId,
|
|
38
|
+
}: BeepAnomalyBannerProps) {
|
|
39
|
+
const href = useMatchHref();
|
|
40
|
+
const focusKey = `${slug}::${stageNumber}::${videoId}`;
|
|
41
|
+
const target = `${href("beep-review")}?focus=${encodeURIComponent(focusKey)}`;
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
role="status"
|
|
45
|
+
className="flex flex-wrap items-start gap-3 rounded-2xl border border-live/40 bg-live/[0.08] px-4 py-3.5"
|
|
46
|
+
>
|
|
47
|
+
<span
|
|
48
|
+
aria-hidden
|
|
49
|
+
className="mt-0.5 inline-flex size-5.5 shrink-0 items-center justify-center rounded-full border border-live/60 bg-live/10 font-mono text-[0.6875rem] font-bold text-live"
|
|
50
|
+
>
|
|
51
|
+
!
|
|
52
|
+
</span>
|
|
53
|
+
<div className="flex min-w-[14rem] flex-1 flex-col gap-1">
|
|
54
|
+
<div className="font-display text-[0.75rem] font-bold uppercase tracking-[0.08em] text-live">
|
|
55
|
+
Looks like the beep is wrong
|
|
56
|
+
</div>
|
|
57
|
+
<p className="m-0 text-[0.8125rem] leading-snug text-ink-2">
|
|
58
|
+
{reason}
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
<Link
|
|
62
|
+
to={target}
|
|
63
|
+
className="inline-flex items-center gap-1.5 rounded-md border-0 bg-led-fill px-3.5 py-2 font-display text-[0.6875rem] font-bold uppercase tracking-[0.08em] text-ink shadow-[0_0_0_1px_var(--color-led),0_0_14px_var(--color-led-glow)] hover:bg-led"
|
|
64
|
+
>
|
|
65
|
+
<Link2 className="size-3" strokeWidth={2} aria-hidden />
|
|
66
|
+
Review this beep
|
|
67
|
+
<ArrowRight className="size-3" aria-hidden />
|
|
68
|
+
</Link>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|