#!/usr/bin/env bash
#
# Запись голосовых сэмплов на дроиде для оценки качества распознавания в шуме.
#
# Что делает:
#   1. Ставит недостающие утилиты (alsa-utils, sox).
#   2. Записывает фоновый шум локации без речи (по умолчанию 60 с).
#   3. Показывает 10 фраз для ТЦ Афимолл Сити: оператор жмёт Enter — говорит — Enter.
#   4. Даёт записать 5 свободных фраз (как сказал бы реальный посетитель).
#   5. Пишет manifest.tsv (файл, эталонный текст, уровни) и meta.txt,
#      упаковывает всё в архив для отправки.
#
# Запуск (голосовой поиск на дроиде лучше на время остановить, иначе
# микрофон может быть занят):
#   bash record_samples.sh "<площадка>" <id устройства> <расстояние>
#   bash record_samples.sh "ТЦ Афимолл Сити, фудкорт" droid-07 0.5m
#
# Что не указано — скрипт спросит. Остальное обычно менять не нужно,
# но можно через переменные окружения:
#   ALSA_DEV=plughw:1,0  микрофон (по умолчанию default; список: --list-devices)
#   RATE=16000           частота, Гц (как в проде)
#   NOISE_SEC=60         запись фона, с (0 — пропустить)
#   PHRASES_FILE=...     свои фразы, по одной на строку
#   OUT_ROOT=./samples   каталог для сессий
#
#   bash record_samples.sh --list-devices  показать микрофоны и выйти

set -euo pipefail

LOCATION=""
DEVICE_ID=""
DISTANCE=""
ALSA_DEV="${ALSA_DEV:-default}"
RATE="${RATE:-16000}"
NOISE_SEC="${NOISE_SEC:-60}"
PHRASES_FILE="${PHRASES_FILE:-}"
OUT_ROOT="${OUT_ROOT:-./samples}"
FREE_COUNT=5
LIST_DEVICES=0

DEFAULT_PHRASES=$(cat <<'EOF'
Построй маршрут до КидЗании
Как пройти в М.Видео
Где находится Л'Этуаль
Проложи маршрут до Gloria Jeans
Где магазин Lime
Как дойти до Детского мира
Как выйти к метро Выставочная
Где ближайший банкомат
Как пройти на фудкорт
Где туалет
EOF
)

die()  { echo "Ошибка: $*" >&2; exit 1; }
info() { echo -e "\033[1;34m==>\033[0m $*"; }
warn() { echo -e "\033[1;33m[!]\033[0m $*"; }

usage() { awk 'NR > 1 && /^#/ { sub(/^# ?/, ""); print; next } NR > 1 { exit }' "$0"; exit "${1:-0}"; }

