817f830565
Phase 1 of the review asked for one directory per experiment, a force CSV, video of the pre-processing image, and metadata that ties them together. The record channel from the previous change already kept the whole session; this adds the on-disk format. What - Add record/experiment.py: ExperimentRecorder writes one directory per session containing session.json, forces.csv, frames.csv, events.csv, raw_NNNN.mp4 (segmented) and an optional lossless PNG sequence. - forces.csv uses the reviewed column order (session_id, frame_id, t_rel_s, FN_raw, FX_raw, FY_raw, FN_N, FX_N, FY_N, valid, calibration_id) and leaves a cell empty when a value is unavailable: an uncalibrated newton value or a failed readout is never written as 0. - frames.csv is the frame-index mapping: frame_id, t_rel_s, acquisition wall clock, segment, segment_frame, in_video. forces.csv and frames.csv share the packet frame_id and the same t_rel_s origin, so video and data can be aligned after dropped or segmented frames. - session.json carries format version, session id, start/end time in UTC, device and config (with a sha256 of the config file), app/shared/SDK versions, calibration reference, axis convention, filter parameters, video codec and the lossy-MP4 caveat, plus the event log and end-of-run statistics. It is written once at start so a crash still leaves identifiable metadata. - Add a record control to the toolbar: mode (video+data / data only) and a start/stop toggle that stays enabled while recording. - Log zeroing, calibration application and session end as timestamped events. Why - The recorder is fed from the worker sink list on the acquisition thread and only appends to a bounded queue, so disk I/O can never stall acquisition. - On_packet copies the raw frame instead of holding a reference: the packet frame may alias a buffer the SDK reuses between frames. - A queue overflow is counted and alarmed (rate limited), and a write or encode failure is counted, reported and after 5 consecutive errors stops the recording with failed=true rather than silently losing data. Verification - shared/tests/test_experiment_recorder.py: 12 cases covering the directory layout, the CSV contract, frame-id/time-base agreement, segment rolling, data-only and lossless modes, queue overflow accounting, encoder failure, metadata fields, crash-before-stop, and that on_packet never blocks. - tools/ui_check.py gains an "experiment record" section (169 checks, 0 failures overall), driving a recording through the UI and asserting the layout, the empty-cell rule and the event sequence. - apps/studio/tests/test_ui_smoke.py mirrors those invariants. - docs/record-channel.md documents the format and the remaining phase-2 work. Not in this change: CSV export of a selected time range, full-session curve browsing, calibration diagnostics (outlier exclusion, fit residuals) and PyOpenGL 3D.
187 lines
7.6 KiB
JSON
187 lines
7.6 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…",
|
||
"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": "—"
|
||
},
|
||
"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…", "sn": "SN",
|
||
"sn_placeholder": "Serial number (optional)",
|
||
"live_raw_label": "Live raw",
|
||
"live_raw": "Live raw FN={fn} FX={fx} FY={fy}",
|
||
"col_weight": "Weight (N)",
|
||
"col_raw": "Raw mean",
|
||
"col_n": "n",
|
||
"col_std": "σ",
|
||
"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_done": "Calibration applied to {sensor_type}",
|
||
"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}"
|
||
}
|
||
}
|