#!/usr/bin/env bash
# transcribe.sh — HOLLYLAND 2chピンマイクのリアルタイム文字起こし
#
# 使い方: ./transcribe.sh        (停止: Ctrl+C)
#
# 仕様:
#   - 30秒チャンクで録音 → 左ch=りつは / 右ch=暦 に分離
#   - whisper-cppで日本語文字起こし(並列バックグラウンド処理)
#   - transcript.md に追記(flockで排他制御)

set -u

# ================ 設定 ================
DEVICE_NAME="${DEVICE_NAME:-Wireless Microphone}"   # ffmpeg avfoundation で見つける名前
CHUNK_SEC="${CHUNK_SEC:-30}"
SAMPLE_RATE="${SAMPLE_RATE:-48000}"
MODEL="${MODEL:-medium}"                            # tiny/base/small/medium/large-v3
LANG="${LANG_CODE:-ja}"
LEFT_NAME="${LEFT_NAME:-りつは}"                     # 左ch話者(逆だったらSETUP.md参照)
RIGHT_NAME="${RIGHT_NAME:-暦}"                       # 右ch話者
THREADS="${THREADS:-4}"
# ======================================

cd "$(dirname "$0")"
BASE_DIR="$(pwd)"
CHUNKS_DIR="$BASE_DIR/chunks"
# 🔴 自己増殖バグ対策: L/R分離後の中間wav/txtは chunks/ ではなく専用dirへ出す。
# これにより watch_chunks の glob が自分の出力を再マッチして無限再帰(_L_L_L...)するのを構造的に防ぐ。
SPLITS_DIR="$BASE_DIR/.splits"
TRANSCRIPT="$BASE_DIR/transcript.md"
LOCK_FILE="$BASE_DIR/.transcript.lock"
LOG_FILE="$BASE_DIR/.session.log"
PROCESSED_DIR="$BASE_DIR/.processed"

mkdir -p "$CHUNKS_DIR" "$PROCESSED_DIR" "$SPLITS_DIR"
: > "$LOG_FILE"

# transcript.md ヘッダ存在保証
if [ ! -s "$TRANSCRIPT" ]; then
  cat > "$TRANSCRIPT" <<'HDR'
# ライブ会議トランスクリプト

> ピンマイク2chからリアルタイム文字起こしを追記する。
> 左ch=りつは、右ch=暦(逆だったらSETUP.md参照して入れ替え)。

---

HDR
fi

# ================ 依存チェック ================
need() { command -v "$1" >/dev/null 2>&1 || { echo "[ERR] $1 が見つからない。SETUP.md参照"; exit 1; }; }
need ffmpeg
# flockはmacOS標準にないのでmkdir方式の排他制御を使う(下のブロック参照)

# whisper-cli or whisper-cpp どちらでも動くように
if command -v whisper-cli >/dev/null 2>&1; then
  WHISPER_BIN="whisper-cli"
elif command -v whisper-cpp >/dev/null 2>&1; then
  WHISPER_BIN="whisper-cpp"
else
  echo "[ERR] whisper-cli / whisper-cpp が見つからない。'brew install whisper-cpp' を待つかSETUP.md参照"
  exit 1
fi

# モデルファイル探索
MODEL_PATH=""
for cand in \
  "$HOME/.cache/whisper/ggml-${MODEL}.bin" \
  "$(brew --prefix 2>/dev/null)/share/whisper-cpp/ggml-${MODEL}.bin" \
  "/opt/homebrew/share/whisper-cpp/ggml-${MODEL}.bin" \
  "/usr/local/share/whisper-cpp/ggml-${MODEL}.bin" \
  "$BASE_DIR/models/ggml-${MODEL}.bin"
do
  if [ -f "$cand" ]; then MODEL_PATH="$cand"; break; fi
done

if [ -z "$MODEL_PATH" ]; then
  echo "[ERR] ggml-${MODEL}.bin が見つからない。'whisper-cpp-download-ggml-model ${MODEL}' で取得"
  echo "      取得後の実体パス例: /opt/homebrew/share/whisper-cpp/ggml-${MODEL}.bin"
  exit 1
fi