# ------------------------------------------------------------------ args
case "${1:-}" in
    --list-devices) LIST_DEVICES=1 ;;
    -h|--help) usage 0 ;;
    -*) echo "Неизвестный параметр: $1" >&2; usage 1 ;;
    *)
        [ $# -le 3 ] || { echo "Слишком много аргументов" >&2; usage 1; }
        LOCATION="${1:-}"; DEVICE_ID="${2:-}"; DISTANCE="${3:-}"
        ;;
esac

# ------------------------------------------------------------------ deps
install_deps() {
    local missing=()
    command -v arecord >/dev/null 2>&1 || missing+=(alsa-utils)
    command -v sox >/dev/null 2>&1 || missing+=(sox)
    [ ${#missing[@]} -eq 0 ] && return 0

    info "Устанавливаю: ${missing[*]}"
    local sudo=""
    [ "$(id -u)" -ne 0 ] && sudo="sudo"

    if command -v apt-get >/dev/null 2>&1; then
        $sudo apt-get update -qq && $sudo apt-get install -y -qq "${missing[@]}"
    elif command -v dnf >/dev/null 2>&1; then
        $sudo dnf install -y "${missing[@]}"
    elif command -v yum >/dev/null 2>&1; then
        $sudo yum install -y "${missing[@]}"
    elif command -v pacman >/dev/null 2>&1; then
        $sudo pacman -Sy --noconfirm "${missing[@]}"
    elif command -v apk >/dev/null 2>&1; then
        $sudo apk add "${missing[@]}"
    else
        die "не найден пакетный менеджер, поставьте вручную: ${missing[*]}"
    fi
}

install_deps

if [ "$LIST_DEVICES" -eq 1 ]; then
    info "ALSA устройства записи (arecord -l):"
    arecord -l || true
    if command -v pactl >/dev/null 2>&1; then
        info "PulseAudio/PipeWire источники (pactl):"
        pactl list sources short || true
    fi
    echo
    echo "Для карты N, устройства M используйте: ALSA_DEV=plughw:N,M bash $0 ..."
    exit 0
fi

# ------------------------------------------------------------- metadata
[ -n "$LOCATION" ]  || read -r -p "Площадка (например «ТЦ Афимолл Сити, фудкорт»): " LOCATION
[ -n "$DEVICE_ID" ] || read -r -p "ID устройства [$(hostname)]: " DEVICE_ID
[ -n "$DISTANCE" ]  || read -r -p "Расстояние до микрофона (например 0.5m): " DISTANCE
DEVICE_ID="${DEVICE_ID:-$(hostname)}"
[ -n "$LOCATION" ] || die "площадка обязательна"
[ -n "$DISTANCE" ] || die "расстояние обязательно"

ts="$(date +%Y%m%d_%H%M%S)"
safe_loc="$(echo "$LOCATION" | sed 's#[[:space:]/,]\+#_#g')"
SESSION="${ts}_${DEVICE_ID}_${safe_loc}"
DIR="${OUT_ROOT}/${SESSION}"
mkdir -p "$DIR"
MANIFEST="$DIR/manifest.tsv"
ERR_LOG="$DIR/.arecord.err"

printf 'file\tref_text\tkind\tlocation\tdevice_id\tdistance\tduration_sec\trms_db\tpeak_db\tnote\n' > "$MANIFEST"

{
    echo "date:      $(date -Iseconds)"
    echo "location:  $LOCATION"
    echo "device_id: $DEVICE_ID"
    echo "hostname:  $(hostname)"
    echo "distance:  $DISTANCE"
    echo "alsa_dev:  $ALSA_DEV"
    echo "rate:      $RATE"
    echo "kernel:    $(uname -a)"
    echo
    echo "--- arecord -l"
    arecord -l 2>&1 || true
    echo
    echo "--- amixer (capture)"
    amixer sget Capture 2>&1 || true
    if command -v pactl >/dev/null 2>&1; then
        echo
        echo "--- pactl sources"
        pactl list sources short 2>&1 || true
    fi
} > "$DIR/meta.txt"

# ------------------------------------------------------------- recording
arec() {
    arecord -q -D "$ALSA_DEV" -f S16_LE -c 1 -r "$RATE" -t wav "$@"
}

# Пишет в $1 до нажатия Enter. Возвращает 1, если arecord не стартовал.
record_until_enter() {
    local out="$1"
    arec "$out" 2>"$ERR_LOG" &
    local pid=$!
    sleep 0.3
    if ! kill -0 "$pid" 2>/dev/null; then
        warn "arecord не запустился:"
        cat "$ERR_LOG" >&2
        return 1
    fi
    echo -e "   \033[1;31m● ЗАПИСЬ\033[0m — говорите, затем Enter"
    read -r -s
    sleep 0.3  # чтобы не отрезать хвост последнего слова
    # TERM, а не INT: у фоновых процессов неинтерактивного bash SIGINT
    # игнорируется. arecord по TERM корректно дописывает WAV-заголовок.
    kill -TERM "$pid" 2>/dev/null || true
    wait "$pid" 2>/dev/null || true
}

# stats: duration rms_db peak_db
audio_stats() {
    local f="$1" dur rms pk
    dur="$(sox --i -D "$f" 2>/dev/null || echo 0)"
    rms="$(sox "$f" -n stats 2>&1 | awk '/^RMS lev dB/ {print $4}')"
    pk="$(sox "$f" -n stats 2>&1 | awk '/^Pk lev dB/ {print $4}')"
    echo "${dur:-0} ${rms:-nan} ${pk:-nan}"
}

check_levels() {
    local rms="$1" pk="$2"
    awk -v r="$rms" -v p="$pk" 'BEGIN {
        if (p != "nan" && p+0 > -0.5) print "   [!] Перегруз (клиппинг): говорите тише/дальше или уменьшите gain";
        if (r == "-inf" || (r != "nan" && r+0 < -55)) print "   [!] Очень тихо: проверьте, что выбран нужный микрофон и он не заглушён";
    }'
}

add_row() {
    # file ref kind note
    local f="$1" ref="$2" kind="$3" note="${4:-}" dur rms pk
    read -r dur rms pk < <(audio_stats "$DIR/$f")
    printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
        "$f" "$ref" "$kind" "$LOCATION" "$DEVICE_ID" "$DISTANCE" \
        "$dur" "$rms" "$pk" "$note" >> "$MANIFEST"
    echo "   длительность ${dur}s, RMS ${rms} dB, пик ${pk} dB"
    check_levels "$rms" "$pk"
}

# Записывает одну фразу с возможностью перезаписи/правки текста.
# $1 — номер, $2 — эталонный текст (пусто для свободной фразы), $3 — kind
record_phrase() {
    local idx="$1" ref="$2" kind="$3" f ans note
    f="$(printf '%s_%03d.wav' "$kind" "$idx")"
    while true; do
        echo
        if [ -n "$ref" ]; then
            echo -e "[$idx] Скажите: \033[1m$ref\033[0m"
        else
            echo "[$idx] Свободная фраза: скажите запрос своими словами"
        fi
        read -r -p "   Enter — начать запись, s — пропустить, q — закончить: " ans
        case "$ans" in
            s) return 0 ;;
            q) return 2 ;;
        esac
        record_until_enter "$DIR/$f" || return 1

        if [ -z "$ref" ]; then
            read -r -p "   Что было сказано (точно, как произнесли): " ref
        fi
        read -r -p "   Enter — ок, r — перезаписать, e — исправить текст: " ans
        case "$ans" in
            r) rm -f "$DIR/$f"; [ "$kind" = "free" ] && ref=""; continue ;;
            e) read -r -p "   Фактически сказано: " ref ;;
        esac
        read -r -p "   Заметка (шум рядом, объявление, музыка… можно пусто): " note
        add_row "$f" "$ref" "$kind" "$note"
        return 0
    done
}

