Files
orisys-app/apps/studio/config/locales/en_US.json
T
ykcheungab 22cc59c402 Fix the phase-1/2 review findings in recording, calibration and depth replay
What
- record: one packet->row conversion (record/sample.py). A failed compute chain
  records no force value at all, a newton value is written only for an axis that
  really has a calibration curve, and mono_t (not the wall clock) orders history
  with a per-row calibration hash.
- record lifecycle: explicit starting/recording/stopping/stopped/failed states
  owned by the writer thread; stop() reports completed=False on timeout and does
  not write ended_at; consecutive write failures stop acceptance immediately.
- record capacity: byte budget for the video queue (512 MiB default); a full
  queue sheds the *frame* and parks the tiny data row, so a video overflow no
  longer deletes CSV rows. Lost rows leave a dropped=1 index row and are counted
  as dropped_gaps / unrecorded_gaps.
- calibration identity: sha256 content hash instead of a path, minted from the
  calibration the sensor actually loaded (reported before the first frame) and
  confirmed by the acquisition thread; events are timestamped when they happen.
- calibration diagnostics: the fit summary, residual and validation error now
  come from the model that export/apply would use (piecewise_linear); validation
  points are held out of the build; repeated reference loads are merged with an
  n-weighted mean and pooled std.
- calibration record: every point keeps its full three-axis sample series
  (frame id, time, raw, newtons, valid) plus diagnostics, written to an
  independent calibration_session_*.json that can be reopened for inspection;
  save record / export coefficients / apply to device are three separate actions.
- depth replay: the cursor's frame is bound to the depth result by request
  sequence and frame id, so a moved cursor never shows another frame's map; the
  SDK is stepped from its current position instead of re-reading from the start;
  the live chain (box deformation+contact, finger unroll+tactile) is replayed
  with the exact resolved config path; algorithm state is rebuilt from a reset
  marker or from the recorded pre-roll, and says so when it cannot be.

Why
The review (D:/orisys_ws/_review_phase12/review.md) showed the app writing raw
counts into the newton columns, recording a stale value after a failed compute,
losing CSV rows when the video queue filled, reporting success while a writer was
still running, diagnosing a model nobody applies, applying a calibration before
the device confirmed it, dropping the samples behind a calibration point, showing
a stale depth map under a moved cursor and recomputing depth of the wrong frame.

Verification
- D:/orisys_ws/_review_phase12/reproduce.py: all ten acceptance lines flipped
  (log saved as _review_phase12/reproduce_after.log). Including 25/25 rows kept
  with 8 video frames shed, frames [2]->[3], depth 8.0/8.0, fit 2/2 points.
- apps/studio/tools/ui_check.py: 249 checks, 0 failures (twice).
- shared suites: 91 tests, 0 failures (new: test_record_semantics,
  test_record_lifecycle, test_calibration_model, test_calibration_record,
  test_depth_replay).
- apps/studio/tests/test_ui_smoke.py: 23/30; the 7 remaining are harness
  artifacts of the sandbox (4 tmp_path PermissionError, 3 parametrized tests the
  shim cannot drive), unchanged from before this change.
- compileall clean; locale parity zh/en + t() key coverage: 0 failures.

Not in this change
- P2#10 disk-based browsing of a long recording and range CSV export (next
  round, per your choice).
- Stability-triggered calibration sampling, replay fixed colour scale, PyOpenGL
  three-dimensional view.
- Hardware verification: no camera attached, so the SDK's real processing chain
  and the pre-roll's effect on a live tracker were exercised with fakes only.
2026-09-13 14:55:53 +08:00

296 lines
14 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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",
"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}",
"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_state_across_segment": "the algorithm state cannot be carried across a segment",
"warn_state_approximate": "the algorithm state is an approximation"
},
"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}",
"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_warnings": "Note: {detail}",
"open_record_failed": "Could not open the calibration record: {error}",
"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}",
"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})",
"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",
"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}"
}
}