#!/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 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= 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:-}" echo "TTS model : ${TTS_MODEL:-}" echo "TTS voice : ${TTS_VOICE:-}" echo "Cue voice profile : $CUE_VOICE_PROFILE" echo "Reference audio : ${TTS_REFERENCE_AUDIO:-}" 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:-} / ${TTS_TOP_P:-} / ${TTS_TOP_K:-}" echo "Style tag : ${TTS_STYLE_TAG:-}" 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:-}" "$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'"