717 lines
25 KiB
Bash
Executable File
717 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# meetings — audio → transcript → summary + action items, all local
|
|
#
|
|
# Transcribes audio using whisper.cpp or parakeet.cpp (GGUF/GGML models),
|
|
# then uses Ollama to summarize and extract action items.
|
|
#
|
|
# Usage:
|
|
# ./meetings <audio_file> # full pipeline
|
|
# ./meetings setup # install deps + download model
|
|
# ./meetings doctor # check dependencies
|
|
# ./meetings config # show current config
|
|
#
|
|
set -euo pipefail
|
|
|
|
# ─── colours ────────────────────────────────────────────────────────
|
|
RED='\033[0;31m'; GRN='\033[0;32m'; YEL='\033[1;33m'
|
|
BLU='\033[0;34m'; CYN='\033[0;36m'; RST='\033[0m'
|
|
BOLD='\033[1m'
|
|
|
|
# ─── paths ──────────────────────────────────────────────────────────
|
|
MEETINGS_DIR="${MEETINGS_DIR:-$HOME/.meetings}"
|
|
CONFIG_FILE="$MEETINGS_DIR/config"
|
|
MODELS_DIR="$MEETINGS_DIR/models"
|
|
|
|
# ─── helpers ───────────────────────────────────────────────────────
|
|
die() { printf "${RED}error: %s${RST}\n" "$*" >&2; exit 1; }
|
|
info() { printf "${BLU}▸ %s${RST}\n" "$*" >&2; }
|
|
ok() { printf "${GRN}✓ %s${RST}\n" "$*" >&2; }
|
|
warn() { printf "${YEL}⚠ %s${RST}\n" "$*" >&2; }
|
|
step() { printf "\n${BOLD}${CYN}── %s ──${RST}\n" "$*" >&2; }
|
|
|
|
# Format seconds into human-readable time (e.g., "1m 23s" or "45s")
|
|
format_time() {
|
|
local secs="$1" minutes seconds
|
|
minutes=$((secs / 60))
|
|
seconds=$((secs % 60))
|
|
if [[ $minutes -gt 0 ]]; then
|
|
printf "%dm %ds" "$minutes" "$seconds"
|
|
else
|
|
printf "%ds" "$seconds"
|
|
fi
|
|
}
|
|
|
|
banner() {
|
|
cat <<'BAN'
|
|
|
|
┌─────────────────────────────────────────────┐
|
|
│ 🎤 M E E T I N G S │
|
|
│ audio → transcript → summary + actions │
|
|
│ whisper.cpp · parakeet.cpp · ollama │
|
|
└─────────────────────────────────────────────┘
|
|
|
|
BAN
|
|
}
|
|
|
|
# ─── load config: env vars > config file > defaults ────────────────
|
|
load_config() {
|
|
# defaults
|
|
STT_ENGINE=""
|
|
STT_MODEL=""
|
|
OLLAMA_MODEL="llama3.1:8b"
|
|
OLLAMA_HOST="http://localhost:11434"
|
|
THREADS="4"
|
|
LANGUAGE="en"
|
|
OUTPUT_DIR="."
|
|
# config file overrides defaults
|
|
[[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE"
|
|
# env vars override everything (|| true prevents set -e exit on empty vars)
|
|
[[ -n "${MEETINGS_STT:-}" ]] && STT_ENGINE="$MEETINGS_STT" || true
|
|
[[ -n "${MEETINGS_STT_MODEL:-}" ]] && STT_MODEL="$MEETINGS_STT_MODEL" || true
|
|
[[ -n "${MEETINGS_LLM:-}" ]] && OLLAMA_MODEL="$MEETINGS_LLM" || true
|
|
[[ -n "${MEETINGS_THREADS:-}" ]] && THREADS="$MEETINGS_THREADS" || true
|
|
[[ -n "${MEETINGS_LANG:-}" ]] && LANGUAGE="$MEETINGS_LANG" || true
|
|
[[ -n "${MEETINGS_OUTPUT:-}" ]] && OUTPUT_DIR="$MEETINGS_OUTPUT" || true
|
|
}
|
|
|
|
# ─── detect STT engine ──────────────────────────────────────────────
|
|
detect_stt() {
|
|
if command -v parakeet-cli &>/dev/null; then
|
|
echo "parakeet"
|
|
elif command -v whisper-cli &>/dev/null; then
|
|
echo "whisper"
|
|
else
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
get_stt_binary() {
|
|
local engine="${1:-$STT_ENGINE}"
|
|
case "$engine" in
|
|
whisper) command -v whisper-cli 2>/dev/null || echo "" ;;
|
|
parakeet) command -v parakeet-cli 2>/dev/null || echo "" ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
find_stt_model() {
|
|
local engine="${1:-whisper}"
|
|
if [[ -n "${STT_MODEL:-}" && -f "${STT_MODEL}" ]]; then
|
|
echo "$STT_MODEL"; return 0
|
|
fi
|
|
case "$engine" in
|
|
whisper)
|
|
for f in "$MODELS_DIR"/ggml-*.bin "$MODELS_DIR"/ggml-*.gguf; do
|
|
[[ -f "$f" ]] && echo "$f" && return 0
|
|
done
|
|
;;
|
|
parakeet)
|
|
for f in "$MODELS_DIR"/*parakeet*.gguf "$MODELS_DIR"/*tdt*.gguf; do
|
|
[[ -f "$f" ]] && echo "$f" && return 0
|
|
done
|
|
;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
# ─── doctor ─────────────────────────────────────────────────────────
|
|
cmd_doctor() {
|
|
banner >&2
|
|
local ok_count=0 total=0
|
|
|
|
total=$((total+1))
|
|
if command -v ffmpeg &>/dev/null; then
|
|
ok "ffmpeg: $(command -v ffmpeg)"; ok_count=$((ok_count+1))
|
|
else
|
|
warn "ffmpeg: not found"
|
|
fi
|
|
|
|
total=$((total+1))
|
|
if command -v ollama &>/dev/null; then
|
|
ok "ollama: $(command -v ollama)"; ok_count=$((ok_count+1))
|
|
if curl -sf "${OLLAMA_HOST:-http://localhost:11434}/api/tags" &>/dev/null; then
|
|
ok " server: running at ${OLLAMA_HOST:-http://localhost:11434}"
|
|
else
|
|
warn " server: not responding (run: ollama serve)"
|
|
fi
|
|
else
|
|
warn "ollama: not found"
|
|
fi
|
|
|
|
total=$((total+1))
|
|
local engine="${STT_ENGINE:-$(detect_stt)}"
|
|
local bin
|
|
bin="$(get_stt_binary "$engine")"
|
|
if [[ -n "$bin" ]]; then
|
|
ok "STT engine: $engine ($bin)"; ok_count=$((ok_count+1))
|
|
else
|
|
warn "STT engine: not found (run: ./meetings setup)"
|
|
fi
|
|
|
|
total=$((total+1))
|
|
local model
|
|
model="$(find_stt_model "$engine")" || true
|
|
if [[ -n "$model" ]]; then
|
|
ok "STT model: $model"; ok_count=$((ok_count+1))
|
|
else
|
|
warn "STT model: not found (run: ./meetings setup)"
|
|
fi
|
|
|
|
total=$((total+1))
|
|
local ollama_model="${OLLAMA_MODEL:-llama3.1:8b}"
|
|
local model_name
|
|
model_name="$(echo "$ollama_model" | cut -d: -f1)"
|
|
if ollama list 2>/dev/null | awk '{print $1}' | grep -qF "$model_name"; then
|
|
ok "LLM model: $ollama_model (pulled)"; ok_count=$((ok_count+1))
|
|
else
|
|
warn "LLM model: $ollama_model (not pulled — run: ollama pull $ollama_model)"
|
|
fi
|
|
|
|
echo "" >&2
|
|
if [[ $ok_count -eq $total ]]; then
|
|
ok "All good ($ok_count/$total)"
|
|
else
|
|
warn "Ready ($ok_count/$total). Run './meetings setup' to install missing pieces."
|
|
fi
|
|
}
|
|
|
|
# ─── setup ──────────────────────────────────────────────────────────
|
|
cmd_setup() {
|
|
load_config
|
|
banner >&2
|
|
mkdir -p "$MEETINGS_DIR" "$MODELS_DIR"
|
|
|
|
# ── ffmpeg ──
|
|
step "Checking ffmpeg"
|
|
if command -v ffmpeg &>/dev/null; then
|
|
ok "ffmpeg already installed"
|
|
else
|
|
info "Installing ffmpeg..."
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
brew install ffmpeg || die "Could not install ffmpeg"
|
|
elif command -v apt-get &>/dev/null; then
|
|
sudo apt-get update && sudo apt-get install -y ffmpeg
|
|
elif command -v dnf &>/dev/null; then
|
|
sudo dnf install -y ffmpeg
|
|
else
|
|
die "Please install ffmpeg manually: https://ffmpeg.org/download.html"
|
|
fi
|
|
fi
|
|
|
|
# ── ollama ──
|
|
step "Checking Ollama"
|
|
if command -v ollama &>/dev/null; then
|
|
ok "ollama already installed"
|
|
else
|
|
info "Installing Ollama..."
|
|
curl -fsSL https://ollama.com/install.sh | sh || die "Could not install Ollama"
|
|
fi
|
|
|
|
if ! curl -sf "${OLLAMA_HOST:-http://localhost:11434}/api/tags" &>/dev/null; then
|
|
info "Starting Ollama server..."
|
|
ollama serve &>/dev/null &
|
|
sleep 3
|
|
fi
|
|
|
|
# ── choose STT engine ──
|
|
step "Choosing STT engine"
|
|
echo "" >&2
|
|
echo " 1) whisper.cpp — battle-tested, many languages, brew install" >&2
|
|
echo " 2) parakeet.cpp — faster, great English, smaller footprint" >&2
|
|
echo "" >&2
|
|
local choice
|
|
if [[ -n "${STT_ENGINE:-}" ]]; then
|
|
choice="$STT_ENGINE"
|
|
info "Using pre-configured engine: $choice"
|
|
else
|
|
read -rp " Choose [1/2, default=1]: " choice
|
|
case "${choice:-1}" in
|
|
2|parakeet) choice="parakeet" ;;
|
|
*) choice="whisper" ;;
|
|
esac
|
|
fi
|
|
|
|
case "$choice" in
|
|
whisper) setup_whisper ;;
|
|
parakeet) setup_parakeet ;;
|
|
esac
|
|
|
|
STT_ENGINE="$choice"
|
|
grep -q "^STT_ENGINE=" "$CONFIG_FILE" 2>/dev/null \
|
|
&& sed -i '' "s/^STT_ENGINE=.*/STT_ENGINE=$choice/" "$CONFIG_FILE" 2>/dev/null \
|
|
|| echo "STT_ENGINE=$choice" >> "$CONFIG_FILE"
|
|
|
|
# ── pull LLM ──
|
|
step "Pulling LLM: ${OLLAMA_MODEL:-llama3.1:8b}"
|
|
local ollama_model="${OLLAMA_MODEL:-llama3.1:8b}"
|
|
if ollama list 2>/dev/null | awk '{print $1}' | grep -qF "$(echo "$ollama_model" | cut -d: -f1)"; then
|
|
ok "$ollama_model already pulled"
|
|
else
|
|
ollama pull "$ollama_model" || warn "Could not pull $ollama_model. Run: ollama pull $ollama_model"
|
|
fi
|
|
|
|
echo "" >&2
|
|
ok "Setup complete! Run './meetings doctor' to verify, then './meetings <audio_file>' to process."
|
|
}
|
|
|
|
setup_whisper() {
|
|
step "Installing whisper.cpp"
|
|
if command -v whisper-cli &>/dev/null; then
|
|
ok "whisper-cli already available at $(command -v whisper-cli)"
|
|
else
|
|
info "Installing via package manager..."
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
brew install whisper-cpp || die "Could not install whisper-cpp via brew"
|
|
else
|
|
die "Please install whisper-cli manually: https://github.com/ggml-org/whisper.cpp
|
|
On macOS: brew install whisper-cpp
|
|
Or build from source: git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp && cmake -B build && cmake --build build -j"
|
|
fi
|
|
fi
|
|
|
|
step "Downloading Whisper model"
|
|
local model_choice
|
|
echo "" >&2
|
|
echo " Available models (from HuggingFace ggerganov/whisper.cpp):" >&2
|
|
echo " tiny.en — 75 MB (fastest, English only)" >&2
|
|
echo " base.en — 142 MB (good for quick tests)" >&2
|
|
echo " small.en — 466 MB (recommended for English) ★" >&2
|
|
echo " medium.en — 1.5 GB (high accuracy, English only)" >&2
|
|
echo " large-v3-turbo — 809 MB (best multilingual, fast)" >&2
|
|
echo " large-v3 — 2.9 GB (best accuracy, any language)" >&2
|
|
echo "" >&2
|
|
if [[ -n "${STT_MODEL:-}" && -f "${STT_MODEL}" ]]; then
|
|
info "Using existing model: $STT_MODEL"
|
|
else
|
|
read -rp " Choose model [default=small.en]: " model_choice
|
|
model_choice="${model_choice:-small.en}"
|
|
local model_file="$MODELS_DIR/ggml-${model_choice}.bin"
|
|
if [[ -f "$model_file" ]]; then
|
|
ok "Model already downloaded: $model_file"
|
|
else
|
|
info "Downloading ggml-${model_choice}.bin (this may take a moment)..."
|
|
curl -L --progress-bar \
|
|
-o "$model_file" \
|
|
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${model_choice}.bin" \
|
|
|| die "Failed to download model"
|
|
ok "Downloaded: $model_file"
|
|
fi
|
|
STT_MODEL="$model_file"
|
|
echo "STT_MODEL=$model_file" >> "$CONFIG_FILE"
|
|
fi
|
|
}
|
|
|
|
setup_parakeet() {
|
|
step "Installing parakeet.cpp"
|
|
if command -v parakeet-cli &>/dev/null; then
|
|
ok "parakeet-cli already available at $(command -v parakeet-cli)"
|
|
else
|
|
info "Checking for pre-built binary or Docker..."
|
|
if command -v docker &>/dev/null; then
|
|
ok "Docker available — parakeet.cpp will run via container"
|
|
else
|
|
die "parakeet-cli not found and Docker not installed. Please either:
|
|
- Build parakeet.cpp: https://github.com/mudler/parakeet.cpp
|
|
- Or install Docker for the container approach"
|
|
fi
|
|
fi
|
|
|
|
step "Downloading Parakeet model"
|
|
local model_choice
|
|
echo "" >&2
|
|
echo " Available models (from HuggingFace mudler/parakeet-cpp-gguf):" >&2
|
|
echo " tdt_ctc-110m-q8_0 — 178 MB (fast, good English, smallest) ★" >&2
|
|
echo " tdt_ctc-110m-f16 — 268 MB (fast, lossless English)" >&2
|
|
echo " tdt-0.6b-v3-f16 — 1.4 GB (multilingual, recommended)" >&2
|
|
echo "" >&2
|
|
if [[ -n "${STT_MODEL:-}" && -f "${STT_MODEL}" ]]; then
|
|
info "Using existing model: $STT_MODEL"
|
|
else
|
|
read -rp " Choose model [default=tdt_ctc-110m-q8_0]: " model_choice
|
|
model_choice="${model_choice:-tdt_ctc-110m-q8_0}"
|
|
local model_file="$MODELS_DIR/${model_choice}.gguf"
|
|
if [[ -f "$model_file" ]]; then
|
|
ok "Model already downloaded: $model_file"
|
|
else
|
|
info "Downloading ${model_choice}.gguf..."
|
|
curl -L --progress-bar \
|
|
-o "$model_file" \
|
|
"https://huggingface.co/mudler/parakeet-cpp-gguf/resolve/main/${model_choice}.gguf" \
|
|
|| die "Failed to download model"
|
|
ok "Downloaded: $model_file"
|
|
fi
|
|
STT_MODEL="$model_file"
|
|
echo "STT_MODEL=$model_file" >> "$CONFIG_FILE"
|
|
fi
|
|
}
|
|
|
|
# ─── config display ─────────────────────────────────────────────────
|
|
cmd_config() {
|
|
load_config
|
|
banner >&2
|
|
echo "" >&2
|
|
if [[ -f "$CONFIG_FILE" ]]; then
|
|
echo " Saved config ($CONFIG_FILE):" >&2
|
|
echo "" >&2
|
|
while IFS='=' read -r key val; do
|
|
printf " %-16s %s\n" "$key" "$val" >&2
|
|
done < "$CONFIG_FILE"
|
|
else
|
|
echo " (no saved config — run: ./meetings setup)" >&2
|
|
fi
|
|
echo "" >&2
|
|
echo " Effective settings:" >&2
|
|
local stt_display="${STT_ENGINE:-$(detect_stt)}"
|
|
stt_display="${stt_display:-(not set)}"
|
|
echo " STT engine: $stt_display" >&2
|
|
echo " STT model: ${STT_MODEL:-(auto-detect)}" >&2
|
|
echo " LLM model: $OLLAMA_MODEL" >&2
|
|
echo " Ollama host: $OLLAMA_HOST" >&2
|
|
echo " Threads: $THREADS" >&2
|
|
echo " Language: $LANGUAGE" >&2
|
|
echo " Output dir: $OUTPUT_DIR" >&2
|
|
echo "" >&2
|
|
}
|
|
|
|
# ─── convert audio ──────────────────────────────────────────────────
|
|
convert_audio() {
|
|
local input="$1" output="$2"
|
|
info "Converting audio to 16kHz mono WAV..."
|
|
ffmpeg -y -i "$input" -ar 16000 -ac 1 -c:a pcm_s16le "$output" \
|
|
-loglevel warning 2>/dev/null \
|
|
|| die "ffmpeg conversion failed for $input"
|
|
ok "Audio converted: $(du -h "$output" | cut -f1)"
|
|
}
|
|
|
|
# ─── transcribe with whisper ───────────────────────────────────────
|
|
transcribe_whisper() {
|
|
local wav_file="$1" model="$2" binary="$3"
|
|
info "Transcribing with whisper.cpp..."
|
|
local lang_flag=""
|
|
[[ "$LANGUAGE" != "auto" ]] && lang_flag="-l $LANGUAGE"
|
|
|
|
local transcript
|
|
transcript=$("$binary" \
|
|
-m "$model" \
|
|
-f "$wav_file" \
|
|
-t "$THREADS" \
|
|
$lang_flag \
|
|
-nt \
|
|
--no-prints \
|
|
2>/dev/null) || die "Whisper transcription failed"
|
|
|
|
echo "$transcript"
|
|
}
|
|
|
|
# ─── transcribe with parakeet ──────────────────────────────────────
|
|
transcribe_parakeet() {
|
|
local wav_file="$1" model="$2" binary="$3"
|
|
info "Transcribing with parakeet.cpp..."
|
|
|
|
local decoder_flag=""
|
|
if echo "$model" | grep -qi "ctc"; then
|
|
decoder_flag="--decoder ctc"
|
|
elif echo "$model" | grep -qi "tdt"; then
|
|
decoder_flag="--decoder tdt"
|
|
fi
|
|
|
|
if [[ -z "$binary" ]] && command -v docker &>/dev/null; then
|
|
info "Using Docker for parakeet.cpp..."
|
|
docker run --rm \
|
|
-v "$model:/model.gguf:ro" \
|
|
-v "$wav_file:/audio.wav:ro" \
|
|
ghcr.io/mudler/parakeet.cpp-cli:latest \
|
|
transcribe --model /model.gguf --input /audio.wav $decoder_flag 2>/dev/null \
|
|
|| die "Docker parakeet transcription failed"
|
|
else
|
|
"$binary" transcribe --model "$model" --input "$wav_file" $decoder_flag 2>/dev/null \
|
|
|| die "Parakeet transcription failed"
|
|
fi
|
|
}
|
|
|
|
# ─── call Ollama ───────────────────────────────────────────────────
|
|
ollama_generate() {
|
|
local prompt="$1" system="${2:-}"
|
|
local json_payload
|
|
|
|
if [[ -n "$system" ]]; then
|
|
json_payload=$(jq -n \
|
|
--arg model "$OLLAMA_MODEL" \
|
|
--arg system "$system" \
|
|
--arg prompt "$prompt" \
|
|
'{model: $model, system: $system, prompt: $prompt, stream: false}')
|
|
else
|
|
json_payload=$(jq -n \
|
|
--arg model "$OLLAMA_MODEL" \
|
|
--arg prompt "$prompt" \
|
|
'{model: $model, prompt: $prompt, stream: false}')
|
|
fi
|
|
|
|
curl -sf "$OLLAMA_HOST/api/generate" \
|
|
-d "$json_payload" 2>/dev/null \
|
|
| jq -r '.response // empty' \
|
|
|| die "Ollama request failed. Is the server running? (ollama serve)"
|
|
}
|
|
|
|
# ─── main pipeline ──────────────────────────────────────────────────
|
|
cmd_process() {
|
|
local input_file="$1"
|
|
shift || true
|
|
|
|
# Load config BEFORE flag parsing, so flags can override
|
|
load_config
|
|
|
|
# Start timer
|
|
local start_time
|
|
start_time=$(date +%s)
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--stt) STT_ENGINE="$2"; shift 2 ;;
|
|
--model) STT_MODEL="$2"; shift 2 ;;
|
|
--llm) OLLAMA_MODEL="$2"; shift 2 ;;
|
|
--lang) LANGUAGE="$2"; shift 2 ;;
|
|
--threads) THREADS="$2"; shift 2 ;;
|
|
--output) OUTPUT_DIR="$2"; shift 2 ;;
|
|
*) die "Unknown option: $1. Run: ./meetings help" ;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$input_file" ]] && die "Usage: ./meetings <audio_file>"
|
|
[[ ! -f "$input_file" ]] && die "File not found: $input_file"
|
|
|
|
STT_ENGINE="${STT_ENGINE:-$(detect_stt)}"
|
|
[[ -z "$STT_ENGINE" ]] && die "No STT engine found. Run: ./meetings setup"
|
|
|
|
local binary
|
|
binary="$(get_stt_binary "$STT_ENGINE")"
|
|
[[ -z "$binary" ]] && die "Could not find $STT_ENGINE binary. Run: ./meetings setup"
|
|
|
|
local model
|
|
if [[ -n "${STT_MODEL:-}" ]] && [[ -f "$STT_MODEL" ]]; then
|
|
model="$STT_MODEL"
|
|
else
|
|
model="$(find_stt_model "$STT_ENGINE")" \
|
|
|| die "No STT model found. Run: ./meetings setup"
|
|
fi
|
|
[[ ! -f "$model" ]] && die "Model file not found: $model"
|
|
|
|
command -v ffmpeg &>/dev/null || die "ffmpeg not found. Install it first."
|
|
curl -sf "$OLLAMA_HOST/api/tags" &>/dev/null \
|
|
|| die "Ollama server not responding at $OLLAMA_HOST. Run: ollama serve"
|
|
|
|
# ── create output directory ──
|
|
local basename
|
|
basename="$(date +%Y-%m-%d_%H%M)_$(basename "${input_file%.*}" | tr ' ' '_')"
|
|
local outdir="$OUTPUT_DIR/$basename"
|
|
mkdir -p "$outdir"
|
|
|
|
banner >&2
|
|
echo " Input: $input_file" >&2
|
|
echo " STT engine: $STT_ENGINE" >&2
|
|
echo " STT model: $(basename "$model")" >&2
|
|
echo " LLM model: $OLLAMA_MODEL" >&2
|
|
echo " Language: $LANGUAGE" >&2
|
|
echo " Output: $outdir/" >&2
|
|
echo "" >&2
|
|
|
|
# ── step 1: convert ──
|
|
step "Step 1/4 — Converting audio"
|
|
local step_start step_end step_elapsed
|
|
step_start=$(date +%s)
|
|
local wav_file="$outdir/audio_16k.wav"
|
|
convert_audio "$input_file" "$wav_file"
|
|
step_end=$(date +%s)
|
|
step_elapsed=$((step_end - step_start))
|
|
local time1
|
|
time1=$(format_time "$step_elapsed")
|
|
ok "Completed in ${time1}"
|
|
|
|
# ── step 2: transcribe ──
|
|
step "Step 2/4 — Transcribing with $STT_ENGINE"
|
|
step_start=$(date +%s)
|
|
local transcript
|
|
case "$STT_ENGINE" in
|
|
whisper) transcript="$(transcribe_whisper "$wav_file" "$model" "$binary")" ;;
|
|
parakeet) transcript="$(transcribe_parakeet "$wav_file" "$model" "$binary")" ;;
|
|
*) die "Unknown engine: $STT_ENGINE" ;;
|
|
esac
|
|
|
|
if [[ -z "$transcript" || -z "$(echo "$transcript" | tr -d '[:space:]')" ]]; then
|
|
die "Transcription produced empty output"
|
|
fi
|
|
|
|
echo "$transcript" > "$outdir/transcript.txt"
|
|
local word_count
|
|
word_count=$(echo "$transcript" | wc -w | tr -d ' ')
|
|
ok "Transcript: $word_count words"
|
|
step_end=$(date +%s)
|
|
step_elapsed=$((step_end - step_start))
|
|
local time2
|
|
time2=$(format_time "$step_elapsed")
|
|
ok "Completed in ${time2}"
|
|
|
|
# ── step 3: summarize ──
|
|
step "Step 3/4 — Summarizing ($OLLAMA_MODEL)"
|
|
step_start=$(date +%s)
|
|
local summarize_prompt
|
|
summarize_prompt="Please provide a clear, concise summary of the following meeting transcript.
|
|
Structure the summary with:
|
|
- **Topic**: What the meeting was about
|
|
- **Key Points**: Main topics discussed (3-5 bullet points)
|
|
- **Decisions**: Any decisions that were made
|
|
- **Open Questions**: Things left unresolved
|
|
|
|
TRANSCRIPT:
|
|
$transcript"
|
|
|
|
local summary
|
|
summary="$(ollama_generate "$summarize_prompt" "You are an expert meeting summarizer. Be concise but thorough. Always use markdown formatting.")"
|
|
echo "$summary" > "$outdir/summary.md"
|
|
ok "Summary saved"
|
|
step_end=$(date +%s)
|
|
step_elapsed=$((step_end - step_start))
|
|
local time3
|
|
time3=$(format_time "$step_elapsed")
|
|
ok "Completed in ${time3}"
|
|
|
|
# ── step 4: action items ──
|
|
step "Step 4/4 — Extracting action items ($OLLAMA_MODEL)"
|
|
step_start=$(date +%s)
|
|
local actions_prompt
|
|
actions_prompt="Extract all action items, tasks, and commitments from the following meeting transcript.
|
|
For each action item, provide:
|
|
- **Who**: The person responsible (or 'Unassigned' if unclear)
|
|
- **What**: The specific task or action
|
|
- **When**: Any deadline mentioned (or 'No deadline specified')
|
|
- **Priority**: High / Medium / Low (based on urgency and context)
|
|
|
|
Be thorough — capture every commitment, follow-up, and task mentioned or implied.
|
|
Format as a numbered list.
|
|
|
|
TRANSCRIPT:
|
|
$transcript"
|
|
|
|
local actions
|
|
actions="$(ollama_generate "$actions_prompt" "You are an expert at extracting action items from meeting notes. Be thorough and specific. Always use markdown formatting.")"
|
|
echo "$actions" > "$outdir/action_items.md"
|
|
ok "Action items saved"
|
|
step_end=$(date +%s)
|
|
step_elapsed=$((step_end - step_start))
|
|
local time4
|
|
time4=$(format_time "$step_elapsed")
|
|
ok "Completed in ${time4}"
|
|
|
|
# ── combine report ──
|
|
cat > "$outdir/report.md" <<REPORT
|
|
# Meeting Report
|
|
|
|
**Date:** $(date '+%B %d, %Y at %H:%M')
|
|
**Source:** $(basename "$input_file")
|
|
**STT:** $STT_ENGINE | **LLM:** $OLLAMA_MODEL
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
$summary
|
|
|
|
---
|
|
|
|
## Action Items
|
|
|
|
$actions
|
|
|
|
---
|
|
|
|
## Full Transcript
|
|
|
|
$transcript
|
|
REPORT
|
|
|
|
echo "" >&2
|
|
ok "All done! Files saved to: $outdir/" >&2
|
|
echo "" >&2
|
|
echo " 📄 Report: $outdir/report.md" >&2
|
|
echo " 📝 Transcript: $outdir/transcript.txt" >&2
|
|
echo " 📋 Summary: $outdir/summary.md" >&2
|
|
echo " ✅ Action Items: $outdir/action_items.md" >&2
|
|
echo "" >&2
|
|
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
|
echo "" >&2
|
|
head -50 "$outdir/report.md" | tail -35 >&2
|
|
echo "" >&2
|
|
echo "(showing preview — full report at $outdir/report.md)" >&2
|
|
|
|
# Display elapsed time breakdown
|
|
local end_time elapsed minutes seconds
|
|
end_time=$(date +%s)
|
|
elapsed=$((end_time - start_time))
|
|
minutes=$((elapsed / 60))
|
|
seconds=$((elapsed % 60))
|
|
echo "" >&2
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
|
echo "⏱ Processing Time Breakdown:" >&2
|
|
echo " Step 1 (Convert audio): ${time1:-N/A}" >&2
|
|
echo " Step 2 (Transcribe): ${time2:-N/A}" >&2
|
|
echo " Step 3 (Summarize): ${time3:-N/A}" >&2
|
|
echo " Step 4 (Action items): ${time4:-N/A}" >&2
|
|
echo " ─────────────────────────────────────" >&2
|
|
printf " TOTAL: %dm %ds\n" "$minutes" "$seconds" >&2
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
|
}
|
|
|
|
# ─── entry point ────────────────────────────────────────────────────
|
|
case "${1:-help}" in
|
|
setup) shift; cmd_setup ;;
|
|
doctor) load_config; cmd_doctor ;;
|
|
config) cmd_config ;;
|
|
help|--help|-h)
|
|
banner >&2
|
|
cat <<'HELP'
|
|
Usage:
|
|
./meetings <audio_file> Run the full pipeline
|
|
./meetings setup Install deps + download model
|
|
./meetings doctor Check all dependencies
|
|
./meetings config Show current configuration
|
|
|
|
Options (override config):
|
|
--stt <whisper|parakeet> STT engine to use
|
|
--model <path> Path to GGUF/GGML model file
|
|
--llm <ollama_model> Ollama model for summarization
|
|
--lang <code> Language code (default: en, auto for whisper)
|
|
--threads <N> Number of threads for STT
|
|
--output <dir> Output directory (default: .)
|
|
|
|
Environment variables:
|
|
MEETINGS_DIR Config & models directory (default: ~/.meetings)
|
|
MEETINGS_STT STT engine (whisper/parakeet)
|
|
MEETINGS_STT_MODEL Path to model file
|
|
MEETINGS_LLM Ollama model name (default: llama3.1:8b)
|
|
MEETINGS_THREADS Thread count (default: 4)
|
|
MEETINGS_LANG Language code (default: en)
|
|
MEETINGS_OUTPUT Output directory (default: .)
|
|
|
|
Examples:
|
|
# First time — install everything
|
|
./meetings setup
|
|
|
|
# Process a meeting recording
|
|
./meetings recording.mp3
|
|
|
|
# Use a different LLM model
|
|
./meetings meeting.wav --llm llama3.1:8b
|
|
|
|
# Use parakeet with multilingual model
|
|
./meetings call.wav --stt parakeet --lang auto
|
|
|
|
# Specify output directory
|
|
./meetings interview.m4a --output ./reports
|
|
HELP
|
|
;;
|
|
*)
|
|
cmd_process "$@"
|
|
;;
|
|
esac |