Skip to main content

summarize-meeting.sh

#!/usr/bin/env bash
set -euo pipefail

# ------------------------------------------------------------
# summarize-meeting.sh
# Post-session summarization on Mac using ollama
# Usage: summarize-meeting.sh [--de|--en] [--game <slug>] [<transcript.txt>]
#        Defaults to English, most recent transcript if none given
# ------------------------------------------------------------

OLLAMA_MODEL="${OLLAMA_MODEL:-qwen2.5:32b}gemma3:27b}"
BASE="$HOME/Syncthing/TranscriptOMatic/recordings"
META_DIR="$(cd "$(dirname "$0")/../meta" 2>/dev/null && pwd || echo "$HOME/Syncthing/TranscriptOMatic/meta")"
LANG_MODE="en"
TRANSCRIPT=""
GAME_SLUG=""

# ------------------------------------------------------------
# Argument parsing
# ------------------------------------------------------------

while [[ $# -gt 0 ]]; do
  case "$1" in
    --de)     LANG_MODE="de"; shift ;;
    --en)     LANG_MODE="en"; shift ;;
    --game)   GAME_SLUG="$2"; shift 2 ;;
    -*)
      echo "Usage: summarize-meeting [--de|--en] [--game <slug>] [<transcript.txt>]" >&2
      exit 2
      ;;
    *)
      TRANSCRIPT="$1"; shift ;;
  esac
done

# ------------------------------------------------------------
# Find transcript
# ------------------------------------------------------------

if [[ -z "$TRANSCRIPT" ]]; then
  # Prefer normalized transcript; fall back to plain transcript
  TRANSCRIPT="$(ls -t "$BASE"/**/*_normalized.txt 2>/dev/null | head -n1 || true)"
  if [[ -z "$TRANSCRIPT" ]]; then
    TRANSCRIPT="$(ls -t "$BASE"/**/*_transcript.txt 2>/dev/null | head -n1 || true)"
  fi
  if [[ -z "$TRANSCRIPT" ]]; then
    echo "โŒ No transcript found in $BASE" >&2
    echo "   Usage: summarize-meeting [--de|--en] [--game <slug>] [<transcript.txt>]" >&2
    exit 1
  fi
fi

if [[ ! -f "$TRANSCRIPT" ]]; then
  echo "โŒ File not found: $TRANSCRIPT" >&2
  exit 1
fi

# If a plain transcript was given explicitly, check whether a normalized version exists
if [[ "$TRANSCRIPT" == *_transcript.txt ]]; then
  NORMALIZED="${TRANSCRIPT/_transcript.txt/_transcript_normalized.txt}"
  if [[ -f "$NORMALIZED" ]]; then
    echo "โ„น๏ธ  Using normalized transcript: $NORMALIZED"
    TRANSCRIPT="$NORMALIZED"
  fi
fi

# ------------------------------------------------------------
# Auto-detect game slug from transcript filename if not given
# ------------------------------------------------------------