echo "[INFO] WHISPER_BIN=$WHISPER_BIN"
echo "[INFO] MODEL_PATH=$MODEL_PATH"
echo "[INFO] DEVICE='$DEVICE_NAME'  CHUNK=${CHUNK_SEC}s  SR=${SAMPLE_RATE}"
echo "[INFO] LEFT=$LEFT_NAME  RIGHT=$RIGHT_NAME"
echo "[INFO] transcript -> $TRANSCRIPT"
echo "[INFO] Ctrl+C で停止"
echo

# ================ クリーンアップ ================
PIDS=()
cleanup() {
  echo
  echo "[INFO] 停止中..."
  if [ -n "${FFMPEG_PID:-}" ] && kill -0 "$FFMPEG_PID" 2>/dev/null; then
    kill -INT "$FFMPEG_PID" 2>/dev/null || true
    wait "$FFMPEG_PID" 2>/dev/null || true
  fi
  for p in "${PIDS[@]}"; do
    wait "$p" 2>/dev/null || true
  done
  echo "[INFO] 終了"
  exit 0
}
trap cleanup INT TERM

# ================ チャンク処理関数 ================
# 1チャンク受け取って L/R 分離 → whisper → transcript追記
process_chunk() {
  local wav="$1"
  local base
  base="$(basename "$wav" .wav)"
  # 中間出力は SPLITS_DIR へ(chunks/ に出すと watch_chunks に再マッチして無限増殖する)
  local left="$SPLITS_DIR/${base}_L.wav"
  local right="$SPLITS_DIR/${base}_R.wav"

  # 16kHz mono に落とす(whisperは16k想定で精度・速度↑)
  ffmpeg -nostdin -hide_banner -loglevel error -y \
    -i "$wav" \
    -filter_complex "[0:a]channelsplit=channel_layout=stereo[L][R]" \
    -map "[L]" -ac 1 -ar 16000 "$left" \
    -map "[R]" -ac 1 -ar 16000 "$right" 2>>"$LOG_FILE" || {
      echo "[WARN] split失敗: $wav" >>"$LOG_FILE"
      return
    }

  # 左右並列でwhisper(出力はtxtファイル経由)
  transcribe_one "$left"  "L" &
  local pid_l=$!
  transcribe_one "$right" "R" &
  local pid_r=$!
  wait "$pid_l" "$pid_r" 2>/dev/null || true

  local txt_l="$SPLITS_DIR/${base}_L.txt"
  local txt_r="$SPLITS_DIR/${base}_R.txt"

  local line_l line_r
  line_l="$(extract_text "$txt_l")"
  line_r="$(extract_text "$txt_r")"

  if [ -z "$line_l" ] && [ -z "$line_r" ]; then
    mv "$wav" "$PROCESSED_DIR/" 2>/dev/null || rm -f "$wav"
    rm -f "$left" "$right" "$txt_l" "$txt_r"
    return
  fi

  local ts
  ts="$(date '+%Y-%m-%d %H:%M:%S')"

  # mkdir方式の排他追記(macOS互換・flock不要)
  while ! mkdir "${LOCK_FILE}.d" 2>/dev/null; do sleep 0.05; done
  {
    echo "## $ts"
    [ -n "$line_l" ] && echo "- [$LEFT_NAME] $line_l"
    [ -n "$line_r" ] && echo "- [$RIGHT_NAME] $line_r"
    echo
  } >> "$TRANSCRIPT"
  rmdir "${LOCK_FILE}.d" 2>/dev/null || true

  mv "$wav" "$PROCESSED_DIR/" 2>/dev/null || rm -f "$wav"
  rm -f "$left" "$right" "$txt_l" "$txt_r"
}

# whisper-cli/cpp 1ファイル実行
transcribe_one() {
  local in_wav="$1"
  local _label="$2"  # ログ用
  local out_base="${in_wav%.wav}"
  "$WHISPER_BIN" \
    -m "$MODEL_PATH" \
    -f "$in_wav" \
    -l "$LANG" \
    -t "$THREADS" \
    -otxt \
    -of "$out_base" \
    --no-prints \
    >>"$LOG_FILE" 2>&1 || true
}