# ----------------------------------------------------------------- flow
info "Сессия: $DIR"
info "Микрофон: $ALSA_DEV, ${RATE} Гц, mono"

# Пробная запись: ловим занятый/несуществующий микрофон сразу.
if ! arec -d 1 "$DIR/.probe.wav" 2>"$ERR_LOG"; then
    cat "$ERR_LOG" >&2
    die "не удалось записать с '$ALSA_DEV'. Посмотрите --list-devices; если микрофон занят — остановите голосовой поиск."
fi
rm -f "$DIR/.probe.wav"

if [ "$NOISE_SEC" -gt 0 ]; then
    echo
    info "Шаг 1. Фон локации: ${NOISE_SEC} с без обращения к дроиду."
    read -r -p "   Enter — начать: " _
    arec -d "$NOISE_SEC" "$DIR/noise_000.wav"
    read -r -p "   Что звучало (музыка, толпа, объявления…): " note
    add_row "noise_000.wav" "" "noise" "$note"
fi

if [ -n "$PHRASES_FILE" ]; then
    [ -f "$PHRASES_FILE" ] || die "нет файла $PHRASES_FILE"
    mapfile -t PHRASES < <(grep -v '^[[:space:]]*$' "$PHRASES_FILE")
else
    mapfile -t PHRASES <<< "$DEFAULT_PHRASES"
fi

echo
info "Шаг 2. Фразы по списку (${#PHRASES[@]} шт.). Говорите как обычный посетитель."
i=1
for p in "${PHRASES[@]}"; do
    rc=0
    record_phrase "$i" "$p" "phrase" || rc=$?
    [ "$rc" -eq 2 ] && break
    [ "$rc" -eq 1 ] && die "запись прервана"
    i=$((i + 1))
done

echo
info "Шаг 3. Свободные фразы (${FREE_COUNT} шт., q — закончить раньше)."
for j in $(seq 1 "$FREE_COUNT"); do
    rc=0
    record_phrase "$j" "" "free" || rc=$?
    [ "$rc" -eq 2 ] && break
    [ "$rc" -eq 1 ] && die "запись прервана"
done

rm -f "$ERR_LOG"
archive="${OUT_ROOT}/${SESSION}.tar.gz"
tar -czf "$archive" -C "$OUT_ROOT" "$SESSION"

echo
info "Готово. Записей: $(($(wc -l < "$MANIFEST") - 1))"
info "Архив для отправки: $archive"