if [[ -z "$GAME_SLUG" ]]; then
  TRANSCRIPT_BASENAME="$(basename "$TRANSCRIPT")"
  # Try to find a matching meta file by checking if any slug appears in the filename
  for META_FILE in "$META_DIR"/*.yaml; do
    [[ -f "$META_FILE" ]] || continue
    CANDIDATE_SLUG="$(basename "$META_FILE" .yaml)"
    if [[ "$TRANSCRIPT_BASENAME" == *"$CANDIDATE_SLUG"* ]]; then
      GAME_SLUG="$CANDIDATE_SLUG"
      break
    fi
  done
fi

# ------------------------------------------------------------
# Load meta file if available
# ------------------------------------------------------------

META_CONTEXT=""
META_FILE=""

if [[ -n "$GAME_SLUG" ]]; then
  META_FILE="$META_DIR/${GAME_SLUG}.yaml"
  if [[ -f "$META_FILE" ]]; then
    META_CONTEXT="$(cat "$META_FILE")"
    echo "๐Ÿ“‹ Meta:       $META_FILE"
  else
    echo "โš ๏ธ  No meta file found for slug '$GAME_SLUG' in $META_DIR" >&2
  fi
fi

# ------------------------------------------------------------
# Output paths
# ------------------------------------------------------------

SESSION="$(dirname "$TRANSCRIPT")"
TRANSCRIPT_BASE="$(basename "$TRANSCRIPT" .txt)"
SUMMARY="$SESSION/${TRANSCRIPT_BASE}_summary.md"

# ------------------------------------------------------------
# Skip if summary already exists (use FORCE=1 to override)
# ------------------------------------------------------------

if [[ -f "$SUMMARY" && "${FORCE:-}" != "1" ]]; then
  echo "โญ๏ธ  Summary already exists, skipping: $SUMMARY"
  echo "   Use FORCE=1 summarize-meeting to overwrite."
  exit 0
fi

# ------------------------------------------------------------
# Language-specific prompt
# ------------------------------------------------------------

case "$LANG_MODE" in
  en) PROMPT_LANG="Write the summary in English." ;;
  de) PROMPT_LANG="Schreibe die Zusammenfassung auf Deutsch." ;;
esac

echo "๐Ÿ“„ Transcript: $TRANSCRIPT"
echo "๐Ÿค– Model:      $OLLAMA_MODEL"
echo "๐Ÿ—ฃ๏ธ  Language:   $LANG_MODE"
echo "๐Ÿ“ Summary:    $SUMMARY"
echo "----"

# ------------------------------------------------------------
# Build prompt
# ------------------------------------------------------------

if [[ -n "$META_CONTEXT" ]]; then
  CONTEXT_BLOCK="YouREFERENCE haveDOCUMENT been(metadata providedonly withโ€” not part of the session transcript):
This YAML document provides background context for correctly interpreting the transcript below.
It is NOT a context document for this session (inlog YAMLor format).discussion. Do NOT summarize or reference its contents as in-game or out-of-game events.
Use itย exclusively to:
- Correctly identifyspell and spellidentify character names, player names, locations and in-game terms
- Understand roles androles, group memberships and character relationships
- InterpretThe Irish'lang' field (ga)e.g. andga, French (fr) wordsindicates andlanguage phrasesorigin correctlyof rathera thanname treatingโ€” themit asis transcriptionmetadata, errorsnot a topic of discussion
- Use the 'short' name for characters in continuous prose; use full names only when introducing a character
- The 'aliases' field lists transcription variants of a name โ€” use only the primary key or short name in the summary
unless- contextIf requiresa character appears in the fulltranscript under an alias, identify them by their short name

ContextReference document:
---
${META_CONTEXT}
---
END OF REFERENCE DOCUMENT

The transcript follows below. Summarize only what is in the transcript.
"
else
  CONTEXT_BLOCK=""
fi

# ------------------------------------------------------------
# Run summarization
# ------------------------------------------------------------

ollama --nowordwrap run "$OLLAMA_MODEL" <<EOF > "$SUMMARY"
You are an expert note-taker for tabletop roleplaying game sessions.
The transcript is a recording of a TTRPG session and contains both in-character roleplay and out-of-character table talk.

${CONTEXT_BLOCK}
Rules:
- Clearly distinguish between in-character events and out-of-character discussion
- Quote spoken statements in their original language
- ${PROMPT_LANG}
- Use character short names (as provided in context) rather than full names where natural
- ONLY summarize events, decisions and facts that are explicitly stated in the transcript
- If you are not certain something happened, omit it entirely โ€” do NOT infer, imply or extrapolate
- Pay close attention to who does what: do not invert agency (e.g. who challenges whom, who saves whom)
- Do not conflate events from different sessions; only summarize what happens in this transcript
- Subtext and player motivation matter: note what characters say and do, not what the model thinks they mean

Deliver:
1) Session overview (3-5 sentences summarising the main in-game events)
2) Key in-game decisions and developments
3) Important character moments (emotional beats, revelations, relationship shifts)
4) Highlights (memorable quotes, unexpected twists, standout scenes)
5) Notable out-of-character moments (rules discussions, retcons, player notes)
6) Cliffhangers and open threads going into the next session
7) Characters introduced or significantly developed this session

Transcript:
$(cat "$TRANSCRIPT")
EOF

echo "โœ… Summary written to $SUMMARY"