From ab9011c8256b119a67f3d06e947876eede3745dd Mon Sep 17 00:00:00 2001
From: firestar5683 <168790843+firestar5683@users.noreply.github.com>
Date: Thu, 10 Sep 2026 15:22:36 -0500
Subject: [PATCH] range
---
.../assets/mobile/js/components/AppShell.js | 2 +-
.../js/components/LateralTuningPanel.js | 116 ++++++++++++++++--
starpilot/system/the_galaxy/flm_workspace.py | 40 ++++++
.../the_galaxy/tests/test_dashcam_routes.py | 1 +
.../the_galaxy/tests/test_flm_workspace.py | 14 ++-
.../the_galaxy/tests/test_ui_vue_frontend.py | 6 +
starpilot/system/the_galaxy/the_galaxy.py | 5 +-
starpilot/system/the_galaxy/utilities.py | 1 +
8 files changed, 169 insertions(+), 16 deletions(-)
diff --git a/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js b/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js
index 11d5217a0b..577c00fa99 100644
--- a/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js
+++ b/starpilot/system/the_galaxy/assets/mobile/js/components/AppShell.js
@@ -118,7 +118,7 @@ export const AppShell = {
- {{ tr("Galaxy") }}
+ Galaxy
0 && !this.isOnroad && !this.laneCentering
+ return this.selectedRoutes.length > 0 && this.selectedSegmentCount > 0 && this.selectedSegmentCount <= MAX_SEGMENTS && !this.isOnroad && !this.laneCentering
+ },
+ selectedSegmentCount() {
+ return this.selectedRoutes.reduce((total, routeName) => total + this.routeSelectedSegmentCount(routeName), 0)
},
canApplyTrial() {
const trial = this.activeTrial
@@ -147,6 +153,32 @@ export const LateralTuningPanel = {
const dur = min >= 60 ? `${Math.floor(min / 60)}h ${min % 60}m` : `~${min} min`
return `${seg} segment${seg === 1 ? "" : "s"} (${dur})`
},
+ routeSegmentBounds(routeName) {
+ const route = this.routes.find((item) => item && item.name === routeName) || {}
+ const count = Math.max(0, Math.round(this.num(route.segmentCount)))
+ const first = Math.max(0, Math.round(this.num(route.firstSegmentNum)))
+ return { first, last: first + Math.max(0, count - 1), count }
+ },
+ routeSelectedSegmentCount(routeName) {
+ const bounds = this.routeSegmentBounds(routeName)
+ if (!bounds.count) return 0
+ const selected = this.segmentRanges[routeName] || {}
+ const rawStart = String(selected.start ?? "").trim()
+ const rawEnd = String(selected.end ?? "").trim()
+ const start = Math.min(bounds.last, Math.max(bounds.first, rawStart === "" ? bounds.first : this.num(rawStart, bounds.first)))
+ const end = Math.max(bounds.first, Math.min(bounds.last, rawEnd === "" ? bounds.last : this.num(rawEnd, bounds.last)))
+ return end >= start ? end - start + 1 : 0
+ },
+ setSegmentRange(routeName, key, value) {
+ const cleaned = String(value ?? "").replace(/[^\d]/g, "")
+ this.segmentRanges = {
+ ...this.segmentRanges,
+ [routeName]: {
+ ...(this.segmentRanges[routeName] || {}),
+ [key]: cleaned,
+ },
+ }
+ },
syncFeedback(report) {
const fb = (report && report.feedback) || {}
this.feedbackAccepted = Array.isArray(fb.acceptedDimensions) ? [...fb.acceptedDimensions] : []
@@ -307,14 +339,65 @@ export const LateralTuningPanel = {
},
toggleRoute(name) {
const set = new Set(this.selectedRoutes)
- if (set.has(name)) set.delete(name); else set.add(name)
+ const ranges = { ...this.segmentRanges }
+ if (set.has(name)) {
+ set.delete(name)
+ delete ranges[name]
+ } else {
+ const remaining = MAX_SEGMENTS - this.selectedSegmentCount
+ if (remaining <= 0) {
+ showSnackbar(`FLM is limited to ${MAX_SEGMENTS} segments at a time.`, "error")
+ return
+ }
+ const bounds = this.routeSegmentBounds(name)
+ if (!bounds.count) return
+ set.add(name)
+ if (bounds.count > remaining) {
+ ranges[name] = { start: String(bounds.first), end: String(bounds.first + remaining - 1) }
+ }
+ }
+ this.segmentRanges = ranges
this.selectedRoutes = [...set]
},
- clearSelection() { this.selectedRoutes = [] },
+ selectFirstSegments() {
+ const selected = []
+ const ranges = {}
+ let remaining = MAX_SEGMENTS
+ for (const route of this.routes) {
+ if (remaining <= 0) break
+ const bounds = this.routeSegmentBounds(route.name)
+ if (!bounds.count) continue
+ const take = Math.min(bounds.count, remaining)
+ selected.push(route.name)
+ if (take < bounds.count) {
+ ranges[route.name] = { start: String(bounds.first), end: String(bounds.first + take - 1) }
+ }
+ remaining -= take
+ }
+ this.selectedRoutes = selected
+ this.segmentRanges = ranges
+ },
+ clearSelection() {
+ this.selectedRoutes = []
+ this.segmentRanges = {}
+ },
+ selectedSegmentRanges() {
+ const ranges = {}
+ for (const routeName of this.selectedRoutes) {
+ const selected = this.segmentRanges[routeName] || {}
+ const start = String(selected.start ?? "").trim()
+ const end = String(selected.end ?? "").trim()
+ if (start || end) ranges[routeName] = { start: start || null, end: end || null }
+ }
+ return ranges
+ },
async analyze() {
if (!this.canAnalyze || this.busy) return
- const ok = await this.runWith(() => api.flmAnalyze(this.selectedRoutes, {}), "FLM analysis started.")
- if (ok && this.selectedRoutes.length) this.selectedRoutes = []
+ const ok = await this.runWith(() => api.flmAnalyze(this.selectedRoutes, this.selectedSegmentRanges()), "FLM analysis started.")
+ if (ok && this.selectedRoutes.length) {
+ this.selectedRoutes = []
+ this.segmentRanges = {}
+ }
},
async stopAnalyze() {
await this.runWith(() => api.flmStopAnalyze(), "FLM analysis stopped.")
@@ -520,24 +603,33 @@ export const LateralTuningPanel = {
Local Routes
-
Pick up to 8 routes to analyze. Whole routes are used.
+
Pick routes and segment ranges to analyze. A run is limited to 5 segments total.
Loading local routes...
No local routes found.
-
+
+ {{ selectedSegmentCount }}/{{ maxSegments }} segments selected
+
+
diff --git a/starpilot/system/the_galaxy/flm_workspace.py b/starpilot/system/the_galaxy/flm_workspace.py
index 9af8a148de..d3be0d72fd 100644
--- a/starpilot/system/the_galaxy/flm_workspace.py
+++ b/starpilot/system/the_galaxy/flm_workspace.py
@@ -42,6 +42,7 @@ FLM_STATUS_PATH = Path("/tmp/galaxy_flm_status.json")
FLM_LOG_PATH = Path("/tmp/galaxy_flm.log")
FLM_STATUS_MAX_AGE_SECONDS = 3600.0
FLM_ANALYZER_ROUTE_LIMIT = 8
+FLM_MAX_ANALYSIS_SEGMENTS = 5
FLM_ANALYZER_PROCESS = None
FLM_ANALYZER_LOCK = threading.Lock()
FLM_PROGRESS_FILENAME = "progress.json"
@@ -553,6 +554,42 @@ def normalize_segment_ranges(route_names: list[str], segment_ranges: Any) -> dic
return normalized
+def selected_segment_count(route_names: list[str], footage_paths: list[str],
+ segment_ranges: dict[str, dict[str, int | None]] | None = None) -> int:
+ normalized = normalize_segment_ranges(route_names, segment_ranges)
+ total = 0
+ for route in route_names[:FLM_ANALYZER_ROUTE_LIMIT]:
+ route = str(route).strip()
+ if not route:
+ continue
+ segments = []
+ for footage_path in footage_paths:
+ candidate = utilities.get_segments_in_route(route, footage_path)
+ if candidate:
+ segments = candidate
+ break
+
+ segment_range = normalized.get(route, {})
+ start = segment_range.get("start")
+ end = segment_range.get("end")
+ if segments:
+ numbers = [_parse_segment_num(segment) for segment in segments]
+ lower = start if start is not None else min(numbers)
+ upper = end if end is not None else max(numbers)
+ total += sum(lower <= number <= upper for number in numbers)
+ elif start is not None and end is not None:
+ total += max(0, end - start + 1)
+ return total
+
+
+def enforce_segment_limit(route_names: list[str], footage_paths: list[str],
+ segment_ranges: dict[str, dict[str, int | None]] | None = None) -> int:
+ count = selected_segment_count(route_names, footage_paths, segment_ranges)
+ if count > FLM_MAX_ANALYSIS_SEGMENTS:
+ raise ValueError(f"FLM analysis is limited to {FLM_MAX_ANALYSIS_SEGMENTS} segments at a time (requested {count}).")
+ return count
+
+
def start_flm_background_analysis(route_names: list[str], footage_paths: list[str],
segment_ranges: dict[str, dict[str, int | None]] | None = None) -> bool:
global FLM_ANALYZER_PROCESS
@@ -567,6 +604,8 @@ def start_flm_background_analysis(route_names: list[str], footage_paths: list[st
except FLMAnalysisCancelled:
return False
+ enforce_segment_limit(route_names, footage_paths, segment_ranges)
+
ensure_flm_workspace()
process_to_watch = None
with FLM_ANALYZER_LOCK:
@@ -2424,6 +2463,7 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
segment_ranges = normalize_segment_ranges(route_names, segment_ranges)
sources, warnings = resolve_route_sources(route_names, footage_paths, segment_ranges)
+ enforce_segment_limit(route_names, footage_paths, segment_ranges)
if not sources:
raise RuntimeError("No local routes with qlogs or rlogs were found for the selected routes.")
diff --git a/starpilot/system/the_galaxy/tests/test_dashcam_routes.py b/starpilot/system/the_galaxy/tests/test_dashcam_routes.py
index 13864037e0..c7573c64cb 100644
--- a/starpilot/system/the_galaxy/tests/test_dashcam_routes.py
+++ b/starpilot/system/the_galaxy/tests/test_dashcam_routes.py
@@ -67,6 +67,7 @@ def test_process_route_is_metadata_only_and_retains_fields(monkeypatch, tmp_path
"isCustomName": True,
"is_preserved": True,
"segmentCount": 4,
+ "firstSegmentNum": 3,
"approxDurationSeconds": 240,
}
diff --git a/starpilot/system/the_galaxy/tests/test_flm_workspace.py b/starpilot/system/the_galaxy/tests/test_flm_workspace.py
index 46d313b4a6..3ae0a3e160 100644
--- a/starpilot/system/the_galaxy/tests/test_flm_workspace.py
+++ b/starpilot/system/the_galaxy/tests/test_flm_workspace.py
@@ -232,13 +232,23 @@ def test_segment_ranges_limit_resolved_route_sources(tmp_path, monkeypatch):
sources, warnings = module.resolve_route_sources(
[route],
[str(tmp_path)],
- {route: {"start": 4, "end": 9}},
+ {route: {"start": 4, "end": 8}},
)
- assert [source.segment_num for source in sources] == [4, 5, 6, 7, 8, 9]
+ assert [source.segment_num for source in sources] == [4, 5, 6, 7, 8]
assert warnings == []
+def test_segment_limit_rejects_more_than_five_selected_segments(tmp_path, monkeypatch):
+ module, _ = _load_flm_workspace_module(tmp_path)
+ route = "00000001--abcdef1234"
+ segment_names = [f"{route}--{segment}" for segment in range(12)]
+ monkeypatch.setattr(module.utilities, "get_segments_in_route", lambda *_args: segment_names)
+
+ with pytest.raises(ValueError, match="limited to 5 segments"):
+ module.enforce_segment_limit([route], [str(tmp_path)], {route: {"start": 4, "end": 9}})
+
+
def test_segment_range_rejects_reversed_bounds(tmp_path):
module, _ = _load_flm_workspace_module(tmp_path)
with pytest.raises(ValueError, match="first segment"):
diff --git a/starpilot/system/the_galaxy/tests/test_ui_vue_frontend.py b/starpilot/system/the_galaxy/tests/test_ui_vue_frontend.py
index 3964508e97..d01bdefe5b 100644
--- a/starpilot/system/the_galaxy/tests/test_ui_vue_frontend.py
+++ b/starpilot/system/the_galaxy/tests/test_ui_vue_frontend.py
@@ -494,6 +494,12 @@ def test_ui_all_remaining_classic_tools_native_no_embed():
"js/components/LateralTuningPanel.js"]:
assert "fetch(" not in _read(rel), f"{rel} should not use raw fetch()"
+ lateral = _read("js/components/LateralTuningPanel.js")
+ assert "MAX_SEGMENTS = 5" in lateral
+ assert "segmentRanges" in lateral and "selectedSegmentRanges" in lateral
+ assert "flmAnalyze(this.selectedRoutes, this.selectedSegmentRanges())" in lateral
+ assert "routeSelectedSegmentCount" in lateral
+
def test_ui_cameras_hub_vasm_and_pip_native_no_embed():
app = _read("js/app.js")
diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py
index 9f2176aa0d..289feb378a 100644
--- a/starpilot/system/the_galaxy/the_galaxy.py
+++ b/starpilot/system/the_galaxy/the_galaxy.py
@@ -8296,7 +8296,10 @@ def setup(app):
except (TypeError, ValueError) as error:
return jsonify({"error": str(error)}), 400
- started = flm_workspace.start_flm_background_analysis(route_names, FOOTAGE_PATHS, segment_ranges)
+ try:
+ started = flm_workspace.start_flm_background_analysis(route_names, FOOTAGE_PATHS, segment_ranges)
+ except (TypeError, ValueError) as error:
+ return jsonify({"error": str(error)}), 400
if not started:
return jsonify({"error": "Failed to start FLM analysis."}), 500
diff --git a/starpilot/system/the_galaxy/utilities.py b/starpilot/system/the_galaxy/utilities.py
index e24c1894da..0f603d855f 100644
--- a/starpilot/system/the_galaxy/utilities.py
+++ b/starpilot/system/the_galaxy/utilities.py
@@ -3260,6 +3260,7 @@ def process_route(footage_path, route_name, segment_count=0, first_segment_num=0
"isCustomName": custom_name is not None,
"is_preserved": has_preserve_attr(segment_path),
"segmentCount": max(0, int(segment_count)),
+ "firstSegmentNum": max(0, int(first_segment_num)),
"approxDurationSeconds": max(0, int(segment_count)) * 60,
}