client sdk

This commit is contained in:
valenti
2026-10-21 17:56:04 +01:00
parent 1cafe3dd3c
commit d6bc2437c9
237 changed files with 5227 additions and 1019 deletions
+413
View File
@@ -0,0 +1,413 @@
#!/usr/bin/env bash
#
# Analyze the current TTS voice / cue-asset settings from `.env`, decide which
# generated cue banks are stale, delete the generated files, and restart the
# pm2 service so the engine regenerates them at startup.
#
# The script is intentionally conservative:
# - by default it inspects every language subdirectory currently present under
# `assets/fillers`, `assets/start_filler` and `assets/backchannel`, plus
# `LANG_DEFAULT`;
# - it stores a per-directory voice signature in `.voice_signature`;
# - if the signature did not change, it leaves the bank alone;
# - `--force` overrides that check and refreshes the selected banks anyway.
#
# Usage examples:
# ./run/refresh_voice_assets.sh
# ./run/refresh_voice_assets.sh --dry-run
# ./run/refresh_voice_assets.sh --lang es --lang en
# ./run/refresh_voice_assets.sh --base-lang-only --force
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ENV_FILE="$PROJECT_DIR/.env"
PM2_APP="${PM2_APP:-pm2-piperust}"
SIGNATURE_FILE=".voice_signature"
DRY_RUN=0
FORCE=0
BASE_LANG_ONLY=0
NO_RESTART=0
declare -a REQUESTED_LANGS=()
if [ ! -f "$ENV_FILE" ]; then
echo "[refresh_voice_assets] missing .env: $ENV_FILE" >&2
exit 1
fi
usage() {
cat <<'EOF'
refresh_voice_assets.sh
Analyze voice-related `.env` settings, refresh stale cue asset banks, and
restart the pm2 service so the engine regenerates them.
Options:
--dry-run Print what would be refreshed, do not delete or restart
--force Refresh the selected banks even when the signature matches
--lang <code> Restrict the refresh to one language (repeatable)
--base-lang-only Restrict the refresh to LANG_DEFAULT
--no-restart Delete files but do not restart pm2
--help Show this help
Environment override:
PM2_APP=<name> pm2 app to restart (default: pm2-piperust)
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--force) FORCE=1 ;;
--base-lang-only) BASE_LANG_ONLY=1 ;;
--no-restart) NO_RESTART=1 ;;
--lang)
shift
[ $# -gt 0 ] || { echo "[refresh_voice_assets] --lang requires a value" >&2; exit 1; }
REQUESTED_LANGS+=("$1")
;;
--help|-h)
usage
exit 0
;;
*)
echo "[refresh_voice_assets] unknown argument: $1" >&2
usage >&2
exit 1
;;
esac
shift
done
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
TTS_ENGINE="${TTS_ENGINE:-fish}"
LANG_DEFAULT="${LANG_DEFAULT:-es}"
FILLERS_DIR="${FILLERS_DIR:-assets/fillers}"
START_FILLER_DIR="${START_FILLER_DIR:-assets/start_filler}"
BACKCHANNEL_DIR="${BACKCHANNEL_DIR:-assets/backchannel}"
TTS_MODEL="${TTS_MODEL:-}"
TTS_VOICE="${TTS_VOICE:-}"
ASSET_VOICE_PROFILE="${ASSET_VOICE_PROFILE:-}"
TTS_REFERENCE_AUDIO="${TTS_REFERENCE_AUDIO:-}"
TTS_REFERENCE_TEXT="${TTS_REFERENCE_TEXT:-}"
TTS_TEMPERATURE="${TTS_TEMPERATURE:-}"
TTS_TOP_P="${TTS_TOP_P:-}"
TTS_TOP_K="${TTS_TOP_K:-}"
TTS_STYLE_TAG="${TTS_STYLE_TAG:-}"
SGLANG_URL="${SGLANG_URL:-}"
KOKORO_URL="${KOKORO_URL:-}"
KOKORO_VOICE="${KOKORO_VOICE:-}"
START_FILLER_TEXTS="${START_FILLER_TEXTS:-}"
BACKCHANNEL_TEXTS="${BACKCHANNEL_TEXTS:-}"
abs_path() {
local p="$1"
if [[ "$p" = /* ]]; then
printf '%s\n' "$p"
else
printf '%s\n' "$PROJECT_DIR/$p"
fi
}
sha_file() {
local path="$1"
if [ -f "$path" ]; then
sha256sum "$path" | awk '{print $1}'
else
printf 'missing'
fi
}
sha_text() {
printf '%s' "$1" | sha256sum | awk '{print $1}'
}
count_entries() {
printf '%s' "$1" | tr '|' '\n' | awk 'NF { c++ } END { print c + 0 }'
}
sanitize_asset_segment() {
local raw="$1"
printf '%s' "$raw" \
| tr '[:upper:]' '[:lower:]' \
| sed -E 's/[^a-z0-9._ -]+/_/g; s/[ .-]+/_/g; s/^_+//; s/_+$//'
}
cue_voice_profile() {
local explicit
explicit="$(sanitize_asset_segment "$ASSET_VOICE_PROFILE")"
if [ -n "$explicit" ]; then
printf '%s\n' "$explicit"
return 0
fi
local voice
voice="$(sanitize_asset_segment "$TTS_VOICE")"
if [ -n "$voice" ] && [ "$voice" != "default" ]; then
printf '%s\n' "$voice"
return 0
fi
if [ -n "$TTS_REFERENCE_AUDIO" ]; then
local stem
stem="$(basename "${TTS_REFERENCE_AUDIO%.*}")"
stem="$(sanitize_asset_segment "$stem")"
if [ -n "$stem" ]; then
printf '%s\n' "$stem"
return 0
fi
fi
if [ -n "$voice" ]; then
printf '%s\n' "$voice"
else
printf 'default\n'
fi
}
read_reference_text_mode() {
if [ -z "$TTS_REFERENCE_TEXT" ]; then
printf 'auto'
elif [ -f "$TTS_REFERENCE_TEXT" ]; then
printf 'file'
else
printf 'inline'
fi
}
reference_text_hash() {
if [ -z "$TTS_REFERENCE_TEXT" ]; then
printf 'auto'
elif [ -f "$TTS_REFERENCE_TEXT" ]; then
sha_file "$TTS_REFERENCE_TEXT"
else
sha_text "$TTS_REFERENCE_TEXT"
fi
}
voice_signature() {
{
printf 'TTS_ENGINE=%s\n' "$TTS_ENGINE"
printf 'LANG_DEFAULT=%s\n' "$LANG_DEFAULT"
printf 'SGLANG_URL=%s\n' "$SGLANG_URL"
printf 'TTS_MODEL=%s\n' "$TTS_MODEL"
printf 'TTS_VOICE=%s\n' "$TTS_VOICE"
printf 'ASSET_VOICE_PROFILE=%s\n' "$ASSET_VOICE_PROFILE"
printf 'TTS_REFERENCE_AUDIO=%s\n' "$TTS_REFERENCE_AUDIO"
printf 'TTS_REFERENCE_AUDIO_HASH=%s\n' "$(sha_file "$TTS_REFERENCE_AUDIO")"
printf 'TTS_REFERENCE_TEXT_MODE=%s\n' "$(read_reference_text_mode)"
printf 'TTS_REFERENCE_TEXT_HASH=%s\n' "$(reference_text_hash)"
printf 'TTS_TEMPERATURE=%s\n' "$TTS_TEMPERATURE"
printf 'TTS_TOP_P=%s\n' "$TTS_TOP_P"
printf 'TTS_TOP_K=%s\n' "$TTS_TOP_K"
printf 'TTS_STYLE_TAG=%s\n' "$TTS_STYLE_TAG"
printf 'KOKORO_URL=%s\n' "$KOKORO_URL"
printf 'KOKORO_VOICE=%s\n' "$KOKORO_VOICE"
printf 'START_FILLER_TEXTS_HASH=%s\n' "$(sha_text "$START_FILLER_TEXTS")"
printf 'BACKCHANNEL_TEXTS_HASH=%s\n' "$(sha_text "$BACKCHANNEL_TEXTS")"
} | sha256sum | awk '{print $1}'
}
print_summary() {
echo "Project : $PROJECT_DIR"
echo "Env file : $ENV_FILE"
echo "pm2 app : $PM2_APP"
echo "TTS engine : $TTS_ENGINE"
echo "SGLang URL : ${SGLANG_URL:-<empty>}"
echo "TTS model : ${TTS_MODEL:-<empty>}"
echo "TTS voice : ${TTS_VOICE:-<empty>}"
echo "Cue voice profile : $CUE_VOICE_PROFILE"
echo "Reference audio : ${TTS_REFERENCE_AUDIO:-<empty>}"
echo "Reference audio hash : $(sha_file "$TTS_REFERENCE_AUDIO")"
echo "Reference text mode : $(read_reference_text_mode)"
echo "Reference text hash : $(reference_text_hash)"
echo "Temperature / top-p/k : ${TTS_TEMPERATURE:-<empty>} / ${TTS_TOP_P:-<empty>} / ${TTS_TOP_K:-<empty>}"
echo "Style tag : ${TTS_STYLE_TAG:-<empty>}"
echo "LANG_DEFAULT : $LANG_DEFAULT"
echo "FILLERS_DIR : $(abs_path "$FILLERS_DIR")"
echo "START_FILLER_DIR : $(abs_path "$START_FILLER_DIR")"
echo "BACKCHANNEL_DIR : $(abs_path "$BACKCHANNEL_DIR")"
echo "START_FILLER_TEXTS : $(count_entries "$START_FILLER_TEXTS") entries"
echo "BACKCHANNEL_TEXTS : $(count_entries "$BACKCHANNEL_TEXTS") entries"
echo "Voice signature : $CURRENT_SIGNATURE"
}
declare -A LANGS=()
add_lang() {
local lang="$1"
[ -n "$lang" ] || return 0
LANGS["$lang"]=1
}
collect_languages_from_dir() {
local base_dir="$1"
[ -d "$base_dir" ] || return 0
local d
for d in "$base_dir"/*; do
[ -d "$d" ] || continue
add_lang "$(basename "$d")"
done
}
FILLERS_ABS="$(abs_path "$FILLERS_DIR")"
START_FILLER_ABS="$(abs_path "$START_FILLER_DIR")"
BACKCHANNEL_ABS="$(abs_path "$BACKCHANNEL_DIR")"
CUE_VOICE_PROFILE="$(cue_voice_profile)"
CURRENT_SIGNATURE="$(voice_signature)"
if [ "${#REQUESTED_LANGS[@]}" -gt 0 ]; then
for lang in "${REQUESTED_LANGS[@]}"; do
add_lang "$lang"
done
elif [ "$BASE_LANG_ONLY" -eq 1 ]; then
add_lang "$LANG_DEFAULT"
else
add_lang "$LANG_DEFAULT"
collect_languages_from_dir "$FILLERS_ABS"
collect_languages_from_dir "$START_FILLER_ABS"
collect_languages_from_dir "$BACKCHANNEL_ABS"
fi
mapfile -t TARGET_LANGS < <(printf '%s\n' "${!LANGS[@]}" | sort)
bank_dir() {
local bank_root="$1"
local lang="$2"
printf '%s/%s/%s\n' "$bank_root" "$lang" "$CUE_VOICE_PROFILE"
}
count_raw() {
local dir="$1"
if [ -d "$dir" ]; then
find "$dir" -maxdepth 1 -type f -name '*.raw' | wc -l | awk '{print $1}'
else
printf '0'
fi
}
saved_signature() {
local dir="$1"
local path="$dir/$SIGNATURE_FILE"
[ -f "$path" ] && cat "$path" || true
}
refresh_reason() {
local dir="$1"
local saved="$2"
if [ "$FORCE" -eq 1 ]; then
printf 'forced'
elif [ ! -d "$dir" ]; then
printf 'missing dir'
elif [ -z "$saved" ]; then
printf 'missing signature'
elif [ "$saved" != "$CURRENT_SIGNATURE" ]; then
printf 'signature changed'
else
printf ''
fi
}
clean_bank() {
local kind="$1"
local dir="$2"
mkdir -p "$dir"
find "$dir" -maxdepth 1 -type f -name '*.raw' -delete
rm -f "$dir/$SIGNATURE_FILE"
if [ "$kind" = "start-filler" ] || [ "$kind" = "backchannel" ]; then
rm -f "$dir/texts.txt"
fi
}
write_signature_marker() {
local dir="$1"
mkdir -p "$dir"
printf '%s\n' "$CURRENT_SIGNATURE" > "$dir/$SIGNATURE_FILE"
}
NEEDS_REFRESH=0
declare -a PLAN=()
print_summary
echo
echo "Target languages : ${TARGET_LANGS[*]}"
echo
printf '%-14s %-6s %-18s %-8s %-64s %s\n' "Bank" "Lang" "Voice profile" "Clips" "Saved signature" "Action"
printf '%-14s %-6s %-18s %-8s %-64s %s\n' "--------------" "------" "------------------" "--------" "----------------------------------------------------------------" "----------------"
for lang in "${TARGET_LANGS[@]}"; do
for kind in filler start-filler backchannel; do
case "$kind" in
filler) dir="$(bank_dir "$FILLERS_ABS" "$lang")" ;;
start-filler) dir="$(bank_dir "$START_FILLER_ABS" "$lang")" ;;
backchannel) dir="$(bank_dir "$BACKCHANNEL_ABS" "$lang")" ;;
esac
clips="$(count_raw "$dir")"
saved="$(saved_signature "$dir")"
reason="$(refresh_reason "$dir" "$saved")"
if [ -n "$reason" ]; then
action="refresh ($reason)"
NEEDS_REFRESH=1
PLAN+=("$kind::$dir")
else
action="keep"
fi
printf '%-14s %-6s %-18s %-8s %-64s %s\n' "$kind" "$lang" "$CUE_VOICE_PROFILE" "$clips" "${saved:-<none>}" "$action"
done
done
if [ "$NEEDS_REFRESH" -eq 0 ]; then
echo
echo "No refresh needed: signatures already match the current .env voice settings."
exit 0
fi
echo
if [ "$DRY_RUN" -eq 1 ]; then
echo "Dry run only: no files deleted, no pm2 restart."
exit 0
fi
echo "Cleaning stale/generated cue assets..."
for item in "${PLAN[@]}"; do
kind="${item%%::*}"
dir="${item#*::}"
clean_bank "$kind" "$dir"
echo " cleaned $kind -> $dir"
done
if [ "$NO_RESTART" -eq 1 ]; then
echo
echo "Files cleaned. Restart skipped (--no-restart)."
echo "Run: pm2 restart $PM2_APP --update-env"
exit 0
fi
echo
echo "Restarting pm2 app: $PM2_APP"
pm2 restart "$PM2_APP" --update-env
echo "Waiting for /health..."
for _ in $(seq 1 30); do
if curl -fsS "http://localhost:${SERVER_PORT:-8787}/health" >/dev/null 2>&1; then
break
fi
sleep 1
done
echo "Writing signature markers..."
for lang in "${TARGET_LANGS[@]}"; do
write_signature_marker "$(bank_dir "$FILLERS_ABS" "$lang")"
write_signature_marker "$(bank_dir "$START_FILLER_ABS" "$lang")"
write_signature_marker "$(bank_dir "$BACKCHANNEL_ABS" "$lang")"
done
echo
echo "Done."
echo "Tip: inspect recent regeneration logs with:"
echo " pm2 logs $PM2_APP --nostream --lines 120 | rg 'refreshing|generated|up to date|clip banks bootstrapped'"
+41
View File
@@ -0,0 +1,41 @@
import { AudioAgentClient, AudioAgentRest } from '../sdk/audio-agent-client.js';
const rest = new AudioAgentRest('http://localhost:8787');
console.log('REST health =>', JSON.stringify(await rest.health()));
const tools = await rest.getTools();
console.log('REST tools =>', tools.count, tools.source);
const client = new AudioAgentClient({ baseUrl: 'http://localhost:8787' });
const seen = [];
await new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('timeout waiting for cues')), 8000);
client.on('cues', (e) => {
seen.push(`cues(lang=${e.lang})`);
clearTimeout(timer);
resolve();
});
client.on('error', (e) => console.error('ERR event', e));
client.connect().then(() => console.log('WS connected =>', client.url)).catch(reject);
});
// Observe the turn events + decoded PCM frames.
let audioFrames = 0;
client.on('user', (e) => seen.push(`user(${JSON.stringify(e.text)})`));
client.on('assistant_delta', (e) => seen.push(`assistant_delta(len=${e.text.length})`));
client.on('assistant_end', () => seen.push('assistant_end'));
client.on('cue', (e) => seen.push(`cue(${e.kind})`));
client.on('tool_call', (e) => seen.push(`tool_call(${e.name})`));
client.on('audio', (e) => {
audioFrames++;
if (e.samples.length !== 882) seen.push(`BAD frame ${e.samples.length}`);
});
// Send a PCM frame (882 samples of silence) + a text turn, then confirm events flow.
console.log('sendAudio =>', client.sendAudio(new Int16Array(882)));
client.sendUserText('di solo "ciao"');
await new Promise((r) => setTimeout(r, 12000));
client.close();
console.log('events seen:', seen.join(', '));
console.log('audio frames decoded:', audioFrames);
process.exit(0);