# whisper出力txtから本文抽出(ノイズ・空白除去)
extract_text() {
  local f="$1"
  [ -f "$f" ] || { echo ""; return; }
  # whisper特有のノイズ語を捨てる + 行頭末空白除去 + 全行連結
  local t
  t="$(cat "$f" 2>/dev/null \
    | sed -E 's/\[[A-Z_ ]+\]//g' \
    | sed -E 's/^[[:space:]]+|[[:space:]]+$//g' \
    | grep -v '^$' \
    | tr '\n' ' ' \
    | sed -E 's/[[:space:]]+/ /g' \
    | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
  # よくある空虚出力を弾く
  case "$t" in
    ""|"。"|"、"|"ご視聴ありがとうございました。"|"ありがとうございました。"|"おやすみなさい。") echo ""; return;;
  esac
  echo "$t"
}

# ================ chunkウォッチャ(別プロセスで並列処理) ================
watch_chunks() {
  local seen=""
  while true; do
    # 完成済みチャンク(=次のチャンクが既にできてる or プロセス終了)だけ拾う
    # 🔴 入力chunk(CHUNK_NNN.wav)だけ拾う。3桁数字直後に .wav。
    #    _L/_R 等のサフィックス付きは弾く(=自分の出力を再マッチさせない第2の砦)。
    local files
    files="$(ls -1 "$CHUNKS_DIR"/CHUNK_[0-9][0-9][0-9].wav 2>/dev/null | sort)"
    [ -z "$files" ] && { sleep 1; continue; }

    # 最新のwavはまだffmpegが書き込み中の可能性がある → 1個前まで処理
    local arr=()
    while IFS= read -r line; do arr+=("$line"); done <<< "$files"
    local n="${#arr[@]}"
    local upto
    if kill -0 "${FFMPEG_PID:-0}" 2>/dev/null; then
      upto=$((n - 1))   # 録音中:最新は除外
    else
      upto=$n           # 録音終わってる:全部処理
    fi

    local i=0
    while [ "$i" -lt "$upto" ]; do
      local f="${arr[$i]}"
      i=$((i + 1))
      [ -f "$f" ] || continue
      # 🔴 第3の砦: 万一サフィックス付き(_L/_R を含む)が混じっても処理しない
      case "$(basename "$f")" in CHUNK_[0-9][0-9][0-9].wav) ;; *) continue;; esac
      case "$seen" in *"|$f|"*) continue;; esac
      seen="${seen}|$f|"
      # 並列上限ガード: 走行中の process_chunk が MAX_PARALLEL 以上なら待つ(暴走防止)
      local MAX_PARALLEL="${MAX_PARALLEL:-4}"
      while [ "$(jobs -rp | wc -l)" -ge "$MAX_PARALLEL" ]; do sleep 0.2; done
      ( process_chunk "$f" ) &
      PIDS+=($!)
    done

    # 録音停止 & 全処理待ち
    if ! kill -0 "${FFMPEG_PID:-0}" 2>/dev/null; then
      # 残りファイルなくなるまで待つ
      sleep 2
      if [ -z "$(ls -1 "$CHUNKS_DIR"/CHUNK_[0-9][0-9][0-9].wav 2>/dev/null)" ]; then
        break
      fi
    fi
    sleep 1
  done
}

# ================ 録音開始(ffmpeg segment) ================
# avfoundation はデバイス名 or インデックス指定可。名前優先で投げる。
# 失敗したらSETUP.md参照のメッセージ
ffmpeg -nostdin -hide_banner -loglevel warning \
  -f avfoundation \
  -i ":${DEVICE_NAME}" \
  -f segment -segment_time "$CHUNK_SEC" -reset_timestamps 1 \
  -c:a pcm_s16le \
  "$CHUNKS_DIR/CHUNK_%03d.wav" \
  >>"$LOG_FILE" 2>&1 &
FFMPEG_PID=$!

sleep 2
if ! kill -0 "$FFMPEG_PID" 2>/dev/null; then
  echo "[ERR] ffmpeg起動失敗。SETUP.mdの『デバイス番号特定』を試して DEVICE_NAME 環境変数で指定:"
  echo "      DEVICE_NAME=':0' ./transcribe.sh   など"
  echo "--- ffmpeg log tail ---"
  tail -n 20 "$LOG_FILE"
  exit 1
fi

echo "[INFO] 録音開始 (ffmpeg pid=$FFMPEG_PID)"
watch_chunks
cleanup
