hal-api-docs / openapi.json
leo
HAL API docs with Swagger UI
64648e8
Raw
History Blame
91.2 kB
{"openapi":"3.1.0","info":{"title":"HAL Hardware Runtime","description":"Hardware driver API for the OS. Controls servo motors (5-axis Feetech), RGB LEDs (64x WS2812), camera, audio (mic/speaker), display, and AI voice pipeline. OS Server (Go, port 5000) bridges requests here.","version":"0.0.824"},"servers":[{"url":"/api/hardware","description":"Via OS server admin proxy (browser)"},{"url":"/","description":"Direct (loopback / SSH tunnel)"}],"paths":{"/audio":{"get":{"tags":["Audio"],"summary":"Get Audio Info","description":"Get audio device availability.","operationId":"get_audio_info_audio_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AudioDevicesResponse"}}}}}}},"/audio/volume":{"get":{"tags":["Audio"],"summary":"Get Volume","description":"Get current speaker volume from amixer.\n\nReads back through the same envelope `set_volume` writes through:\n - DAC controls -> parse [X.XdB] and map back via _db_to_pct\n - everything else -> parse [NN%] directly\nDAC controls are tried first so round-trip is stable on codecs whose raw%\nrange differs from our [-60dB, +2dB] envelope (e.g. WM8960, Rockchip).","operationId":"get_volume_audio_volume_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VolumeResponse"}}}}}},"post":{"tags":["Audio"],"summary":"Set Volume","description":"Set system speaker volume (0-100%). Routes to the BT headset sink\n(PulseAudio) when one is active, else to the built-in speaker (amixer).","operationId":"set_volume_audio_volume_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VolumeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio/play-tone":{"post":{"tags":["Audio"],"summary":"Play Tone","description":"Play a test tone through the speaker.","operationId":"play_tone_audio_play_tone_post","parameters":[{"name":"frequency","in":"query","required":false,"schema":{"type":"integer","default":440,"title":"Frequency"}},{"name":"duration_ms","in":"query","required":false,"schema":{"type":"integer","default":500,"title":"Duration Ms"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio/record":{"post":{"tags":["Audio"],"summary":"Record Audio","description":"Record audio from the microphone. Returns WAV bytes.","operationId":"record_audio_audio_record_post","parameters":[{"name":"duration_ms","in":"query","required":false,"schema":{"type":"integer","default":3000,"title":"Duration Ms"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bluetooth/available":{"get":{"tags":["Bluetooth"],"summary":"Bt Available","description":"Tell the UI whether bluetoothctl works on this host.","operationId":"bt_available_bluetooth_available_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/bluetooth/scan/start":{"post":{"tags":["Bluetooth"],"summary":"Bt Scan Start","operationId":"bt_scan_start_bluetooth_scan_start_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/bluetooth/scan/results":{"get":{"tags":["Bluetooth"],"summary":"Bt Scan Results","operationId":"bt_scan_results_bluetooth_scan_results_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/bluetooth/devices":{"get":{"tags":["Bluetooth"],"summary":"Bt Devices","operationId":"bt_devices_bluetooth_devices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/bluetooth/pair":{"post":{"tags":["Bluetooth"],"summary":"Bt Pair","operationId":"bt_pair_bluetooth_pair_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MacRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bluetooth/forget":{"post":{"tags":["Bluetooth"],"summary":"Bt Forget","operationId":"bt_forget_bluetooth_forget_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MacRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bluetooth/active":{"get":{"tags":["Bluetooth"],"summary":"Bt Active Get","operationId":"bt_active_get_bluetooth_active_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["Bluetooth"],"summary":"Bt Active Set","description":"Toggle voice routing.\n\nmac = null / \"\" → route back to the device's built-in speaker + mic.\nmac = MAC → ensure connected, find PortAudio indices, route TTS+STT\n to the BT device. STT mic falls back to the device mic if the\n device has no input (BT speaker case).","operationId":"bt_active_set_bluetooth_active_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/bluetooth/status":{"get":{"tags":["Bluetooth"],"summary":"Bt Status","description":"Snapshot for the monitor card — what's active, what's around.","operationId":"bt_status_bluetooth_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/camera":{"get":{"tags":["Camera"],"summary":"Get Camera Info","description":"Get camera availability, negotiated resolution, FPS, zoom.","operationId":"get_camera_info_camera_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraInfoResponse"}}}}}}},"/camera/zoom":{"post":{"tags":["Camera"],"summary":"Set Camera Zoom","description":"Set digital zoom factor (1.0 = no zoom, applies to all frame consumers).\n\nSide effect: zoom > 1 narrows the FOV seen by sensing (face recog, motion,\npose, emotion) and tracking. Use for focusing on a small subject (e.g.\nlaptop screen in a video call); set back to 1.0 to restore wide view.","operationId":"set_camera_zoom_camera_zoom_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraZoomRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraInfoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/camera/disable":{"post":{"tags":["Camera"],"summary":"Disable Camera","description":"Stop the camera capture loop (manual). Sets manual override.","operationId":"disable_camera_camera_disable_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/camera/enable":{"post":{"tags":["Camera"],"summary":"Enable Camera","description":"Restart the camera capture loop (manual). Clears manual override.","operationId":"enable_camera_camera_enable_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/camera/snapshot":{"get":{"tags":["Camera"],"summary":"Camera Snapshot","description":"Capture a single JPEG frame from the camera (freezes servos for stability).\n\nOptional resize: pass width and/or height to downscale the output. Aspect\nratio is preserved; if both given, the frame is fit inside the requested\nbox. Upscaling above source is not allowed (just blurs without detail) —\nrequests above source are clamped.","operationId":"camera_snapshot_camera_snapshot_get","parameters":[{"name":"save","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Save"}},{"name":"width","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":4096,"minimum":1},{"type":"null"}],"description":"Resize output width (preserves aspect ratio). Capped at source width — never upscales.","title":"Width"},"description":"Resize output width (preserves aspect ratio). Capped at source width — never upscales."},{"name":"height","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":4096,"minimum":1},{"type":"null"}],"description":"Resize output height (preserves aspect ratio). Capped at source height — never upscales.","title":"Height"},"description":"Resize output height (preserves aspect ratio). Capped at source height — never upscales."},{"name":"quality","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"JPEG quality 1-100.","default":85,"title":"Quality"},"description":"JPEG quality 1-100."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/camera/stream":{"get":{"tags":["Camera"],"summary":"Camera Stream","description":"MJPEG stream from the camera.","operationId":"camera_stream_camera_stream_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/emotion/status":{"get":{"tags":["Emotion"],"summary":"Emotion Status","description":"Return current emotion state.","operationId":"emotion_status_emotion_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/emotion/presets":{"get":{"tags":["Emotion"],"summary":"List Emotion Presets","description":"Return all available emotion presets with their LED color and effect.","operationId":"list_emotion_presets_emotion_presets_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/emotion":{"post":{"tags":["Emotion"],"summary":"Express Emotion","description":"Express an emotion by coordinating servo animation + LED color simultaneously.","operationId":"express_emotion_emotion_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmotionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmotionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/led":{"get":{"tags":["LED"],"summary":"Get Led State","description":"Get LED strip info.","operationId":"get_led_state_led_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LEDStateResponse"}}}}}}},"/led/color":{"get":{"tags":["LED"],"summary":"Get Led Color","description":"Get current LED state: actual pixel color read from strip, effect, scene, brightness.","operationId":"get_led_color_led_color_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LEDColorResponse"}}}}}}},"/led/solid":{"post":{"tags":["LED"],"summary":"Set Led Solid","description":"Fill entire LED strip with a single color.","operationId":"set_led_solid_led_solid_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LEDSolidRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/led/paint":{"post":{"tags":["LED"],"summary":"Set Led Paint","description":"Set individual pixel colors (multi-color fills, gradients).","operationId":"set_led_paint_led_paint_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LEDPaintRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/led/off":{"post":{"tags":["LED"],"summary":"Turn Off Leds","description":"Turn off all LEDs.","operationId":"turn_off_leds_led_off_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/LEDOffRequest"},{"type":"null"}],"title":"Req"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/led/effect":{"post":{"tags":["LED"],"summary":"Start Led Effect","description":"Start a LED effect (breathing, candle, rainbow, notification_flash, pulse).","operationId":"start_led_effect_led_effect_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LEDEffectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LEDEffectResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/led/status":{"post":{"tags":["LED"],"summary":"Set Led Status","description":"Apply an os-server status state (booting/error/ota/…) by NAME. The os-server\nowns the status state machine (WHEN); HAL owns the appearance (WHAT) via\nSTATUS_LED_PRESETS, overridable per device in presets.json. Applied transiently\nthrough the normal effect path, so it never clobbers the user's saved LED state.","operationId":"set_led_status_led_status_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LEDStatusRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LEDEffectResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/led/restore":{"post":{"tags":["LED"],"summary":"Restore Led","description":"Restore the strip to the user's saved LED state.\n\nUsed by Buddy (and other transient drivers) after they release the\nstrip. If no user state exists, the strip is cleared to off so the\ntransient color/effect doesn't linger.","operationId":"restore_led_led_restore_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/led/effect/stop":{"post":{"tags":["LED"],"summary":"Stop Led Effect","description":"Stop the currently running LED effect.","operationId":"stop_led_effect_led_effect_stop_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/audio/play":{"post":{"tags":["Audio"],"summary":"Audio Play","description":"Search YouTube and play audio through the speaker.","operationId":"audio_play_audio_play_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MusicPlayRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audio/stop":{"post":{"tags":["Audio"],"summary":"Audio Stop","description":"Stop current music playback.","operationId":"audio_stop_audio_stop_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/speaker/mute":{"post":{"tags":["Audio"],"summary":"Mute Speaker","description":"Mute all audio output -- TTS, music, backchannel suppressed.","operationId":"mute_speaker_speaker_mute_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/speaker/unmute":{"post":{"tags":["Audio"],"summary":"Unmute Speaker","description":"Unmute audio output.","operationId":"unmute_speaker_speaker_unmute_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/audio/status":{"get":{"tags":["Audio"],"summary":"Audio Status","description":"Get music playback status.","operationId":"audio_status_audio_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MusicStatusResponse"}}}}}}},"/audio/history":{"get":{"tags":["Audio"],"summary":"Audio History","description":"Return music playback history for AI to learn user preferences.","operationId":"audio_history_audio_history_get","parameters":[{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date"}},{"name":"person","in":"query","required":false,"schema":{"type":"string","default":"","title":"Person"}},{"name":"last","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Last"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/scene":{"get":{"tags":["Scene"],"summary":"List Scenes","description":"List all available lighting scene presets.","operationId":"list_scenes_scene_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneListResponse"}}}}}},"post":{"tags":["Scene"],"summary":"Activate Scene","description":"Activate a lighting scene preset.","operationId":"activate_scene_scene_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SceneResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/scene/off":{"post":{"tags":["Scene"],"summary":"Deactivate Scene","description":"Deactivate current scene and return to idle state.\n\nReverses ALL peripheral changes made by scene activation:\nservo hold, camera, mic, speaker, LED.","operationId":"deactivate_scene_scene_off_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/sensing":{"get":{"tags":["Sensing"],"summary":"Get Sensing State","description":"Get perception state.","operationId":"get_sensing_state_sensing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensingResponse"}}}}}}},"/sensing/pose-snapshot":{"get":{"tags":["Sensing"],"summary":"Get Pose Snapshot","description":"Return the most recent annotated pose frame as JPEG.\n\nWalks every bucket dir and picks the newest .jpg file regardless of\nbucket. Prefer /sensing/pose-snapshot/{ts} when you have a specific\nsample timestamp (e.g. clicking a row in the monitor table).","operationId":"get_pose_snapshot_sensing_pose_snapshot_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/sensing/pose-snapshot/{ts}":{"get":{"tags":["Sensing"],"summary":"Get Pose Snapshot At","description":"Return the annotated pose frame for a specific sample timestamp.\n\n`ts` is int(unix-seconds) — matches int(sample.ts). Scans buckets/*\nfor `<ts>_<score>.jpg`. 404 when the file has been pruned (ephemeral\nbucket dropped at window close, or kept bucket aged past retention).","operationId":"get_pose_snapshot_at_sensing_pose_snapshot__ts__get","parameters":[{"name":"ts","in":"path","required":true,"schema":{"type":"integer","title":"Ts"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sensing/pose-bucket/{bucket_id}":{"get":{"tags":["Sensing"],"summary":"Get Pose Bucket","description":"Return the bucket.json manifest for a kept pose window. Used by the\nFlow Monitor turn card popup to render the full sample table without\nre-fetching `/sensing` (which only carries the live window).","operationId":"get_pose_bucket_sensing_pose_bucket__bucket_id__get","parameters":[{"name":"bucket_id","in":"path","required":true,"schema":{"type":"string","title":"Bucket Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sensing/pose-bucket/{bucket_id}/img/{filename}":{"get":{"tags":["Sensing"],"summary":"Get Pose Bucket Image","description":"Serve a single annotated frame from a kept bucket. Filename comes\nfrom bucket.json (`samples[].filename` or `worst_snapshots[]`).","operationId":"get_pose_bucket_image_sensing_pose_bucket__bucket_id__img__filename__get","parameters":[{"name":"bucket_id","in":"path","required":true,"schema":{"type":"string","title":"Bucket Id"}},{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/presence":{"get":{"tags":["Presence"],"summary":"Get Presence","description":"Get current presence state.","operationId":"get_presence_presence_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PresenceResponse"}}}}}}},"/presence/enable":{"post":{"tags":["Presence"],"summary":"Enable Presence","description":"Enable automatic presence-based light control.","operationId":"enable_presence_presence_enable_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/presence/disable":{"post":{"tags":["Presence"],"summary":"Disable Presence","description":"Disable automatic presence-based light control.","operationId":"disable_presence_presence_disable_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/face/enroll":{"post":{"tags":["Face"],"summary":"Face Enroll","description":"Save a JPEG photo, train embeddings, and persist under users/{label}/.","operationId":"face_enroll_face_enroll_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaceEnrollRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaceEnrollResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/face/status":{"get":{"tags":["Face"],"summary":"Face Status","description":"List enrolled persons and count.","operationId":"face_status_face_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaceStatusResponse"}}}}}}},"/face/owners":{"get":{"tags":["Face"],"summary":"Face Owners Detail","description":"List enrolled persons with photo filenames.","operationId":"face_owners_detail_face_owners_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaceOwnersDetailResponse"}}}}}}},"/face/photo/{label}/{filename}":{"get":{"tags":["Face"],"summary":"Face Photo","description":"Serve an owner photo as JPEG.","operationId":"face_photo_face_photo__label___filename__get","parameters":[{"name":"label","in":"path","required":true,"schema":{"type":"string","title":"Label"}},{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/face/file/{label}/{filepath}":{"get":{"tags":["Face"],"summary":"Face File","description":"Serve any text file from a user's directory.","operationId":"face_file_face_file__label___filepath__get","parameters":[{"name":"label","in":"path","required":true,"schema":{"type":"string","title":"Label"}},{"name":"filepath","in":"path","required":true,"schema":{"type":"string","title":"Filepath"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/face/remove":{"post":{"tags":["Face"],"summary":"Face Remove","description":"Remove one person's saved photos and re-train from disk.","operationId":"face_remove_face_remove_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaceRemoveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaceRemoveResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/face/photo/remove":{"post":{"tags":["Face"],"summary":"Face Photo Remove","description":"Remove a single photo from a person and re-train.","operationId":"face_photo_remove_face_photo_remove_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacePhotoRemoveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/face/reset":{"post":{"tags":["Face"],"summary":"Face Reset","description":"Clear all enrolled embeddings and delete all photos on disk.","operationId":"face_reset_face_reset_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaceResetResponse"}}}}}}},"/users/rename":{"post":{"tags":["User"],"summary":"User Rename","description":"Rename a per-user folder. All face / voice / mood / wellbeing data\nlives under the label folder, so this is a single fs rename.\n\nValidation:\n- new_label must normalize cleanly and be non-empty.\n- new_label must not collide with an existing folder.\n- old folder must exist.","operationId":"user_rename_users_rename_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRenameRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/face/stranger-stats":{"get":{"tags":["Face"],"summary":"Face Stranger Stats","description":"Return visit counts for all tracked stranger IDs.","operationId":"face_stranger_stats_face_stranger_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/face/cooldowns":{"get":{"tags":["Face"],"summary":"Face Cooldowns","description":"Return current cooldown state for all tracked persons.","operationId":"face_cooldowns_face_cooldowns_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/face/current-user":{"get":{"tags":["Face"],"summary":"Face Current User","description":"Return who HAL considers \"in front of the device\" right now.\n\nFriend with the newest session_start still within the forget window,\nelse \"unknown\" when only strangers are present, else empty string.\nDedicated endpoint so callers don't have to pull the whole cooldown\npayload just to get one field.","operationId":"face_current_user_face_current_user_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/face/cooldowns/reset":{"post":{"tags":["Face"],"summary":"Face Cooldowns Reset","description":"Reset all face recognition cooldown timers.","operationId":"face_cooldowns_reset_face_cooldowns_reset_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/user/info":{"get":{"tags":["User"],"summary":"User Info","description":"Get basic user info: name, is_friend, telegram identity.","operationId":"user_info_user_info_get","parameters":[{"name":"name","in":"query","required":false,"schema":{"type":"string","default":"","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/servo":{"get":{"tags":["Servo"],"summary":"Get Servo State","description":"Get available recordings and current animation state.","operationId":"get_servo_state_servo_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoStateResponse"}}}}}}},"/servo/upload":{"post":{"tags":["Servo"],"summary":"Upload Servo Recording","description":"Upload a servo recording CSV and make it available in GET /servo.","operationId":"upload_servo_recording_servo_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_servo_recording_servo_upload_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/servo/play":{"post":{"tags":["Servo"],"summary":"Play Recording","description":"Play a pre-recorded servo animation by name.","operationId":"play_recording_servo_play_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/servo/resume":{"post":{"tags":["Servo"],"summary":"Resume Servos","description":"Exit zero-hold mode and resume normal animation loop (plays idle).","operationId":"resume_servos_servo_resume_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/servo/hold":{"post":{"tags":["Servo"],"summary":"Hold Servos","description":"Hold current pose -- suppress idle/ambient animations, torque stays ON.","operationId":"hold_servos_servo_hold_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/servo/move":{"post":{"tags":["Servo"],"summary":"Move Servo","description":"Send joint positions to servo motors with smooth interpolation.","operationId":"move_servo_servo_move_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoMoveRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoMoveResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/servo/zero":{"post":{"tags":["Servo"],"summary":"Zero Servos","description":"Move all servos to 0 deg and hold (torque stays ON).","operationId":"zero_servos_servo_zero_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/servo/release":{"post":{"tags":["Servo"],"summary":"Release Servos","description":"Move servos to idle position then disable torque (safe release).","operationId":"release_servos_servo_release_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/servo/position":{"get":{"tags":["Servo"],"summary":"Get Servo Position","description":"Read current servo joint positions.","operationId":"get_servo_position_servo_position_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoPositionResponse"}}}}}}},"/servo/status":{"get":{"tags":["Servo"],"summary":"Get Servo Status","description":"Ping each servo and return per-joint online/offline status with angle.","operationId":"get_servo_status_servo_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoStatusResponse"}}}}}}},"/servo/aim":{"get":{"tags":["Servo"],"summary":"List Aim Directions","description":"List available aim directions.","operationId":"list_aim_directions_servo_aim_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["Servo"],"summary":"Aim Servo","description":"Aim the device head to a named direction.","operationId":"aim_servo_servo_aim_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoAimRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoAimResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/servo/nudge":{"post":{"tags":["Servo"],"summary":"Nudge Servo","description":"Move servo by relative degrees from current position.","operationId":"nudge_servo_servo_nudge_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoNudgeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoAimResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/servo/track":{"get":{"tags":["Servo"],"summary":"Get Tracking Status","description":"Get current tracking status.","operationId":"get_tracking_status_servo_track_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoTrackResponse"}}}}}},"post":{"tags":["Servo"],"summary":"Start Tracking","description":"Start tracking an object by bounding box. Servo follows the object in real-time.","operationId":"start_tracking_servo_track_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoTrackRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoTrackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/servo/track/stop":{"post":{"tags":["Servo"],"summary":"Stop Tracking","description":"Stop the current tracking session.","operationId":"stop_tracking_servo_track_stop_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoTrackResponse"}}}}}}},"/servo/track/update":{"post":{"tags":["Servo"],"summary":"Update Tracking Bbox","description":"Re-initialize tracker with a new bounding box.","operationId":"update_tracking_bbox_servo_track_update_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoTrackRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServoTrackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker/enroll":{"post":{"tags":["Speaker"],"summary":"Speaker Enroll","description":"Enroll or re-enroll a speaker from 1+ local WAV filepaths.\n\nNew samples are appended to the user's voice folder and the embedding is\nrecomputed from all samples in the folder (old + new).\n\nMissing paths are tolerated: a successful enroll deletes the consumed\nstranger cluster (rmtree), so a retry by OpenClaw can legitimately point\nat a path that no longer exists. Missing paths are skipped with a log\nline. If every path is missing AND the user is already enrolled, return\nthe existing meta as an idempotent success rather than a 400.","operationId":"speaker_enroll_speaker_enroll_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrollSpeakerRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrollResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker/record-enroll":{"post":{"tags":["Speaker"],"summary":"Speaker Record Enroll","description":"Record from the device mic and enroll the captured audio.\n\nCoordinates with the running voice_service to release ALSA cleanly:\npause the listener thread, run arecord, restart the listener, then\nenroll. ``voice_service`` is *paused* (not torn down) so we don't lose\nthe configured tts/stt credentials — restart needs no extra args.","operationId":"speaker_record_enroll_speaker_record_enroll_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordEnrollRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrollResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker/identity":{"post":{"tags":["Speaker"],"summary":"Speaker Update Identity","description":"Attach / update Telegram identity on an existing voice profile.\n\nUse when a user was first enrolled via mic (no Telegram info) and later\nintroduces themselves via Telegram — we can link the two without\nre-uploading audio.","operationId":"speaker_update_identity_speaker_identity_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIdentityRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnrollResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker/reset":{"post":{"tags":["Speaker"],"summary":"Speaker Reset","description":"Delete every voice profile (mirrors /face/reset).\n\nShared identity (``metadata.json``) is preserved — face / mood /\nwellbeing still depend on it.","operationId":"speaker_reset_speaker_reset_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveResponse"}}}}}}},"/speaker/remove":{"post":{"tags":["Speaker"],"summary":"Speaker Remove","description":"Delete the user's voice folder (embedding + samples + metadata).\n\nReturns 404 if the user has no voice profile — mirrors ``/face/remove``\nbehaviour so callers don't silently no-op on a typo. Other per-user data\n(face photos, mood, wellbeing, ...) is preserved regardless.","operationId":"speaker_remove_speaker_remove_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveSpeakerRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker/recognize":{"post":{"tags":["Speaker"],"summary":"Speaker Recognize","description":"Recognize the speaker of a single WAV file.\n\nReturns ``{name: \"unknown\"}`` when no registered speaker exceeds the match\nthreshold, along with ``unknown_audio_path`` so the skill can reuse that\npath for a later enrollment call.","operationId":"speaker_recognize_speaker_recognize_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecognizeSpeakerRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecognizeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/speaker/list":{"get":{"tags":["Speaker"],"summary":"Speaker List","description":"List users with a registered voice — public identity-focused view.\n\nInternal bookkeeping (sample filenames, embedding dim, timestamps) is\ncomputed by the service but intentionally not exposed here — see\n:class:`SpeakerListItem` for the trimmed schema.","operationId":"speaker_list_speaker_list_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListResponse"}}}}}}},"/voice/strangers":{"get":{"tags":["Speaker"],"summary":"Voice Strangers","description":"List unknown-voice clusters with their saved WAV samples.\n\nScans the per-cluster sub-dirs the speaker service writes under\n``SPEAKER_UNKNOWN_AUDIO_DIR/voice_<N>/`` so the web UI can play back\nclips the device has grouped as \"same unknown voice\" before deciding to\nenroll them as a known speaker.","operationId":"voice_strangers_voice_strangers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StrangersResponse"}}}}}}},"/voice/strangers/{hash}":{"delete":{"tags":["Speaker"],"summary":"Voice Stranger Delete Cluster","description":"Delete a whole unknown-voice cluster — centroid row + on-disk dir.\n\nUsed from the web card when the operator decides a cluster is noise or\nbelongs to someone they don't want tracked any further.","operationId":"voice_stranger_delete_cluster_voice_strangers__hash__delete","parameters":[{"name":"hash","in":"path","required":true,"schema":{"type":"string","title":"Hash"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StrangerDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/voice/strangers/{hash}/{filename}":{"delete":{"tags":["Speaker"],"summary":"Voice Stranger Delete Sample","description":"Delete a single WAV from a cluster.\n\nIf the cluster becomes empty after deletion, auto-drop the centroid so\nthe .npy state stays in sync with what the web list can show (an empty\ncluster dir is filtered out of ``GET /voice/strangers``, which would\notherwise orphan the centroid forever).","operationId":"voice_stranger_delete_sample_voice_strangers__hash___filename__delete","parameters":[{"name":"hash","in":"path","required":true,"schema":{"type":"string","title":"Hash"}},{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StrangerDeleteResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/voice/strangers/audio/{hash}/{filename}":{"get":{"tags":["Speaker"],"summary":"Voice Stranger Audio","description":"Stream a stranger-cluster WAV by cluster hash + filename.\n\nPath components are whitelisted (``voice_<digits>`` / ``<safe>.wav``) and\nthe resolved file must sit inside ``SPEAKER_UNKNOWN_AUDIO_DIR`` — blocks\npath-traversal attempts like ``../../etc/passwd``.","operationId":"voice_stranger_audio_voice_strangers_audio__hash___filename__get","parameters":[{"name":"hash","in":"path","required":true,"schema":{"type":"string","title":"Hash"}},{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/system/reboot":{"post":{"tags":["System"],"summary":"Reboot Os","description":"Reboot the operating system.","operationId":"reboot_os_system_reboot_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/system/shutdown":{"post":{"tags":["System"],"summary":"Shutdown Os","description":"Shutdown the operating system.","operationId":"shutdown_os_system_shutdown_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/voice/start":{"post":{"tags":["Voice"],"summary":"Start Voice","description":"Start the voice pipeline (always-on Deepgram STT + TTS).","operationId":"start_voice_voice_start_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceStartRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/voice/stop":{"post":{"tags":["Voice"],"summary":"Stop Voice","description":"Stop the voice pipeline.","operationId":"stop_voice_voice_stop_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/voice/config":{"post":{"tags":["Voice"],"summary":"Update Voice Config","description":"Update voice pipeline config at runtime.","operationId":"update_voice_config_voice_config_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceConfigRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/voice/voices":{"get":{"tags":["Voice"],"summary":"Get Voices","description":"Return available TTS voices for the requested (or current) provider.\n\n`lang` is a BCP-47 stt_language code (e.g. \"vi\", \"zh-CN\"). When set,\nElevenLabs voices are filtered to that language's curated bucket so\nVN/CN owners see only voices that sound natural in their language.\nEmpty / unknown lang returns the full flat list (back-compat for\nolder clients that don't send lang). OpenAI voices ignore lang —\nits built-in voices are language-agnostic.","operationId":"get_voices_voice_voices_get","parameters":[{"name":"provider","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"}},{"name":"lang","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lang"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/voice/speak":{"post":{"tags":["Voice"],"summary":"Speak Text","description":"Synthesize text to speech and play through the speaker.","operationId":"speak_text_voice_speak_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/voice/speak-queue":{"post":{"tags":["Voice"],"summary":"Speak Queue Text","description":"Speak text, queueing if TTS is currently busy.\n\nDiffers from /voice/speak: when the speaker is already in use, /voice/speak\nreturns 409 and the caller drops the text; /voice/speak-queue accepts the\nrequest, pre-synthesizes the audio in the background, and plays it\nseamlessly when the current speech finishes (same open ALSA stream → no\nTTFB gap between sentences). Used by the SSE handler so a multi-sentence\nagent reply that streams sentence-by-sentence is heard as one continuous\nutterance instead of N choppy speak() calls separated by ~400ms each.\n\n409 is still returned when music is playing (speaker fully committed) and\n503 when TTS isn't initialized; both match /voice/speak's contract so\nupstream error handling stays uniform.","operationId":"speak_queue_text_voice_speak_queue_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpeakRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/tts/stop":{"post":{"tags":["Voice"],"summary":"Stop Tts","description":"Interrupt active TTS playback immediately.","operationId":"stop_tts_tts_stop_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/voice/mute":{"post":{"tags":["Voice"],"summary":"Mute Mic","description":"Mute mic -- stop voice pipeline and sound perception.","operationId":"mute_mic_voice_mute_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/voice/unmute":{"post":{"tags":["Voice"],"summary":"Unmute Mic","description":"Unmute mic -- restart voice pipeline.","operationId":"unmute_mic_voice_unmute_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}}}}},"/voice/mic-level":{"get":{"tags":["Voice"],"summary":"Mic Level Stream","description":"Stream live mic input levels as Server-Sent Events (~10Hz).\n\nEach event: `data: {\"level\", \"threshold\", \"active\", \"muted\",\n\"sensing_level\", \"sensing_age_s\", \"sensing_threshold\",\n\"tts_speaking\", \"music_playing\"}`.\n\n- `level` — voice-pipeline mic (STT), latest capture-frame RMS on int16\n scale (0..32768, computed anyway by the VAD loop — zero added DSP\n cost). Falls to 0 while the mic drains under TTS/music or the\n pipeline is down. `threshold` is the VAD wake threshold.\n- `sensing_level` / `sensing_age_s` — noise mic (SoundPerception on\n HAL_AUDIO_SENSING_DEVICE): the last 0.5s sample's RMS and how old it\n is. Sampled once per sensing poll (a few seconds apart, paused\n during/after TTS), NOT continuous — the web bar steps rather than\n pumps. null when sensing/sound perception isn't running.\n `sensing_threshold` is the loud-noise threshold.\n- `tts_speaking` / `music_playing` — live playback state, piggybacked so\n the web audio card flips \"Speaking…/Playing music\" the moment playback\n ends instead of waiting out its 5s `/voice/status` poll.\n\nConsumed by the web Overview audio card (VU meters) via the os-server\n`/api/hardware` proxy — httputil.ReverseProxy streams event-stream\nresponses unbuffered, same as the MJPEG camera stream.","operationId":"mic_level_stream_voice_mic_level_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/voice/status":{"get":{"tags":["Voice"],"summary":"Voice Status","description":"Get voice pipeline status.","operationId":"voice_status_voice_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceStatusResponse"}}}}}}},"/version":{"get":{"tags":["System"],"summary":"Version","description":"Return HAL runtime version.","operationId":"version_version_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/device":{"get":{"tags":["System"],"summary":"Device","description":"This device's identity from DEVICE.md (id/name/type/schema) plus the\nboard the runtime resolved and the capability routes it mounted.","operationId":"device_device_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"tags":["System"],"summary":"Health","description":"Check which hardware drivers are available.","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}}},"components":{"schemas":{"ActiveRequest":{"properties":{"mac":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mac"}},"type":"object","title":"ActiveRequest"},"AudioDevicesResponse":{"properties":{"output_device":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Output Device"},"input_device":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Input Device"},"available":{"type":"boolean","title":"Available"}},"type":"object","required":["output_device","input_device","available"],"title":"AudioDevicesResponse"},"Body_upload_servo_recording_servo_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"recording_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recording Name"}},"type":"object","required":["file"],"title":"Body_upload_servo_recording_servo_upload_post"},"CameraInfoResponse":{"properties":{"available":{"type":"boolean","title":"Available"},"width":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Width"},"height":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Height"},"fps":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Fps"},"disabled":{"type":"boolean","title":"Disabled","default":false},"manual_override":{"type":"boolean","title":"Manual Override","default":false},"zoom":{"type":"number","title":"Zoom","default":1.0}},"type":"object","required":["available","width","height"],"title":"CameraInfoResponse"},"CameraZoomRequest":{"properties":{"zoom":{"type":"number","maximum":5.0,"minimum":1.0,"title":"Zoom","description":"Digital zoom factor, 1.0 = no zoom"}},"type":"object","required":["zoom"],"title":"CameraZoomRequest","examples":[{"zoom":2.0}]},"EmotionRequest":{"properties":{"emotion":{"type":"string","title":"Emotion","description":"Emotion name: curious, happy, sad, thinking, idle, excited, shy, shock"},"intensity":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Intensity","description":"Intensity 0.0-1.0","default":0.7}},"type":"object","required":["emotion"],"title":"EmotionRequest","examples":[{"emotion":"curious","intensity":0.8}]},"EmotionResponse":{"properties":{"status":{"type":"string","title":"Status"},"emotion":{"type":"string","title":"Emotion"},"servo":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Servo"},"led":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Led"}},"type":"object","required":["status","emotion","servo","led"],"title":"EmotionResponse"},"EnrollResponse":{"properties":{"status":{"type":"string","title":"Status"},"meta":{"$ref":"#/components/schemas/SpeakerMeta"}},"type":"object","required":["status","meta"],"title":"EnrollResponse"},"EnrollSpeakerRequest":{"properties":{"name":{"type":"string","minLength":1,"title":"Name","description":"Display name to enroll as."},"wav_paths":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Wav Paths","description":"Local filepaths of WAV files (any sample rate — will be normalized to 16kHz mono)."},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username","description":"Optional Telegram @handle (e.g. 'chloe_92'). Merged into /root/local/users/<name>/metadata.json — same file face-enroll writes."},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id","description":"Optional numeric Telegram user ID (for DM targeting)."},"origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin","description":"Channel the audio came from: 'mic' | 'telegram' | 'other'. Auto-inferred from presence of telegram_* fields if omitted. Encoded in the stored sample filename so list_registered can show which channels contributed."}},"type":"object","required":["name","wav_paths"],"title":"EnrollSpeakerRequest","description":"Enroll one speaker from 1+ local WAV filepaths."},"FaceEnrollRequest":{"properties":{"image_base64":{"type":"string","title":"Image Base64","description":"Base64-encoded image (JPEG or PNG)"},"label":{"type":"string","maxLength":64,"minLength":1,"title":"Label","description":"Person name"},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username","description":"Telegram username of the person"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id","description":"Telegram user ID for DM targeting"}},"type":"object","required":["image_base64","label"],"title":"FaceEnrollRequest"},"FaceEnrollResponse":{"properties":{"status":{"type":"string","title":"Status"},"label":{"type":"string","title":"Label"},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"},"photo_path":{"type":"string","title":"Photo Path"},"enrolled_count":{"type":"integer","title":"Enrolled Count"}},"type":"object","required":["status","label","photo_path","enrolled_count"],"title":"FaceEnrollResponse"},"FaceOwnersDetailResponse":{"properties":{"enrolled_count":{"type":"integer","title":"Enrolled Count"},"persons":{"items":{"$ref":"#/components/schemas/FacePersonDetail"},"type":"array","title":"Persons"}},"type":"object","required":["enrolled_count","persons"],"title":"FaceOwnersDetailResponse"},"FacePersonDetail":{"properties":{"label":{"type":"string","title":"Label"},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"},"photo_count":{"type":"integer","title":"Photo Count"},"photos":{"items":{"type":"string"},"type":"array","title":"Photos"},"mood_days":{"items":{"type":"string"},"type":"array","title":"Mood Days","default":[]},"wellbeing_days":{"items":{"type":"string"},"type":"array","title":"Wellbeing Days","default":[]},"music_suggestion_days":{"items":{"type":"string"},"type":"array","title":"Music Suggestion Days","default":[]},"posture_days":{"items":{"type":"string"},"type":"array","title":"Posture Days","default":[]},"audio_history_days":{"items":{"type":"string"},"type":"array","title":"Audio History Days","default":[]},"voice_samples":{"items":{"type":"string"},"type":"array","title":"Voice Samples","default":[]},"habit_patterns":{"type":"boolean","title":"Habit Patterns","default":false},"files":{"items":{"type":"string"},"type":"array","title":"Files","default":[]}},"type":"object","required":["label","photo_count","photos"],"title":"FacePersonDetail"},"FacePhotoRemoveRequest":{"properties":{"label":{"type":"string","maxLength":64,"minLength":1,"title":"Label"},"filename":{"type":"string","maxLength":128,"minLength":1,"title":"Filename"}},"type":"object","required":["label","filename"],"title":"FacePhotoRemoveRequest"},"FaceRemoveRequest":{"properties":{"label":{"type":"string","maxLength":64,"minLength":1,"title":"Label"}},"type":"object","required":["label"],"title":"FaceRemoveRequest"},"FaceRemoveResponse":{"properties":{"status":{"type":"string","title":"Status"},"label":{"type":"string","title":"Label"},"enrolled_count":{"type":"integer","title":"Enrolled Count"}},"type":"object","required":["status","label","enrolled_count"],"title":"FaceRemoveResponse"},"FaceResetResponse":{"properties":{"status":{"type":"string","title":"Status"},"enrolled_count":{"type":"integer","title":"Enrolled Count"}},"type":"object","required":["status","enrolled_count"],"title":"FaceResetResponse"},"FaceStatusResponse":{"properties":{"enrolled_count":{"type":"integer","title":"Enrolled Count"},"enrolled_names":{"items":{"type":"string"},"type":"array","title":"Enrolled Names"}},"type":"object","required":["enrolled_count","enrolled_names"],"title":"FaceStatusResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status"},"servo":{"type":"boolean","title":"Servo"},"led":{"type":"boolean","title":"Led"},"camera":{"type":"boolean","title":"Camera"},"audio":{"type":"boolean","title":"Audio"},"sensing":{"type":"boolean","title":"Sensing"},"voice":{"type":"boolean","title":"Voice"},"tts":{"type":"boolean","title":"Tts"},"music":{"type":"boolean","title":"Music"},"display":{"type":"boolean","title":"Display"},"thermal":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Thermal"}},"type":"object","required":["status","servo","led","camera","audio","sensing","voice","tts","music","display"],"title":"HealthResponse"},"LEDColorResponse":{"properties":{"led_count":{"type":"integer","title":"Led Count"},"on":{"type":"boolean","title":"On"},"color":{"items":{"type":"integer"},"type":"array","title":"Color"},"hex":{"type":"string","title":"Hex"},"brightness":{"type":"number","title":"Brightness"},"effect":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Effect"},"scene":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scene"}},"type":"object","required":["led_count","on","color","hex","brightness","effect","scene"],"title":"LEDColorResponse"},"LEDEffectRequest":{"properties":{"effect":{"type":"string","title":"Effect","description":"Effect name: breathing, candle, rainbow, notification_flash, pulse, blink"},"color":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Color","description":"Base RGB color for the effect (default: current color)"},"speed":{"type":"number","maximum":5.0,"minimum":0.1,"title":"Speed","description":"Speed multiplier (0.1=slow, 1.0=normal, 5.0=fast)","default":1.0},"duration_ms":{"anyOf":[{"type":"integer","maximum":60000.0,"minimum":100.0},{"type":"null"}],"title":"Duration Ms","description":"Auto-stop after duration (null=indefinite)"},"transient":{"type":"boolean","title":"Transient","description":"If true, don't overwrite user LED state (used by Buddy/transient overlays).","default":false}},"type":"object","required":["effect"],"title":"LEDEffectRequest","examples":[{"color":[255,100,0],"effect":"breathing","speed":1.0},{"effect":"rainbow","speed":0.5},{"color":[255,0,0],"duration_ms":3000,"effect":"notification_flash"}]},"LEDEffectResponse":{"properties":{"status":{"type":"string","title":"Status"},"effect":{"type":"string","title":"Effect"},"speed":{"type":"number","title":"Speed"}},"type":"object","required":["status","effect","speed"],"title":"LEDEffectResponse"},"LEDOffRequest":{"properties":{"transient":{"type":"boolean","title":"Transient","default":false}},"type":"object","title":"LEDOffRequest"},"LEDPaintRequest":{"properties":{"colors":{"items":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"integer"}]},"type":"array","title":"Colors"},"gradient":{"type":"boolean","title":"Gradient","default":false},"transient":{"type":"boolean","title":"Transient","default":false}},"type":"object","required":["colors"],"title":"LEDPaintRequest","examples":[{"colors":[[255,0,0],[0,255,0],[0,0,255]]}]},"LEDSolidRequest":{"properties":{"color":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"integer"}],"title":"Color"},"transient":{"type":"boolean","title":"Transient","description":"If true, don't overwrite user LED state (used by Buddy/transient overlays).","default":false}},"type":"object","required":["color"],"title":"LEDSolidRequest","examples":[{"color":[255,100,0]},{"color":16711680}]},"LEDStateResponse":{"properties":{"led_count":{"type":"integer","title":"Led Count"}},"type":"object","required":["led_count"],"title":"LEDStateResponse"},"LEDStatusRequest":{"properties":{"state":{"type":"string","title":"State","description":"System status state name (booting, error, ota, connectivity, hal_down, agent_down, hardware, ready_flash) — resolved to a color/effect via STATUS_LED_PRESETS."}},"type":"object","required":["state"],"title":"LEDStatusRequest","examples":[{"state":"booting"}]},"ListResponse":{"properties":{"total":{"type":"integer","title":"Total"},"enrolled_names":{"items":{"type":"string"},"type":"array","title":"Enrolled Names"},"speakers":{"items":{"$ref":"#/components/schemas/SpeakerListItem"},"type":"array","title":"Speakers"}},"type":"object","required":["total","enrolled_names","speakers"],"title":"ListResponse"},"MacRequest":{"properties":{"mac":{"type":"string","title":"Mac"}},"type":"object","required":["mac"],"title":"MacRequest"},"MusicPlayRequest":{"properties":{"query":{"type":"string","maxLength":500,"minLength":1,"title":"Query","description":"Song name or search query"},"person":{"type":"string","maxLength":64,"title":"Person","description":"Person name (from face recognition) for per-user history","default":""}},"type":"object","required":["query"],"title":"MusicPlayRequest","examples":[{"person":"alice","query":"The Calling - Wherever You Will Go"}]},"MusicStatusResponse":{"properties":{"available":{"type":"boolean","title":"Available"},"playing":{"type":"boolean","title":"Playing"},"title":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Title"},"speaker_muted":{"type":"boolean","title":"Speaker Muted","default":false}},"type":"object","required":["available","playing"],"title":"MusicStatusResponse"},"PresenceResponse":{"properties":{"state":{"type":"string","title":"State"},"enabled":{"type":"boolean","title":"Enabled"},"seconds_since_motion":{"type":"integer","title":"Seconds Since Motion"},"idle_timeout":{"type":"integer","title":"Idle Timeout"},"away_timeout":{"type":"integer","title":"Away Timeout"}},"type":"object","required":["state","enabled","seconds_since_motion","idle_timeout","away_timeout"],"title":"PresenceResponse"},"RecognizeResponse":{"properties":{"name":{"type":"string","title":"Name"},"confidence":{"type":"number","title":"Confidence"},"match":{"type":"boolean","title":"Match"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"},"has_telegram_identity":{"type":"boolean","title":"Has Telegram Identity","default":false},"unknown_audio_path":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unknown Audio Path"},"voiceprint_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voiceprint Hash"},"candidates":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Candidates","default":[]},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["name","confidence","match"],"title":"RecognizeResponse"},"RecognizeSpeakerRequest":{"properties":{"wav_path":{"type":"string","minLength":1,"title":"Wav Path","description":"Local filepath of WAV file."}},"type":"object","required":["wav_path"],"title":"RecognizeSpeakerRequest"},"RecordEnrollRequest":{"properties":{"name":{"type":"string","minLength":1,"title":"Name","description":"Display name to enroll as."},"duration_sec":{"type":"integer","maximum":60.0,"minimum":1.0,"title":"Duration Sec","description":"Recording length in seconds. Capped at 60 to bound ALSA hold.","default":15},"origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin","description":"Tagged into stored sample filenames so list_registered can distinguish web-triggered enrolls from telegram / mic ambient.","default":"web_device_mic"}},"type":"object","required":["name"],"title":"RecordEnrollRequest","description":"Capture audio from the device's mic, then enroll under ``name``.\n\nWeb Setup / Edit pages can't reach the browser microphone without HTTPS\n(insecure-context restriction on getUserMedia), so the device records its\nown ALSA mic instead. The user stands near the device and reads the\nprompted text while ``arecord`` writes a 16kHz mono WAV which is then\nfed straight to ``SpeakerRecognizer.enroll``."},"RemoveResponse":{"properties":{"status":{"type":"string","title":"Status"},"name":{"type":"string","title":"Name"},"removed":{"type":"boolean","title":"Removed"}},"type":"object","required":["status","name","removed"],"title":"RemoveResponse"},"RemoveSpeakerRequest":{"properties":{"name":{"type":"string","minLength":1,"title":"Name"}},"type":"object","required":["name"],"title":"RemoveSpeakerRequest"},"SceneListResponse":{"properties":{"scenes":{"items":{"type":"string"},"type":"array","title":"Scenes"},"active":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Active"}},"type":"object","required":["scenes","active"],"title":"SceneListResponse"},"SceneRequest":{"properties":{"scene":{"type":"string","title":"Scene","description":"Scene name: reading, focus, relax, movie, night, energize"}},"type":"object","required":["scene"],"title":"SceneRequest","examples":[{"scene":"reading"}]},"SceneResponse":{"properties":{"status":{"type":"string","title":"Status"},"scene":{"type":"string","title":"Scene"},"brightness":{"type":"number","title":"Brightness"},"color":{"items":{"type":"integer"},"type":"array","title":"Color"}},"type":"object","required":["status","scene","brightness","color"],"title":"SceneResponse"},"SensingResponse":{"properties":{"running":{"type":"boolean","title":"Running"},"poll_interval":{"type":"number","title":"Poll Interval"},"last_event_seconds_ago":{"additionalProperties":{"type":"integer"},"type":"object","title":"Last Event Seconds Ago"},"perceptions":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Perceptions"},"presence":{"additionalProperties":true,"type":"object","title":"Presence"}},"type":"object","required":["running","poll_interval","last_event_seconds_ago","perceptions","presence"],"title":"SensingResponse"},"ServoAimRequest":{"properties":{"direction":{"type":"string","title":"Direction","description":"Named direction: desk, wall, left, right, up, down, center, user"},"duration":{"type":"number","maximum":10.0,"minimum":0.0,"title":"Duration","description":"Move duration in seconds — stretched automatically when the move would exceed the SAFETY.md speed ceiling","default":0.5}},"type":"object","required":["direction"],"title":"ServoAimRequest","examples":[{"direction":"desk"},{"direction":"left","duration":3.0}]},"ServoAimResponse":{"properties":{"status":{"type":"string","title":"Status"},"direction":{"type":"string","title":"Direction"},"positions":{"additionalProperties":{"type":"number"},"type":"object","title":"Positions"}},"type":"object","required":["status","direction","positions"],"title":"ServoAimResponse"},"ServoDetail":{"properties":{"id":{"type":"integer","title":"Id"},"angle":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Angle"},"online":{"type":"boolean","title":"Online"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}},"type":"object","required":["id","angle","online"],"title":"ServoDetail"},"ServoMoveRequest":{"properties":{"positions":{"additionalProperties":{"type":"number"},"type":"object","title":"Positions","description":"Joint positions (degrees). Ordered by servo ID: base_yaw.pos (ID 1, min -90 max 90), base_pitch.pos (ID 2, min -90 max 90), elbow_pitch.pos (ID 3, min -90 max 90), wrist_roll.pos (ID 4, min -90 max 90), wrist_pitch.pos (ID 5, min -90 max 90). Values are clamped to safe limits automatically."},"duration":{"type":"number","maximum":10.0,"minimum":0.0,"title":"Duration","description":"Move duration in seconds. 0 = instant jump, >0 = smooth interpolation — stretched automatically when the move would exceed the SAFETY.md speed ceiling","default":0.5}},"type":"object","required":["positions"],"title":"ServoMoveRequest","examples":[{"_comment":"ID1 base_yaw [-90,90] | ID2 base_pitch [-90,90] | ID3 elbow_pitch [-90,90] | ID4 wrist_roll [-90,90] | ID5 wrist_pitch [-90,90]","positions":{"base_pitch.pos":10.0,"base_yaw.pos":0.0,"elbow_pitch.pos":-5.0,"wrist_pitch.pos":0.0,"wrist_roll.pos":0.0}},{"duration":3.0,"positions":{"base_pitch.pos":5.0,"elbow_pitch.pos":5.0}}]},"ServoMoveResponse":{"properties":{"status":{"type":"string","title":"Status"},"requested":{"additionalProperties":{"type":"number"},"type":"object","title":"Requested"},"clamped":{"additionalProperties":{"type":"number"},"type":"object","title":"Clamped"},"duration":{"type":"number","title":"Duration"},"errors":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"title":"Errors"}},"type":"object","required":["status","requested","clamped","duration"],"title":"ServoMoveResponse"},"ServoNudgeRequest":{"properties":{"yaw":{"type":"number","maximum":180.0,"minimum":-180.0,"title":"Yaw","description":"Relative yaw in degrees (negative=left, positive=right)","default":0.0},"pitch":{"type":"number","maximum":90.0,"minimum":-90.0,"title":"Pitch","description":"Relative pitch in degrees (negative=down, positive=up)","default":0.0},"duration":{"type":"number","maximum":10.0,"minimum":0.0,"title":"Duration","description":"Move duration in seconds — stretched automatically when the move would exceed the SAFETY.md speed ceiling","default":0.5}},"type":"object","title":"ServoNudgeRequest","examples":[{"yaw":-15},{"duration":1.5,"pitch":10,"yaw":30}]},"ServoPositionResponse":{"properties":{"positions":{"additionalProperties":{"type":"number"},"type":"object","title":"Positions"}},"type":"object","required":["positions"],"title":"ServoPositionResponse"},"ServoRequest":{"properties":{"recording":{"type":"string","title":"Recording"}},"type":"object","required":["recording"],"title":"ServoRequest","examples":[{"recording":"curious"}]},"ServoStateResponse":{"properties":{"available_recordings":{"items":{"type":"string"},"type":"array","title":"Available Recordings"},"current":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current"}},"type":"object","required":["available_recordings","current"],"title":"ServoStateResponse","examples":[{"available_recordings":["nod","curious","happy_wiggle","idle","sad","excited","shy","shock","listening","thinking_deep","laugh","confused","sleepy","greeting","goodbye","acknowledge","stretching","scanning","wake_up","headshake","music_groove","music_chill","music_hype"],"current":"idle"}]},"ServoStatusResponse":{"properties":{"servos":{"additionalProperties":{"$ref":"#/components/schemas/ServoDetail"},"type":"object","title":"Servos"}},"type":"object","required":["servos"],"title":"ServoStatusResponse"},"ServoTrackRequest":{"properties":{"bbox":{"anyOf":[{"items":{"type":"integer"},"type":"array","maxItems":4,"minItems":4},{"type":"null"}],"title":"Bbox","description":"Bounding box [x, y, w, h]. If omitted, auto-detect using YOLOWorld."},"target":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}],"title":"Target","description":"Object name(s) to track. Pass a single string (e.g. 'cup') or a list of candidate labels (e.g. ['cup', 'mug', 'coffee cup']) when the caller is unsure of the exact word — YOLOWorld evaluates all candidates and the highest-confidence detection is used.","default":""}},"type":"object","title":"ServoTrackRequest","examples":[{"target":"cup"},{"target":["cup","mug","coffee cup"]},{"bbox":[280,200,80,80],"target":"water bottle"}]},"ServoTrackResponse":{"properties":{"status":{"type":"string","title":"Status"},"tracking":{"type":"boolean","title":"Tracking"},"target":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target"},"bbox":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Bbox"},"confidence":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Confidence"}},"type":"object","required":["status","tracking"],"title":"ServoTrackResponse"},"SpeakRequest":{"properties":{"text":{"type":"string","maxLength":2000,"minLength":1,"title":"Text","description":"Text to speak via TTS"},"voice":{"type":"string","title":"Voice","description":"Override TTS voice for this request (e.g. 'Rachel', 'Brian')","default":""},"interruptible":{"type":"boolean","title":"Interruptible","description":"If True, can be interrupted by next speech","default":false},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider","description":"Override TTS provider: 'openai' or 'elevenlabs'"},"tts_api_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tts Api Key","description":"API key for provider override"},"tts_base_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tts Base Url","description":"Base URL for provider override"},"cached":{"type":"boolean","title":"Cached","description":"Look up WAV cache; render+save on miss","default":false},"prerender":{"type":"boolean","title":"Prerender","description":"Render+save to cache without playing (warmup)","default":false},"realtime_feedback":{"type":"boolean","title":"Realtime Feedback","description":"Feed this text to the realtime agent as history (agent replies only)","default":false}},"type":"object","required":["text"],"title":"SpeakRequest","examples":[{"text":"[laugh] Hey! How are you doing today? I missed you! [sigh] It has been so quiet around here.","voice":"Rachel"}]},"SpeakerListItem":{"properties":{"name":{"type":"string","title":"Name"},"display_name":{"type":"string","title":"Display Name"},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"},"has_telegram_identity":{"type":"boolean","title":"Has Telegram Identity","default":false},"enrollment_sources":{"items":{"type":"string"},"type":"array","title":"Enrollment Sources","default":[]},"num_samples":{"type":"integer","title":"Num Samples"}},"type":"object","required":["name","display_name","num_samples"],"title":"SpeakerListItem","description":"Trimmed public view for /speaker/list — identity-focused, no internals.\n\nDrops internal bookkeeping fields (embedding_dim, sample_files,\nsample_origins, enrolled_at, updated_at, last_enrollment_source) — those\nbelong in log/debug output, not the public API response."},"SpeakerMeta":{"properties":{"name":{"type":"string","title":"Name"},"display_name":{"type":"string","title":"Display Name"},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"},"has_telegram_identity":{"type":"boolean","title":"Has Telegram Identity","default":false},"enrollment_sources":{"items":{"type":"string"},"type":"array","title":"Enrollment Sources","default":[]},"last_enrollment_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Enrollment Source"},"num_samples":{"type":"integer","title":"Num Samples"},"embedding_dim":{"type":"integer","title":"Embedding Dim"},"enrolled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Enrolled At"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"},"sample_files":{"items":{"type":"string"},"type":"array","title":"Sample Files","default":[]},"sample_origins":{"additionalProperties":{"type":"string"},"type":"object","title":"Sample Origins","default":{}}},"type":"object","required":["name","display_name","num_samples","embedding_dim"],"title":"SpeakerMeta","description":"Full metadata — used for enroll / identity confirmation responses."},"StatusResponse":{"properties":{"status":{"type":"string","title":"Status"}},"type":"object","required":["status"],"title":"StatusResponse"},"StrangerCluster":{"properties":{"hash":{"type":"string","title":"Hash"},"sample_count":{"type":"integer","title":"Sample Count"},"latest_mtime":{"type":"number","title":"Latest Mtime"},"samples":{"items":{"$ref":"#/components/schemas/StrangerSample"},"type":"array","title":"Samples"}},"type":"object","required":["hash","sample_count","latest_mtime","samples"],"title":"StrangerCluster"},"StrangerDeleteResponse":{"properties":{"status":{"type":"string","title":"Status"},"hash":{"type":"string","title":"Hash"},"filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filename"},"cluster_removed":{"type":"boolean","title":"Cluster Removed","default":false}},"type":"object","required":["status","hash"],"title":"StrangerDeleteResponse"},"StrangerSample":{"properties":{"filename":{"type":"string","title":"Filename"},"size_bytes":{"type":"integer","title":"Size Bytes"},"mtime":{"type":"number","title":"Mtime"}},"type":"object","required":["filename","size_bytes","mtime"],"title":"StrangerSample"},"StrangersResponse":{"properties":{"total":{"type":"integer","title":"Total"},"clusters":{"items":{"$ref":"#/components/schemas/StrangerCluster"},"type":"array","title":"Clusters"}},"type":"object","required":["total","clusters"],"title":"StrangersResponse"},"UpdateIdentityRequest":{"properties":{"name":{"type":"string","minLength":1,"title":"Name"},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"}},"type":"object","required":["name"],"title":"UpdateIdentityRequest","description":"Attach / update Telegram identity on an existing voice profile."},"UserInfoResponse":{"properties":{"name":{"type":"string","title":"Name"},"is_friend":{"type":"boolean","title":"Is Friend"},"telegram_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Id"},"telegram_username":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Telegram Username"}},"type":"object","required":["name","is_friend"],"title":"UserInfoResponse"},"UserRenameRequest":{"properties":{"old_label":{"type":"string","maxLength":64,"minLength":1,"title":"Old Label"},"new_label":{"type":"string","maxLength":64,"minLength":1,"title":"New Label"}},"type":"object","required":["old_label","new_label"],"title":"UserRenameRequest","description":"Rename a user folder under /root/local/users/.\n\nTouches every per-user surface in one move: face photos, voice samples,\nmetadata.json, mood/wellbeing/audio_history JSONLs, habit patterns —\nall live inside the label folder, so a single os.rename moves them\natomically. The face recognizer's 2s disk poller and the speaker\nrecognizer's file-backed registry both pick up the new name on next\nread; we touch the speaker registry inline so list_registered reflects\nthe rename immediately rather than after a full restart."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VoiceConfigRequest":{"properties":{"wake_words":{"items":{"type":"string"},"type":"array","minItems":1,"title":"Wake Words","description":"Wake word list (lowercase matched)"}},"type":"object","required":["wake_words"],"title":"VoiceConfigRequest"},"VoiceStartRequest":{"properties":{"llm_api_key":{"type":"string","minLength":1,"title":"Llm Api Key","description":"OpenAI-compatible API key for AutonomousSTT (LLM-as-STT fallback)"},"llm_base_url":{"type":"string","minLength":1,"title":"Llm Base Url","description":"OpenAI-compatible base URL for TTS and STT"},"tts_api_key":{"type":"string","title":"Tts Api Key","description":"TTS provider API key (OpenAI / ElevenLabs). Empty falls back to llm_api_key — for households where TTS and LLM share one credential.","default":""},"stt_api_key":{"type":"string","title":"Stt Api Key","description":"AutonomousSTT API key. Empty falls back to llm_api_key. Ignored when deepgram_api_key is set.","default":""},"stt_base_url":{"type":"string","title":"Stt Base Url","description":"AutonomousSTT base URL. Empty falls back to llm_base_url.","default":""},"tts_base_url":{"type":"string","title":"Tts Base Url","description":"TTS provider base URL. Empty falls back to llm_base_url.","default":""},"deepgram_api_key":{"type":"string","title":"Deepgram Api Key","description":"Deepgram API key (optional, falls back to Autonomous STT)","default":""},"tts_voice":{"type":"string","title":"Tts Voice","description":"TTS voice name (optional, defaults to config TTS_VOICE)","default":""},"tts_instructions":{"type":"string","title":"Tts Instructions","description":"TTS style/vibe instructions (optional, e.g. 'Speak warmly')","default":""},"tts_provider":{"type":"string","title":"Tts Provider","description":"TTS provider: 'openai' (default) or 'elevenlabs'","default":"openai"}},"type":"object","required":["llm_api_key","llm_base_url"],"title":"VoiceStartRequest"},"VoiceStatusResponse":{"properties":{"voice_available":{"type":"boolean","title":"Voice Available"},"voice_listening":{"type":"boolean","title":"Voice Listening"},"tts_available":{"type":"boolean","title":"Tts Available"},"tts_speaking":{"type":"boolean","title":"Tts Speaking"},"tts_detail":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Tts Detail"},"mic_muted":{"type":"boolean","title":"Mic Muted","default":false},"hw_mic_switch_muted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Hw Mic Switch Muted"}},"type":"object","required":["voice_available","voice_listening","tts_available","tts_speaking"],"title":"VoiceStatusResponse"},"VolumeRequest":{"properties":{"volume":{"type":"integer","maximum":100.0,"minimum":0.0,"title":"Volume","description":"Volume percentage 0-100"}},"type":"object","required":["volume"],"title":"VolumeRequest","examples":[{"volume":75}]},"VolumeResponse":{"properties":{"control":{"type":"string","title":"Control"},"volume":{"type":"integer","title":"Volume"}},"type":"object","required":["control","volume"],"title":"VolumeResponse"}}},"tags":[{"name":"Servo","description":"5-axis Feetech servo motor control. Play pre-recorded animations or send direct joint positions."},{"name":"LED","description":"WS2812 RGB LED strip (64 LEDs). Set solid color, paint individual pixels, or turn off."},{"name":"Camera","description":"USB camera for snapshots and MJPEG streaming."},{"name":"Audio","description":"Low-level audio hardware control. Volume (amixer), raw recording (mic), and test tones. No AI -- just hardware."},{"name":"Emotion","description":"High-level orchestration: single call coordinates servo animation + LED color + display expression for an emotion."},{"name":"Scene","description":"Lighting scene presets (reading, focus, relax, movie, night, energize). Sets LED color temperature and brightness."},{"name":"Presence","description":"PIR motion sensor presence detection. Auto-dims lights when user is idle/away."},{"name":"Display","description":"Round LCD display: pixel art eye expressions (default) or info mode (time, weather, text)."},{"name":"Voice","description":"AI voice pipeline. Deepgram STT (always-on listening) + LLM-based TTS (text-to-speech). Requires API keys."},{"name":"Speaker","description":"Per-user voice enrollment + recognition via cosine similarity on external-API embeddings."},{"name":"System","description":"Health checks and system status."}]}