Update 2026-05-13 14:42:31
This commit is contained in:
parent
54b1e745ca
commit
edddb7e0c3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,106 +1,148 @@
|
|||||||
{
|
{
|
||||||
"total_records": 3,
|
"total_records": 4,
|
||||||
"last_updated": "2026-04-19 20:55:59",
|
"last_updated": "2026-04-19 20:55:59",
|
||||||
"records": [
|
"records": [
|
||||||
{
|
{
|
||||||
"record_name": "R156",
|
"record_name": "testing_14",
|
||||||
"text": "هيه اعرفه طبعا .. ياولد هاد المندوس ياولد هات المندوس.. هذا المندوس مندوسك لايسحبون فلوسك\nالمندوس كله هدايا وفلوس \nتدخل وجيبك فاضي تظهر وجيبك متروووس",
|
"text": "يا هلا مسهلا معاك بوسينده،\nحاب تعمل معي فيديو مميز؟\nتواصل وياي وانا فريقي جاهزين خلنا نبدع معا ونعمل ترند.",
|
||||||
"replay_count": 1,
|
"replay_count": 1,
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"audio_generated_at": "2026-03-17T01:16:32",
|
"audio_generated_at": "2026-05-12 10:40:29",
|
||||||
"last_playback_finished_at": "2026-03-17T01:16:47",
|
"last_playback_finished_at": "2026-05-12 10:40:40",
|
||||||
"saved_at": "2026-04-19 20:55:59",
|
"saved_at": "2026-05-12 10:40:40"
|
||||||
"imported_from_disk": true
|
|
||||||
},
|
},
|
||||||
"audio_capture": {
|
"audio_capture": {
|
||||||
"backend": "unknown"
|
"backend": "parec",
|
||||||
|
"sink": "alsa_output.platform-sound.analog-stereo",
|
||||||
|
"monitor_source": "alsa_output.platform-sound.analog-stereo.monitor",
|
||||||
|
"restored_microphone_source": "alsa_input.platform-sound.analog-stereo"
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"speaker_recording": {
|
"speaker_recording": {
|
||||||
"name": "R156.wav",
|
"name": "testing_14.wav",
|
||||||
"path": "R156.wav",
|
"path": "testing_14.wav",
|
||||||
"size_bytes": 713672,
|
"size_bytes": 468480,
|
||||||
"sample_rate": 24000,
|
"sample_rate": 24000,
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"sample_width_bytes": 2,
|
"sample_width_bytes": 2,
|
||||||
"duration_seconds": 14.867
|
"duration_seconds": 9.76
|
||||||
},
|
},
|
||||||
"gemini_raw_output": {
|
"gemini_raw_output": {
|
||||||
"name": "R156_raw.wav",
|
"name": "testing_14_raw.wav",
|
||||||
"path": "R156_raw.wav",
|
"path": "testing_14_raw.wav",
|
||||||
"size_bytes": 706604,
|
"size_bytes": 468480,
|
||||||
"sample_rate": 24000,
|
"sample_rate": 24000,
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"sample_width_bytes": 2,
|
"sample_width_bytes": 2,
|
||||||
"duration_seconds": 14.72
|
"duration_seconds": 9.76
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"record_name": "R2(1)",
|
"record_name": "testing_15",
|
||||||
"text": "من اجمل البرامج في رمضان",
|
"text": "يا هلا مسهلا معاك بوسينده،\nحاب تعمل معي فيديو مميز؟\nتواصل وياي وانا فريقي جاهزين خلنا نبدع معا ونعمل ترند.",
|
||||||
"replay_count": 1,
|
"replay_count": 1,
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"audio_generated_at": "2026-03-16T19:37:29",
|
"audio_generated_at": "2026-05-12 10:41:16",
|
||||||
"last_playback_finished_at": "2026-03-16T19:37:32",
|
"last_playback_finished_at": "2026-05-12 10:41:26",
|
||||||
"saved_at": "2026-04-19 20:55:59",
|
"saved_at": "2026-05-12 10:41:26"
|
||||||
"imported_from_disk": true
|
|
||||||
},
|
},
|
||||||
"audio_capture": {
|
"audio_capture": {
|
||||||
"backend": "unknown"
|
"backend": "parec",
|
||||||
|
"sink": "alsa_output.platform-sound.analog-stereo",
|
||||||
|
"monitor_source": "alsa_output.platform-sound.analog-stereo.monitor",
|
||||||
|
"restored_microphone_source": "alsa_input.platform-sound.analog-stereo"
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"speaker_recording": {
|
"speaker_recording": {
|
||||||
"name": "R2(1).wav",
|
"name": "testing_15.wav",
|
||||||
"path": "R2(1).wav",
|
"path": "testing_15.wav",
|
||||||
"size_bytes": 131526,
|
"size_bytes": 472320,
|
||||||
"sample_rate": 24000,
|
"sample_rate": 24000,
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"sample_width_bytes": 2,
|
"sample_width_bytes": 2,
|
||||||
"duration_seconds": 2.739
|
"duration_seconds": 9.84
|
||||||
},
|
},
|
||||||
"gemini_raw_output": {
|
"gemini_raw_output": {
|
||||||
"name": "R2(1)_raw.wav",
|
"name": "testing_15_raw.wav",
|
||||||
"path": "R2(1)_raw.wav",
|
"path": "testing_15_raw.wav",
|
||||||
"size_bytes": 117164,
|
"size_bytes": 472320,
|
||||||
"sample_rate": 24000,
|
"sample_rate": 24000,
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"sample_width_bytes": 2,
|
"sample_width_bytes": 2,
|
||||||
"duration_seconds": 2.44
|
"duration_seconds": 9.84
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"record_name": "countdown_intro_2",
|
"record_name": "testing_16",
|
||||||
"text": "Look at the camera, stay ready, hold your pose with me, keep still, keep your smile soft, and in a moment I will count down for the photo.",
|
"text": "يا هلا مسهلا معاك بُوسْنَيْدَة،\nحاب تعمل معي فيديو مميز؟\nتواصل وياي وانا فريقي جاهزين خلنا نبدع معا ونعمل ترند.",
|
||||||
"replay_count": 0,
|
"replay_count": 1,
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"audio_generated_at": "",
|
"audio_generated_at": "2026-05-12 10:42:31",
|
||||||
"last_playback_finished_at": "",
|
"last_playback_finished_at": "2026-05-12 10:42:40",
|
||||||
"saved_at": "2026-04-19 20:55:59",
|
"saved_at": "2026-05-12 10:42:40"
|
||||||
"imported_from_disk": true
|
|
||||||
},
|
},
|
||||||
"audio_capture": {
|
"audio_capture": {
|
||||||
"backend": "unknown"
|
"backend": "parec",
|
||||||
|
"sink": "alsa_output.platform-sound.analog-stereo",
|
||||||
|
"monitor_source": "alsa_output.platform-sound.analog-stereo.monitor",
|
||||||
|
"restored_microphone_source": "alsa_input.platform-sound.analog-stereo"
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"speaker_recording": {
|
"speaker_recording": {
|
||||||
"name": "countdown_intro_2.wav",
|
"name": "testing_16.wav",
|
||||||
"path": "countdown_intro_2.wav",
|
"path": "testing_16.wav",
|
||||||
"size_bytes": 432148,
|
"size_bytes": 426240,
|
||||||
"sample_rate": 24000,
|
"sample_rate": 24000,
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"sample_width_bytes": 2,
|
"sample_width_bytes": 2,
|
||||||
"duration_seconds": 9.002
|
"duration_seconds": 8.88
|
||||||
},
|
},
|
||||||
"gemini_raw_output": {
|
"gemini_raw_output": {
|
||||||
"name": "countdown_intro_2_raw.wav",
|
"name": "testing_16_raw.wav",
|
||||||
"path": "countdown_intro_2_raw.wav",
|
"path": "testing_16_raw.wav",
|
||||||
"size_bytes": 418604,
|
"size_bytes": 426240,
|
||||||
"sample_rate": 24000,
|
"sample_rate": 24000,
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"sample_width_bytes": 2,
|
"sample_width_bytes": 2,
|
||||||
"duration_seconds": 8.72
|
"duration_seconds": 8.88
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"record_name": "testing_17",
|
||||||
|
"text": "يا هلا مسهلا معاك بُوسْنَيْدَة،\nحاب تعمل معي فيديو مميز؟\nتواصل وياي وانا فريقي جاهزين خلنا نبدع معا ونعمل ترند.",
|
||||||
|
"replay_count": 1,
|
||||||
|
"timeline": {
|
||||||
|
"audio_generated_at": "2026-05-12 10:42:31",
|
||||||
|
"last_playback_finished_at": "2026-05-12 10:42:40",
|
||||||
|
"saved_at": "2026-05-12 10:42:42"
|
||||||
|
},
|
||||||
|
"audio_capture": {
|
||||||
|
"backend": "parec",
|
||||||
|
"sink": "alsa_output.platform-sound.analog-stereo",
|
||||||
|
"monitor_source": "alsa_output.platform-sound.analog-stereo.monitor",
|
||||||
|
"restored_microphone_source": "alsa_input.platform-sound.analog-stereo"
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"speaker_recording": {
|
||||||
|
"name": "testing_17.wav",
|
||||||
|
"path": "testing_17.wav",
|
||||||
|
"size_bytes": 426240,
|
||||||
|
"sample_rate": 24000,
|
||||||
|
"channels": 1,
|
||||||
|
"sample_width_bytes": 2,
|
||||||
|
"duration_seconds": 8.88
|
||||||
|
},
|
||||||
|
"gemini_raw_output": {
|
||||||
|
"name": "testing_17_raw.wav",
|
||||||
|
"path": "testing_17_raw.wav",
|
||||||
|
"size_bytes": 426240,
|
||||||
|
"sample_rate": 24000,
|
||||||
|
"channels": 1,
|
||||||
|
"sample_width_bytes": 2,
|
||||||
|
"duration_seconds": 8.88
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
156
shell_scripts/clean_sanad.sh
Executable file
156
shell_scripts/clean_sanad.sh
Executable file
@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# clean_sanad.sh — wipe transient state (logs, recordings, audio, caches).
|
||||||
|
#
|
||||||
|
# Safe by default: shows a preview + asks for confirmation. Won't touch
|
||||||
|
# config files, skills.json, wake_phrases.json, recorded JSONL motions,
|
||||||
|
# or the model directory.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./clean_sanad.sh # interactive — preview + y/N prompt
|
||||||
|
# ./clean_sanad.sh -y # skip the prompt
|
||||||
|
# ./clean_sanad.sh --dry-run # show what would be deleted; delete nothing
|
||||||
|
# ./clean_sanad.sh --logs # logs only
|
||||||
|
# ./clean_sanad.sh --records # recorded turns + typed-replay audio only
|
||||||
|
# ./clean_sanad.sh --cache # __pycache__ only
|
||||||
|
# ./clean_sanad.sh --all # everything (default)
|
||||||
|
#
|
||||||
|
# Override the project location:
|
||||||
|
# SANAD_HOME=/some/path ./clean_sanad.sh
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SANAD_HOME="${SANAD_HOME:-$HOME/Sanad}"
|
||||||
|
if [ ! -d "$SANAD_HOME" ]; then
|
||||||
|
# Fallback for invocation from the repo (workstation)
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SANAD_HOME="$(dirname "$SCRIPT_DIR")"
|
||||||
|
fi
|
||||||
|
if [ ! -d "$SANAD_HOME" ]; then
|
||||||
|
echo "Sanad dir not found: $SANAD_HOME" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── flag parsing ──────────────────────────────────────────
|
||||||
|
TARGET="all"
|
||||||
|
DRY_RUN=0
|
||||||
|
ASSUME_YES=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-y|--yes) ASSUME_YES=1 ;;
|
||||||
|
-n|--dry-run) DRY_RUN=1 ;;
|
||||||
|
--logs) TARGET="logs" ;;
|
||||||
|
--records) TARGET="records" ;;
|
||||||
|
--cache) TARGET="cache" ;;
|
||||||
|
--all) TARGET="all" ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,20p' "$0"; exit 0 ;;
|
||||||
|
*)
|
||||||
|
echo "unknown option: $arg (try -h)" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── targets — grouped per category so we can summarise per-category ──
|
||||||
|
declare -a LOGS_PATHS=()
|
||||||
|
declare -a RECORDS_PATHS=()
|
||||||
|
declare -a CACHE_PATHS=()
|
||||||
|
|
||||||
|
collect_logs() {
|
||||||
|
while IFS= read -r p; do LOGS_PATHS+=("$p"); done < <(
|
||||||
|
find "$SANAD_HOME/logs" -maxdepth 2 -type f 2>/dev/null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
collect_records() {
|
||||||
|
while IFS= read -r p; do RECORDS_PATHS+=("$p"); done < <(
|
||||||
|
find "$SANAD_HOME/data/recordings" -type f 2>/dev/null
|
||||||
|
)
|
||||||
|
while IFS= read -r p; do RECORDS_PATHS+=("$p"); done < <(
|
||||||
|
find "$SANAD_HOME/data/audio" -maxdepth 1 -type f \
|
||||||
|
\( -name "*.wav" -o -name "*.pcm" \) 2>/dev/null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
collect_cache() {
|
||||||
|
while IFS= read -r p; do CACHE_PATHS+=("$p"); done < <(
|
||||||
|
find "$SANAD_HOME" -type d -name "__pycache__" 2>/dev/null
|
||||||
|
)
|
||||||
|
while IFS= read -r p; do CACHE_PATHS+=("$p"); done < <(
|
||||||
|
find "$SANAD_HOME" -type f -name "*.pyc" 2>/dev/null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$TARGET" in
|
||||||
|
logs) collect_logs ;;
|
||||||
|
records) collect_records ;;
|
||||||
|
cache) collect_cache ;;
|
||||||
|
all) collect_logs; collect_records; collect_cache ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Bytes per array (silently skips missing paths)
|
||||||
|
sum_bytes() {
|
||||||
|
local total=0 p sz
|
||||||
|
for p in "$@"; do
|
||||||
|
[ -e "$p" ] || continue
|
||||||
|
sz=$(du -sb "$p" 2>/dev/null | awk '{print $1}')
|
||||||
|
total=$((total + ${sz:-0}))
|
||||||
|
done
|
||||||
|
echo "$total"
|
||||||
|
}
|
||||||
|
fmt() { numfmt --to=iec --suffix=B "$1" 2>/dev/null || echo "${1} B"; }
|
||||||
|
|
||||||
|
LOGS_BYTES=$(sum_bytes "${LOGS_PATHS[@]:-}")
|
||||||
|
RECORDS_BYTES=$(sum_bytes "${RECORDS_PATHS[@]:-}")
|
||||||
|
CACHE_BYTES=$(sum_bytes "${CACHE_PATHS[@]:-}")
|
||||||
|
TOTAL_BYTES=$((LOGS_BYTES + RECORDS_BYTES + CACHE_BYTES))
|
||||||
|
TOTAL_COUNT=$(( ${#LOGS_PATHS[@]} + ${#RECORDS_PATHS[@]} + ${#CACHE_PATHS[@]} ))
|
||||||
|
|
||||||
|
# ── preview ───────────────────────────────────────────────
|
||||||
|
if [ "$TOTAL_COUNT" -eq 0 ]; then
|
||||||
|
echo "Nothing to delete (target=$TARGET)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Sanad clean — target=$TARGET dry_run=$DRY_RUN"
|
||||||
|
echo "Project: $SANAD_HOME"
|
||||||
|
echo
|
||||||
|
printf '┌──────────────┬─────────┬────────────┐\n'
|
||||||
|
printf '│ %-12s │ %7s │ %10s │\n' "Category" "Items" "Size"
|
||||||
|
printf '├──────────────┼─────────┼────────────┤\n'
|
||||||
|
if [ "${#LOGS_PATHS[@]}" -gt 0 ]; then printf '│ %-12s │ %7d │ %10s │\n' "logs" "${#LOGS_PATHS[@]}" "$(fmt "$LOGS_BYTES")"; fi
|
||||||
|
if [ "${#RECORDS_PATHS[@]}" -gt 0 ]; then printf '│ %-12s │ %7d │ %10s │\n' "records" "${#RECORDS_PATHS[@]}" "$(fmt "$RECORDS_BYTES")"; fi
|
||||||
|
if [ "${#CACHE_PATHS[@]}" -gt 0 ]; then printf '│ %-12s │ %7d │ %10s │\n' "cache" "${#CACHE_PATHS[@]}" "$(fmt "$CACHE_BYTES")"; fi
|
||||||
|
printf '├──────────────┼─────────┼────────────┤\n'
|
||||||
|
printf '│ %-12s │ %7d │ %10s │\n' "TOTAL" "$TOTAL_COUNT" "$(fmt "$TOTAL_BYTES")"
|
||||||
|
printf '└──────────────┴─────────┴────────────┘\n'
|
||||||
|
|
||||||
|
# Flatten for the delete loop
|
||||||
|
declare -a PATHS_TO_DELETE=( "${LOGS_PATHS[@]:-}" "${RECORDS_PATHS[@]:-}" "${CACHE_PATHS[@]:-}" )
|
||||||
|
# Strip any empty entries the unset-array fallback may have introduced
|
||||||
|
PATHS_TO_DELETE=("${PATHS_TO_DELETE[@]/#/}")
|
||||||
|
TMP_PATHS=()
|
||||||
|
for p in "${PATHS_TO_DELETE[@]}"; do [ -n "$p" ] && TMP_PATHS+=("$p"); done
|
||||||
|
PATHS_TO_DELETE=("${TMP_PATHS[@]}")
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "Dry run — nothing deleted."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$ASSUME_YES" -ne 1 ]; then
|
||||||
|
read -r -p "Proceed with delete? [y/N] " ans
|
||||||
|
case "$ans" in
|
||||||
|
y|Y|yes|YES) ;;
|
||||||
|
*) echo "Aborted."; exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── delete ────────────────────────────────────────────────
|
||||||
|
removed=0
|
||||||
|
for p in "${PATHS_TO_DELETE[@]}"; do
|
||||||
|
rm -rf -- "$p" && removed=$((removed + 1))
|
||||||
|
done
|
||||||
|
echo "Removed $removed of ${#PATHS_TO_DELETE[@]} item(s)."
|
||||||
|
|
||||||
|
# Recreate empty top-level dirs so next start_sanad.sh run doesn't
|
||||||
|
# complain about missing paths.
|
||||||
|
mkdir -p "$SANAD_HOME/logs" "$SANAD_HOME/data/recordings" "$SANAD_HOME/data/audio"
|
||||||
|
echo "Done."
|
||||||
31
shell_scripts/sanad.service
Normal file
31
shell_scripts/sanad.service
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# systemd user-level unit for Sanad. Install with:
|
||||||
|
#
|
||||||
|
# mkdir -p ~/.config/systemd/user
|
||||||
|
# cp ~/Sanad/shell_scripts/sanad.service ~/.config/systemd/user/sanad.service
|
||||||
|
# systemctl --user daemon-reload
|
||||||
|
# systemctl --user enable --now sanad.service
|
||||||
|
# sudo loginctl enable-linger unitree # run at boot even when not logged in
|
||||||
|
#
|
||||||
|
# Watch logs:
|
||||||
|
# journalctl --user -u sanad.service -f
|
||||||
|
#
|
||||||
|
# Restart after a code/config change:
|
||||||
|
# systemctl --user restart sanad.service
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Sanad robot assistant (FastAPI dashboard + voice/motion subsystems)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=exec
|
||||||
|
WorkingDirectory=%h/Sanad
|
||||||
|
ExecStart=/usr/bin/env bash %h/Sanad/shell_scripts/start_sanad.sh
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
TimeoutStopSec=15
|
||||||
|
KillSignal=SIGINT
|
||||||
|
PassEnvironment=PULSE_RUNTIME_PATH XDG_RUNTIME_DIR
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
64
shell_scripts/start_sanad.sh
Executable file
64
shell_scripts/start_sanad.sh
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# start_sanad.sh — boot Sanad's main.py inside the gemini_sdk conda env.
|
||||||
|
#
|
||||||
|
# Used both manually (./start_sanad.sh) and from the systemd unit
|
||||||
|
# (sanad.service) for boot-time auto-start.
|
||||||
|
#
|
||||||
|
# Override knobs (env vars; all optional):
|
||||||
|
# SANAD_HOME project root (default ~/Sanad)
|
||||||
|
# SANAD_CONDA_ENV conda env name (default gemini_sdk)
|
||||||
|
# SANAD_CONDA_BASE conda install dir (default $HOME/miniconda3)
|
||||||
|
# SANAD_DDS_INTERFACE DDS network iface (default eth0)
|
||||||
|
# SANAD_VOICE_BRAIN gemini | local | model (default gemini)
|
||||||
|
# SANAD_AUDIO_PROFILE builtin | anker | hollyland_builtin (default builtin)
|
||||||
|
# PORT dashboard port (default 8000)
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SANAD_HOME="${SANAD_HOME:-$HOME/Sanad}"
|
||||||
|
SANAD_CONDA_ENV="${SANAD_CONDA_ENV:-gemini_sdk}"
|
||||||
|
SANAD_CONDA_BASE="${SANAD_CONDA_BASE:-$HOME/miniconda3}"
|
||||||
|
|
||||||
|
export SANAD_DDS_INTERFACE="${SANAD_DDS_INTERFACE:-eth0}"
|
||||||
|
export SANAD_VOICE_BRAIN="${SANAD_VOICE_BRAIN:-gemini}"
|
||||||
|
export SANAD_AUDIO_PROFILE="${SANAD_AUDIO_PROFILE:-builtin}"
|
||||||
|
export PORT="${PORT:-8000}"
|
||||||
|
|
||||||
|
# Mandatory environment fixes for Jetson + conda + Unitree SDK
|
||||||
|
export LD_PRELOAD="${LD_PRELOAD:-/usr/lib/aarch64-linux-gnu/libgomp.so.1}"
|
||||||
|
export PYTHONUNBUFFERED=1
|
||||||
|
# Optional: needed if/when the local pipeline imports CosyVoice
|
||||||
|
if [ -d "$HOME/CosyVoice" ]; then
|
||||||
|
export PYTHONPATH="$HOME/CosyVoice:$HOME/CosyVoice/third_party/Matcha-TTS:${PYTHONPATH:-}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$SANAD_HOME" || { echo "Sanad dir not found: $SANAD_HOME" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Activate conda
|
||||||
|
if [ ! -f "$SANAD_CONDA_BASE/etc/profile.d/conda.sh" ]; then
|
||||||
|
echo "Conda not found at $SANAD_CONDA_BASE — set SANAD_CONDA_BASE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$SANAD_CONDA_BASE/etc/profile.d/conda.sh"
|
||||||
|
conda activate "$SANAD_CONDA_ENV" || {
|
||||||
|
echo "Conda env '$SANAD_CONDA_ENV' activation failed" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for the DDS interface to come up — robot may still be booting
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
if ip link show "$SANAD_DDS_INTERFACE" 2>/dev/null | grep -q "state UP"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[start_sanad] $(date) — launching main.py"
|
||||||
|
echo "[start_sanad] SANAD_HOME=$SANAD_HOME"
|
||||||
|
echo "[start_sanad] conda env=$SANAD_CONDA_ENV"
|
||||||
|
echo "[start_sanad] DDS iface=$SANAD_DDS_INTERFACE"
|
||||||
|
echo "[start_sanad] voice brain=$SANAD_VOICE_BRAIN audio=$SANAD_AUDIO_PROFILE"
|
||||||
|
echo "[start_sanad] port=$PORT"
|
||||||
|
|
||||||
|
exec python3 main.py --port "$PORT"
|
||||||
73
voice/live_voice.py
Normal file
73
voice/live_voice.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""Live Voice Commands — voice-to-arm phrase trigger dispatcher.
|
||||||
|
|
||||||
|
Listens to GeminiSubprocess user transcripts, matches against
|
||||||
|
sanad_arm.txt phrases, and fires ARM.trigger_action_by_id.
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
POST /start begin polling transcripts
|
||||||
|
POST /stop stop polling
|
||||||
|
POST /deferred-mode?enabled toggle instant vs deferred trigger
|
||||||
|
POST /trigger-enabled?enabled master gate — allow arm actions or not
|
||||||
|
GET /status running, last heard, last action, etc.
|
||||||
|
GET /triggers arm trigger history (log)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def _loop():
|
||||||
|
from Project.Sanad.main import live_voice
|
||||||
|
if live_voice is None:
|
||||||
|
raise HTTPException(503, "LiveVoiceLoop not initialized.")
|
||||||
|
return live_voice
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status")
|
||||||
|
async def status():
|
||||||
|
from Project.Sanad.main import live_voice
|
||||||
|
if live_voice is None:
|
||||||
|
return {"available": False}
|
||||||
|
return {"available": True, **live_voice.status()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start")
|
||||||
|
async def start():
|
||||||
|
loop = _loop()
|
||||||
|
await loop.start()
|
||||||
|
return {"ok": True, **loop.status()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/stop")
|
||||||
|
async def stop():
|
||||||
|
loop = _loop()
|
||||||
|
await loop.stop()
|
||||||
|
return {"ok": True, **loop.status()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/deferred-mode")
|
||||||
|
async def set_deferred(enabled: bool):
|
||||||
|
loop = _loop()
|
||||||
|
loop.set_deferred(enabled)
|
||||||
|
return {"ok": True, "deferred_mode": loop.deferred_mode}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/trigger-enabled")
|
||||||
|
async def set_trigger_enabled(enabled: bool):
|
||||||
|
"""Master gate for voice → arm triggering. Default OFF."""
|
||||||
|
loop = _loop()
|
||||||
|
loop.set_trigger_enabled(enabled)
|
||||||
|
return {"ok": True, "trigger_enabled": loop.trigger_enabled}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/triggers")
|
||||||
|
async def triggers():
|
||||||
|
loop = _loop()
|
||||||
|
return {
|
||||||
|
"triggers": list(loop.triggers),
|
||||||
|
"total": len(loop.triggers),
|
||||||
|
"dispatch_actions": len(loop.wake_dispatch),
|
||||||
|
}
|
||||||
@ -90,10 +90,20 @@ class LiveVoiceLoop:
|
|||||||
from Project.Sanad.motion.sanad_arm_controller import ARM, OPTION_LIST, OPTION_BY_ID
|
from Project.Sanad.motion.sanad_arm_controller import ARM, OPTION_LIST, OPTION_BY_ID
|
||||||
self.sanad_arm = ARM
|
self.sanad_arm = ARM
|
||||||
self.option_by_id = OPTION_BY_ID
|
self.option_by_id = OPTION_BY_ID
|
||||||
|
# Voice-trigger policy: SDK built-ins only.
|
||||||
|
# JSONL replays (option.file set) are dashboard-only — voice
|
||||||
|
# phrase blocks for laugh/bird/change_battery/move_* never
|
||||||
|
# reach `wake_dispatch`, so a matched phrase for one of those
|
||||||
|
# silently no-ops in voice mode.
|
||||||
|
sdk_only_options = [o for o in OPTION_LIST if not getattr(o, "file", "")]
|
||||||
if SANAD_ARM_TXT.exists():
|
if SANAD_ARM_TXT.exists():
|
||||||
self.wake_dispatch = load_arm_phrase_dispatch(SANAD_ARM_TXT, OPTION_LIST)
|
self.wake_dispatch = load_arm_phrase_dispatch(SANAD_ARM_TXT, sdk_only_options)
|
||||||
log.info("loaded %d arm-action phrase sets from %s",
|
log.info(
|
||||||
len(self.wake_dispatch), SANAD_ARM_TXT.name)
|
"loaded %d arm-action phrase sets from %s "
|
||||||
|
"(SDK-only filter: %d/%d options)",
|
||||||
|
len(self.wake_dispatch), SANAD_ARM_TXT.name,
|
||||||
|
len(sdk_only_options), len(OPTION_LIST),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
log.warning("sanad_arm.txt missing at %s — arm trigger disabled",
|
log.warning("sanad_arm.txt missing at %s — arm trigger disabled",
|
||||||
SANAD_ARM_TXT)
|
SANAD_ARM_TXT)
|
||||||
|
|||||||
75
voice/voice_config.json
Normal file
75
voice/voice_config.json
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"_description": "Tunables for voice/* modules. Loaded via core.config_loader.load('voice').",
|
||||||
|
|
||||||
|
"sanad_voice": {
|
||||||
|
"_comment": "voice/sanad_voice.py — main live voice subprocess. Gemini API credentials (api_key, model, voice_name) come from core_config.json's gemini_defaults — single source of truth.",
|
||||||
|
"mic_gain": 1.0,
|
||||||
|
"play_chunk_bytes": 96000,
|
||||||
|
"log_dir": "~/logs",
|
||||||
|
"log_name": "gemini_live_v2",
|
||||||
|
"session_timeout_sec": 660,
|
||||||
|
"max_reconnect_delay_sec": 30,
|
||||||
|
"max_consecutive_errors": 10,
|
||||||
|
"no_messages_timeout_sec": 30
|
||||||
|
},
|
||||||
|
|
||||||
|
"mic_udp": {
|
||||||
|
"_comment": "G1 built-in mic — UDP multicast subscriber",
|
||||||
|
"group": "239.168.123.161",
|
||||||
|
"port": 5555,
|
||||||
|
"buffer_max_bytes": 64000,
|
||||||
|
"read_timeout_sec": 0.04,
|
||||||
|
"socket_timeout_sec": 1.0
|
||||||
|
},
|
||||||
|
|
||||||
|
"speaker": {
|
||||||
|
"_comment": "G1 built-in speaker — AudioClient.PlayStream wrapper",
|
||||||
|
"app_name": "sanad",
|
||||||
|
"begin_stream_pause_sec": 0.15,
|
||||||
|
"wait_finish_margin_sec": 0.3
|
||||||
|
},
|
||||||
|
|
||||||
|
"vad": {
|
||||||
|
"_comment": "Gemini Live server-side voice-activity-detection config",
|
||||||
|
"start_sensitivity": "START_SENSITIVITY_HIGH",
|
||||||
|
"end_sensitivity": "END_SENSITIVITY_LOW",
|
||||||
|
"prefix_padding_ms": 20,
|
||||||
|
"silence_duration_ms": 200
|
||||||
|
},
|
||||||
|
|
||||||
|
"barge_in": {
|
||||||
|
"threshold": 500,
|
||||||
|
"loud_chunks_needed": 3,
|
||||||
|
"cooldown_sec": 0.3,
|
||||||
|
"echo_suppress_below": 500,
|
||||||
|
"ai_speak_grace_sec": 0.15
|
||||||
|
},
|
||||||
|
|
||||||
|
"recording": {
|
||||||
|
"enabled": true,
|
||||||
|
"dir_relative": "data/recordings"
|
||||||
|
},
|
||||||
|
|
||||||
|
"typed_replay": {
|
||||||
|
"_comment": "voice/typed_replay.py — max_text_len comes from dashboard.api_input",
|
||||||
|
"monitor_chunk_size": 512,
|
||||||
|
"monitor_tail_sec": 0.2
|
||||||
|
},
|
||||||
|
|
||||||
|
"live_voice_loop": {
|
||||||
|
"_comment": "voice/live_voice_loop.py — arm phrase dispatcher. arm_txt filename comes from core.script_files.arm_phrases",
|
||||||
|
"trigger_log_size": 100,
|
||||||
|
"poll_interval_sec": 0.1,
|
||||||
|
"deferred_default": false,
|
||||||
|
"trigger_enabled_default": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"local_tts": {
|
||||||
|
"_comment": "voice/local_tts.py — offline Coqui TTS",
|
||||||
|
"model_subdir": "speecht5_tts_clartts_ar",
|
||||||
|
"vocoder_subdir": "speecht5_hifigan",
|
||||||
|
"xvector_filename": "arabic_xvector_embedding.pt",
|
||||||
|
"sample_rate": 16000,
|
||||||
|
"channels": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user