73592f085f
What
- record/index.py: RecordingIndex streams forces.csv once and keeps only the
small things — one time, one validity flag, one frame id per row plus a byte
offset every 4096 rows (~6.5 MB per 500k rows). row_range() bisects the times,
read_rows() seeks to the nearest checkpoint and scans forward, and overview()
folds every row into its bucket during the same pass to build a per-axis
min/max envelope.
- read_rows(max_points, unit) decimates **while streaming**: each bucket keeps
the extreme row of every axis in the requested unit, so a long interval never
has to exist in memory in full and a shear spike survives a flat normal channel.
- RecordingReader: info() decimates above max_rows (default 200k) and marks
itself decimated, with total_rows and a duration taken from the frame index
rather than from the view; window()/window_all()/overview()/statistics() read
the interval being asked about. list/window semantics unchanged for small
recordings.
- force/source.py: StoreCurveSource (live memory) and RecordingCurveSource (disk
interval + cached envelope) implement one interface — extent, window,
envelope, statistics, duration_s — so ForcePlotsPanel asks a *source* instead
of knowing where the data lives.
- force_plots: reads through the source (window, envelope, statistics, extent),
which also replaces its last direct store accesses.
- playback_dialog: draws the whole-session view through window_all() in the unit
being shown (bounded by CHART_MAX_POINTS), and says so when the view is
decimated ("显示 N / 共 M 行") instead of letting the operator count points.
- experiment._run: a failure that lands while the files are opening can no longer
be overwritten by the writer's own transition to "recording" (found by a flaky
test; the state check now covers STATE_FAILED).
Why
The store releases old blocks above ~4.6 h and the replay loaded every CSV row
into Python lists before drawing anything, so neither the live browser nor the
replay could show an earlier interval of a long recording. The index makes the
disk the data source and memory a cache of what is on screen.
Verification
- shared/tests/test_history_source.py (new, 8 cases) over a 12 000-row recording:
the index reports its rows and reads a bounded interval; a large recording is
marked decimated with its true duration and total rows; a small one is not; an
interval read sees the exact rows including a single peak; a decimated window
keeps the peak; the envelope fits the whole session in ≤200 points, keeps the
spike and is cached; the disk and memory sources agree on extent, values and
statistics; the whole session can be read without holding it all. Shared
suites: 146 tests, 0 failures (three consecutive runs).
- apps/studio/tools/ui_check.py: the replay section now checks the index row
count, that an interval read spans only the interval, the envelope size and
total_rows. 324 checks, 0 failures.
- apps/studio/tests/test_ui_smoke.py: 43 tests, 36 pass; the 7 failures are the
unchanged sandbox/harness artifacts.
- D:/orisys_ws/_review_phase12/reproduce.py: all ten lines unchanged;
compileall, locale and prefs parity clean.
- apps/studio/docs/record-channel.md §8 documents the index, the reader API and
the unified source, and lists what is still hardware work.
Not in this change
- Maximum RSS / backlog / loss measurements at real resolution on the target ARM
board (needs the hardware), and range CSV export.
314 lines
16 KiB
JSON
314 lines
16 KiB
JSON
{
|
||
"app": {
|
||
"title": "Orisys Studio",
|
||
"about_title": "About",
|
||
"about_body": "Orisys Studio desktop application\nVersion: {app_version}",
|
||
"scaffold_hint": "Scaffold shell — acquisition coming later"
|
||
},
|
||
"brand": {
|
||
"name": "ORISYS",
|
||
"tagline": "DieDong Technology"
|
||
},
|
||
"menu": {
|
||
"file": "File",
|
||
"open_video": "Open video…",
|
||
"reveal_config": "Reveal sensor config…",
|
||
"playback": "Open recording replay…",
|
||
"playback_latest": "Replay most recent recording",
|
||
"view": "View",
|
||
"theme": "Theme",
|
||
"theme_dark": "Dark",
|
||
"theme_light": "Light",
|
||
"language": "Language",
|
||
"lang_zh": "简体中文",
|
||
"lang_en": "English",
|
||
"calibration_panel": "Calibration panel",
|
||
"finger_3d": "Finger 3D view",
|
||
"show_fps": "Show FPS",
|
||
"plot_settings": "Plot settings…",
|
||
"rotate_arrows": "Rotate arrows 90°",
|
||
"run": "Run / Stop",
|
||
"stop": "Cancel acquisition",
|
||
"preview": "Preview / Stop preview",
|
||
"reset": "Reset tracker",
|
||
"help": "Help",
|
||
"about": "About"
|
||
},
|
||
"dialog": {
|
||
"plot_settings_title": "Plot settings",
|
||
"plot_window": "Time window",
|
||
"plot_window_tip": "Force plot time window (seconds)",
|
||
"plot_fn_max": "FN max",
|
||
"plot_fn_max_tip": "Normal force axis max (Newton mode)",
|
||
"plot_shear_max": "Shear max",
|
||
"plot_shear_max_tip": "Shear force axis ±max for FX/FY (Newton mode)",
|
||
"plot_fn_raw_max": "FN raw max",
|
||
"plot_fn_raw_max_tip": "Normal-force axis max in raw (uncalibrated) mode. 0 = auto range on the data; the default is sized so idle jitter is invisible in the plot",
|
||
"plot_shear_raw_max": "Shear raw max",
|
||
"plot_shear_raw_max_tip": "Shear-force axis ±max for FX/FY in raw (uncalibrated) mode. 0 = auto range",
|
||
"reset_defaults": "Reset to defaults",
|
||
"ok": "OK",
|
||
"cancel": "Cancel"
|
||
},
|
||
"toolbar": {
|
||
"source": "Source",
|
||
"source_camera": "Camera",
|
||
"source_video": "Video file",
|
||
"search_cameras": "Search",
|
||
"cam_id": "Cam ID",
|
||
"browse_video": "Browse…",
|
||
"video_placeholder": "No video selected",
|
||
"preview": "Preview",
|
||
"preview_stop": "Stop preview",
|
||
"run": "Run",
|
||
"stop": "Stop",
|
||
"run_label": "▶ Run",
|
||
"stop_label": "■ Stop",
|
||
"cancel_label": "■ Cancel",
|
||
"cancel_tip": "Abort the connection (always available while connecting, shortcut Esc)",
|
||
"run_unavailable": "Not available yet",
|
||
"run_disabled_video": "Select a video file first",
|
||
"run_disabled_sensor": "Run is not available for this sensor type",
|
||
"reset": "Reset",
|
||
"units": "Units",
|
||
"units_newton": "Newton",
|
||
"units_raw": "Raw"
|
||
},
|
||
"units": {
|
||
"newton_short": "N",
|
||
"raw_short": "raw"
|
||
},
|
||
"sensor": {
|
||
"type": "Product",
|
||
"config": "Config",
|
||
"thumb_placeholder": "Product model"
|
||
},
|
||
"panels": {
|
||
"arrow": "2D arrows",
|
||
"view_arrows_pure": "Arrows",
|
||
"view_arrows_stitched": "Arrows / Captured",
|
||
"view_depth": "Depth map",
|
||
"view_mode_tip": "Image display mode while running",
|
||
"arrow_empty": "No image yet\nPick a video or camera, then press Run",
|
||
"arrow_empty_tip": "The deformation arrow image appears here while running or previewing",
|
||
"depth_unavailable": "No depth data for this frame\n(not a depth of zero, and no silent fallback to the processed image)",
|
||
"depth_scale": "Depth scale {low} – {high}",
|
||
"depth_scale_tip": "Fixed for this session: the range is latched to the first frame that carried depth so frames stay comparable. Grey marks samples with no depth.",
|
||
"plots": "Force Charts",
|
||
"cal": "Calibration",
|
||
"mesh3d": "3D mesh",
|
||
"mesh3d_empty": "Finger 3D view placeholder —\nOpen3D mesh coming later",
|
||
"mesh3d_empty_tip": "3D mesh preview for finger sensors"
|
||
},
|
||
"chart": {
|
||
"fn": "FN",
|
||
"fx": "FX",
|
||
"fy": "FY",
|
||
"no_data": "—",
|
||
"mode_follow": "Following live",
|
||
"mode_browse": "Browsing history (display paused; capture and recording continue)",
|
||
"return_latest": "Back to latest",
|
||
"return_latest_tip": "Leave history browsing and follow the newest data again",
|
||
"overview": "Whole session",
|
||
"overview_tip": "Fit the entire session; drag the overview strip to move the window",
|
||
"box_zoom": "Box zoom",
|
||
"box_zoom_tip": "Switch to rubber-band zoom (drag a region on the chart)",
|
||
"selection": "Selection {duration}s / {samples} samples — {detail}",
|
||
"selection_empty": "No valid samples in the selection",
|
||
"value_unavailable": "n/a",
|
||
"value_unavailable_tip": "No curve is loaded for {channel}, so it has no value in this unit (raw counts are never shown in its place)",
|
||
"browse_only_tip": "Drag or scroll on the chart to browse history"
|
||
},
|
||
"layout": {
|
||
"default": "Free layout",
|
||
"preset_2d_name": "2D mode (depth + charts)",
|
||
"preset_2d": "2D mode: depth map on the left, force charts on the right, one shared time axis",
|
||
"preset_default": "Free layout restored"
|
||
},
|
||
"playback": {
|
||
"title": "Replay recording",
|
||
"open": "Open recording directory…",
|
||
"open_failed": "Could not open that recording: {error}",
|
||
"none_found": "No recording found under outputs/recordings",
|
||
"loaded": "{name} — {frames} frames / {duration} s",
|
||
"empty": "Pick a recording directory to start replay",
|
||
"position": "{position} / {duration} s",
|
||
"show_newton": "Show newtons",
|
||
"show_raw": "Show raw",
|
||
"recompute_depth": "Recompute depth",
|
||
"recompute_depth_tip": "Depth is not stored with a recording; recompute this frame offline from the video and the config",
|
||
"frame_dropped": "This frame never reached the video (the record queue overflowed)",
|
||
"frame_unreadable": "This frame could not be decoded from the video",
|
||
"depth_hint": "Depth is not stored: press Recompute depth to rebuild it from the video and the config",
|
||
"depth_running": "Recomputing depth… (on a worker thread)",
|
||
"depth_busy": "The previous recomputation is still running",
|
||
"depth_empty": "No depth data for this frame",
|
||
"depth_failed": "Depth recomputation failed: {detail}",
|
||
"depth_sdk_unavailable": "Cannot recompute: the SDK is not available",
|
||
"depth_config_missing": "Cannot recompute: the recording has no sensor config reference",
|
||
"depth_config_file_missing": "Cannot recompute: the config file no longer exists at that path",
|
||
"depth_no_video": "Cannot recompute: this recording has no video segment",
|
||
"depth_not_recomputed": "Depth has not been recomputed for this cursor position\n(the previous frame's map is not reused)",
|
||
"depth_recomputing": "Recomputing depth for the current cursor position…",
|
||
"depth_frame_not_recorded": "This frame was not recorded: its data row was lost to a full queue and only the index row remains",
|
||
"depth_frame_not_in_video": "This frame never reached the video, so its depth cannot be recomputed",
|
||
"depth_state_across_segment": "Replay cannot carry the algorithm state across segments: this one restarts at its first frame",
|
||
"depth_state_approximate": "No reset or pre-roll history for this segment: the algorithm state is an approximation",
|
||
"depth_warnings": "Recomputation notes: {detail}",
|
||
"warnings": "Replay notes: {detail}",
|
||
"decimated": "This recording is long, so the curves are a decimated view ({shown} of {total} rows; intervals are read from disk on demand)",
|
||
"warn_config_changed": "the config differs from recording time, so the recomputed depth may differ",
|
||
"warn_sdk_changed": "the SDK version differs from recording time, so the recomputation may differ",
|
||
"warn_config_missing": "no config hash was recorded, so recomputation cannot be verified",
|
||
"warn_depth_recomputed": "depth comes from offline recomputation, not from stored data",
|
||
"warn_rows_skipped": "some unparsable CSV rows were skipped",
|
||
"warn_frames_dropped": "frames were lost to a full queue; only their index rows remain (shown as \"not recorded\")",
|
||
"warn_out_of_order": "this recording was written by an older version: its rows are not in time order, so the replay re-sorts them and window edges may differ",
|
||
"warn_state_across_segment": "the algorithm state cannot be carried across a segment",
|
||
"warn_state_approximate": "the algorithm state is an approximation",
|
||
"warn_state_approximate_after_rewind": "the video was reopened to reach an earlier frame, so the state may differ",
|
||
"warn_state_too_far_back": "the zeroing is too far back: reading there would exceed the replay budget, so the state is approximate",
|
||
"warn_reset_frame_not_indexed": "the zeroing frame is not in the frame index, so the state cannot be rebuilt from it",
|
||
"warn_reset_frame_not_in_video": "the zeroing frame never reached the video, so the state cannot be rebuilt from it",
|
||
"warn_reset_not_applied": "the replay could not re-apply the zeroing, so the state may differ"
|
||
},
|
||
"record": {
|
||
"label": "Record",
|
||
"mode_video_data": "Video + data",
|
||
"mode_data_only": "Data only",
|
||
"mode_tip": "Video records the pre-processing camera input (no arrows or text overlays). MP4 is a lossy container, not a lossless copy of the raw data.",
|
||
"start_label": "● Start recording",
|
||
"stop_label": "■ Stop recording",
|
||
"start_tip": "Record this experiment: video, force data and metadata into one directory",
|
||
"stop_tip": "Stop recording and write session.json (always available)",
|
||
"requires_run": "Press Run to start acquisition before recording",
|
||
"started": "Recording started ({mode})",
|
||
"start_failed": "Could not start recording: {error}",
|
||
"finished": "Recording finished — {frames} frames / {segments} segment(s) in {path}",
|
||
"finished_with_problems": "Recording finished with problems: {detail}",
|
||
"closing": "Closing the recording files…",
|
||
"failed_state": "The recording failed (see the summary and the log)",
|
||
"alarm": "Recording problem: {detail}"
|
||
},
|
||
"cal": {
|
||
"weight": "Weight (N)",
|
||
"duration": "Record duration",
|
||
"record": "Record",
|
||
"recording_stop": "Stop",
|
||
"record_disabled": "Press Run to start acquisition before recording samples",
|
||
"record_cancelled": "Recording cancelled",
|
||
"empty_hint": "No samples for this channel yet: run, pick a channel and press Record.",
|
||
"delete_row": "Delete row",
|
||
"clear_channel": "Clear channel",
|
||
"clear_all": "Clear all",
|
||
"export": "Export coefficients…",
|
||
"export_tip": "Write the SDK coefficient file only; the running device is untouched",
|
||
"apply": "Apply to device",
|
||
"apply_tip": "Push the last export to the running device (confirmed again)",
|
||
"apply_no_export": "No exported coefficients yet — export them first",
|
||
"save_record": "Save record…",
|
||
"save_record_tip": "Save every point of this session with its full three-axis samples (audit data), independently of the coefficient file",
|
||
"save_record_empty": "No calibration points to save yet",
|
||
"save_record_ok": "Saved the calibration record to {path}\n{points} point(s), {samples} three-axis samples",
|
||
"save_record_failed": "Could not save the calibration record: {error}",
|
||
"open_record": "Open record…",
|
||
"open_record_tip": "Reopen a saved calibration session to inspect every point's samples and diagnostics",
|
||
"open_record_filter": "Calibration records (*.json)",
|
||
"open_record_ok": "Loaded {path} ({points} point(s))",
|
||
"open_record_device": "Recorded on: {device}",
|
||
"open_record_warnings": "Note: {detail}",
|
||
"open_record_failed": "Could not open the calibration record: {error}",
|
||
"apply_mismatch_title": "Calibration does not match the target device",
|
||
"apply_mismatch": "This coefficient file belongs to: {device}\nMismatch: {detail}\n\nCurrent target: {sensor_type} / {config}\n\nApplying it would load coefficients that were not measured on this device.",
|
||
"apply_mismatch_skipped": "Apply cancelled (the calibration belongs to another device)",
|
||
"sn": "SN",
|
||
"sn_placeholder": "Serial number (optional)",
|
||
"live_raw_label": "Live raw",
|
||
"live_raw": "Live raw FN={fn} FX={fx} FY={fy}",
|
||
"col_weight": "Reference load",
|
||
"col_fn": "FN mean",
|
||
"col_fx": "FX mean",
|
||
"col_fy": "FY mean",
|
||
"col_stability": "Stability",
|
||
"col_state": "State",
|
||
"col_raw": "Raw mean",
|
||
"col_n": "n",
|
||
"col_std": "σ",
|
||
"state_ok": "OK",
|
||
"state_excluded": "Excluded",
|
||
"state_validation": "Validation",
|
||
"state_exclude": "Exclude",
|
||
"state_exclude_tip": "Drop the selected point from the fit and the export (kept on record, reversible)",
|
||
"state_restore": "Restore",
|
||
"mark_validation": "Mark validation",
|
||
"mark_validation_tip": "Hold the selected point out of the fit and use it to test the fit",
|
||
"excluded": "Point excluded from the fit",
|
||
"restored": "Point restored",
|
||
"marked_validation": "Point marked as a validation point",
|
||
"unmarked_validation": "Validation mark removed",
|
||
"fit_insufficient": "A fit needs at least 2 usable points (currently {count})",
|
||
"fit_summary": "Exported model {channel}: {method} ({count} points / {breakpoints} breakpoints, max residual {residual} N)",
|
||
"fit_validation": "Independent validation on {count} point(s), max deviation {error} N",
|
||
"method_piecewise_linear": "piecewise linear",
|
||
"method_linear": "linear",
|
||
"point_title": "Calibration point {channel} @ {load} N",
|
||
"point_channel": "Loaded axis",
|
||
"point_direction": "Direction",
|
||
"point_round": "Round",
|
||
"point_duration": "Actual sampling time",
|
||
"point_duration_value": "{actual} s (requested {requested} s)",
|
||
"point_samples": "Samples",
|
||
"point_samples_value": "{valid} valid / {total} total",
|
||
"point_axes": "Three-axis mean ± std",
|
||
"point_residual": "Fit residual",
|
||
"col_actions": "",
|
||
"recording": "Recording calibration samples…",
|
||
"recording_stop_tip": "Click to cancel this calibration recording (always available)",
|
||
"record_ok": "Recorded weight={weight} raw={raw}",
|
||
"record_failed_few": "Recording failed — too few samples",
|
||
"export_ok": "Exported {path}",
|
||
"export_local": "Also wrote {path}",
|
||
"export_warnings": "Skipped: {detail}",
|
||
"export_failed": "Export failed: {error}",
|
||
"export_no_model": "Nothing to export: {detail}",
|
||
"apply_title": "Apply calibration",
|
||
"apply_confirm": "Apply the calibration just exported to the current device?\n\nSensor type: {sensor_type}\nConfig: {config}\n\nChoosing No keeps the exported file and leaves the running device untouched.",
|
||
"apply_skipped": "Exported; not applied to the device",
|
||
"apply_pending": "Requested calibration for {sensor_type}; waiting for the device to confirm…",
|
||
"applied": "Device confirmed the calibration is in effect (version {version})",
|
||
"applied_none": "Device confirmed: no calibration is loaded (the column stays empty)",
|
||
"export_only": "Exported coefficients to {path} (not applied to the device)",
|
||
"apply_no_target": "No device to apply to (run an acquisition first)"
|
||
},
|
||
"status": {
|
||
"ready": "Ready",
|
||
"phase_idle": "Idle",
|
||
"phase_connecting": "Connecting",
|
||
"phase_running": "Running",
|
||
"phase_error": "Error",
|
||
"camera_none": "No cameras found",
|
||
"camera_found": "Found {count} camera(s): {names}",
|
||
"preview_started": "Preview started (camera {camera_id})",
|
||
"preview_stopped": "Preview stopped",
|
||
"preview_failed": "Preview failed: {error}",
|
||
"connecting": "Connecting…",
|
||
"connect_slow": "This is taking a while — click “Cancel” or press Esc to abort",
|
||
"run_started": "Running ({source})",
|
||
"run_stopped": "Stopped",
|
||
"run_stopping": "Stopping… (releasing the device)",
|
||
"run_stopping_cancelled": "Connection cancelled (releasing the device)",
|
||
"sample_invalid": "Invalid sample (#{count}): {reason} — marked invalid and excluded from the curves and from calibration",
|
||
"samples_dropped": "Record buffer is full; the oldest {count} samples were released (the full session is no longer complete)",
|
||
"run_failed": "Run failed: {error}",
|
||
"video_ended": "Video ended",
|
||
"camera_disconnected": "Camera disconnected",
|
||
"reset_ok": "Tracker reset",
|
||
"reset_pending": "Zeroing requested; waiting for the acquisition thread to confirm…",
|
||
"finger_run_unavailable": "Run is not available for this sensor type",
|
||
"video_required": "Select a video file first",
|
||
"config_missing": "Sensor config not found for {type}",
|
||
"units_raw_fallback": "Calibration unavailable — showing raw units",
|
||
"reveal_failed": "Could not reveal config: {error}"
|
||
}
|
||
}
|