Metadata-Version: 2.5
Name: meeting-minute
Version: 0.2.0
Summary: Transcribe meetings locally with faster-whisper/whisper.cpp, then extract structured info via Claude/Cursor skills.
Project-URL: Homepage, https://github.com/milky-way-66/metting-minutes
Project-URL: Repository, https://github.com/milky-way-66/metting-minutes
Project-URL: Issues, https://github.com/milky-way-66/metting-minutes/issues
Author-email: longnv <hiendv@kaopiz.com>
License-Expression: MIT
License-File: LICENSE
Keywords: claude,cli,cursor,meeting-minutes,transcription,vtt,whisper
Requires-Python: >=3.10
Requires-Dist: faster-whisper>=1.0.0
Requires-Dist: rich>=13.7.0
Requires-Dist: typer>=0.12.0
Provides-Extra: whispercpp
Requires-Dist: pywhispercpp>=1.2.0; extra == 'whispercpp'
Description-Content-Type: text/markdown

# meeting-minute

Turn a meeting recording into structured minutes, without the audio leaving your machine.

1. **`mm transcribe`** converts video/audio to a timestamped `.vtt` transcript locally,
   using [faster-whisper](https://github.com/SYSTRAN/faster-whisper) (or `whisper.cpp`
   on Mac, opt-in). No API keys, no uploads.
2. **`mm skill init`** writes extraction skills into your project so Claude Code or
   Cursor can turn that `.vtt` into a summary, action items, a decision log, a Q&A
   log, or whatever you define — every claim cited back to a timestamp.

Defaults are tuned for **Japanese** accuracy and verified by A/B runs on real meeting
audio; `en` and `vi` are supported too. See [Accuracy notes](#accuracy-notes-japanese)
for what was measured, and [DESIGN.md](DESIGN.md) for the architecture.

## Install

```bash
uv tool install meeting-minute      # or: pipx install meeting-minute
```

Requires Python 3.10+ and **`ffmpeg` on PATH** (`brew install ffmpeg`,
`winget install ffmpeg`, or your distro's package manager).

The first `mm transcribe` downloads the Whisper model — `large-v3` is **~3 GB** and is
cached in `~/.cache/huggingface`, so budget for that once. Pass `--model small` for a
much smaller download if you are just trying it out.

## Quickstart

```bash
mm transcribe meeting.mp4            # -> meeting.vtt
mm skill init                        # -> .claude/skills/ and .cursor/rules/
```

Then, in Claude Code or Cursor: *"extract action items from meeting.vtt"*.

You get files like this, with every row traceable to a moment in the recording:

```markdown
| Task | Owner | Due | Source |
| --- | --- | --- | --- |
| Send the revised API spec to the client | Mai | by Friday | [00:04:12] |
| Book the load-testing window | unassigned | none | [00:21:40] |
```

## Transcribe

```bash
mm transcribe recording.mp4
mm transcribe call.mp3 -l en -o transcript.vtt
mm transcribe long-call.mp4 --backend whisper-cpp   # Mac Metal, needs the whispercpp extra
```

**Names and domain terms are the single biggest accuracy win.** Whisper mishears
proper nouns far more than ordinary speech, and telling it what to expect fixes them:

```bash
mm transcribe 会議.mp4 --glossary 'カオピーズ,グエン・ホン・クアン,要件定義,統括'
```

Since a project's vocabulary is the same for every recording in it, keep it in a file
instead and it is picked up automatically:

```bash
mm glossary                 # create .meeting-minute/glossary.txt
mm glossary --show          # print the terms that would be used
mm transcribe 会議.mp4       # uses the file — no flag needed
```

One term per line, `#` for comments. `--glossary-file` points somewhere else;
`--glossary` adds one-off terms on top of the file.

Long files are split into chunks (default 2 minutes) and decoded in parallel with
progress shown. If interrupted, re-running the same command resumes from the last
completed chunk; `--fresh` forces a full re-run.

### Useful flags

| Flag | Effect |
| --- | --- |
| `-l, --language` | `ja` (default), `en`, `vi`, or `auto` |
| `-m, --model` | Whisper model; default `large-v3` |
| `-o, --output` | Output `.vtt` path |
| `-j, --workers` | Chunks decoded in parallel (`0` = auto) |
| `--glossary-file` | Term file to use instead of the project default |
| `--keep-silence` | Transcribe silent stretches instead of dropping them |
| `--no-normalize` | Skip the gain lift applied to near-inaudible audio |
| `--fresh` | Ignore resume state |

`mm -h` and `mm transcribe -h` list the rest.

## Extract, via Claude/Cursor

```bash
mm skill init                   # both targets (default)
mm skill init --target claude   # .claude/skills/<template>/SKILL.md
mm skill init --target cursor   # .cursor/rules/<template>.mdc
```

The four built-ins are `summary`, `action-items`, `decisions`, and `qa`. Each states
the fields to capture, what to *exclude*, and an example of its own output; shared
guidance about reading a VTT is appended automatically, so a custom template only has
to describe its own job.

### Custom templates

```bash
mm skill list
mm skill add risks                            # scaffolds .meeting-minute/templates/risks
mm skill init --template risks
```

A template is a Markdown file with frontmatter:

```md
---
name: risks
description: Extracts risks, blockers, and concerns raised in a meeting transcript. Use when the user asks what could go wrong, what is at risk, or what is blocking the project in a .vtt transcript.
outputFile: risks.md
---

Instructions for what to extract and how to format the output.
```

Four rules do the heavy lifting, and a custom template should keep them:

- **Say what it does *and when to use it* in the `description`.** Claude and Cursor
  select a skill by matching that one line against what the user asked, so
  `Extracts risks from a meeting transcript.` describes a skill that never fires.
  Name the words a user would actually say, in third person, as above.
- **Cite a timestamp for every entry** (`[HH:MM:SS]`). Timestamps are the one thing a
  VTT has that a plain transcript does not, and a cited claim can be checked against
  the recording in seconds. It also anchors the model: an entry it cannot cite is
  usually an entry it invented.
- **Say what to exclude, not just what to collect.** "Extract action items" quietly
  sweeps in ideas that were floated and dropped. The built-ins name the near-misses
  explicitly.
- **Allow an empty result.** A meeting with no decisions is normal; a heading that
  must be filled is how a decision log acquires fictional decisions. Templates ask
  for `_None found._` instead.

The rendered skills open with a numbered workflow — read the whole file, collect with
timestamps, drop what you cannot cite, then format in the transcript's own language —
and warn that the input is speech recognition output with no speaker labels, so
attribution has to be earned from the text and garbled passages are flagged rather
than confidently repaired.

## Languages

`ja`, `en`, and `vi` are supported (plus `auto`); anything else is rejected with a
clear error rather than silently mistranscribed.

```bash
mm transcribe hop.mp4 -l ja
mm transcribe standup.mp4 -l en --glossary 'Kaopiz,Quan,Jira'
mm transcribe hop-noi-bo.mp4 -l vi --glossary 'Kaopiz,Quan'
```

Choosing a language does more than pass a code to Whisper. Each carries its own
profile: the initial prompt is written **in that language** (a Japanese prompt does
nothing to bias English decoding), the glossary is folded into a sentence in that
language (`A、Bという言葉が出てきます。` vs `Terms such as A, B are used.`), and cues
break on that script's sentence marks. Cue length differs too — Japanese packs far
more meaning per character, so its cues cap at 40 characters against 90 for Latin
text.

## Config

- Global defaults: `~/.config/meeting-minute/config.json`
- Per-project overrides: `.meeting-minute/config.json`

```json
{
  "model": "large-v3",
  "language": "ja",
  "backend": "faster-whisper",
  "chunkMinutes": 2,
  "workers": 0,
  "batchSize": 16,
  "beamSize": 5,
  "computeType": "auto",
  "removeSilence": true,
  "glossary": ["カオピーズ", "要件定義", "統括"],
  "glossaryFile": "",
  "templates": ["summary", "action-items"]
}
```

## Accuracy notes (Japanese)

Defaults are tuned for accuracy over speed, based on A/B runs against real meeting
recordings. Several plausible-sounding settings measured *worse*, which is why they
are off:

- **Model.** `large-v3` is the default. `large-v2` lost punctuation and misheard
  記憶→曲 and 勘違い→間違い on longer input; `kotoba-whisper` reads raw Japanese
  well but emits no punctuation and ignores the initial prompt; `medium` misheard
  月に1回→先に一回. `large-v3-turbo` matched `large-v3` at roughly 3x the speed —
  set `"model"` to it if you want that trade.
- **Precision.** `computeType` resolves to `float32` on CPU rather than
  ctranslate2's `int8`, which silently dropped whole phrases.
- **Glossary.** Supplying participant names and domain terms is the part of the
  prompt that does real work. On the sample recordings it fixed スタックホルダー →
  ステークホルダー, 丹さん → タンさん, and シェアリング → ヒアリング, each with no
  other change to the transcript. It does not fix everything: ITデスク stayed
  ITデスク with ITヘルプデスク in the glossary, because the syllables simply are not
  in the audio. Bias, not override.
- **The glossary is folded in as a sentence, not a list.** This one is easy to get
  wrong and expensive: appending `用語: ステークホルダー。` corrected the term but
  cost three cues and 39 characters of real speech at the end of the recording.
  The same single term as prose (`…という言葉が出てきます。`) corrected it and lost
  nothing — output otherwise identical to the no-glossary run. Whisper imitates its
  prompt's format, and a labelled list is not what a transcript looks like.
- **The prompt sets style, not subject.** Whisper treats `initialPrompt` as text
  it is *continuing*, so it inherits both the vocabulary and the format. The
  built-in prompts therefore say only that this is meeting speech written in full
  sentences with punctuation — the Japanese one exists mainly because without it
  the transcript arrives as one unpunctuated run, and the Vietnamese one carries
  diacritics because an unaccented prompt invites unaccented Vietnamese back.
  A prompt that *describes the agenda* measured no better than a content-neutral
  one (135 vs 142 characters recovered on the same clip) and is actively risky:
  you cannot know a meeting's topics in advance, and over quiet stretches
  `large-v3` recites the prompt into the transcript verbatim. Put terms in
  `glossary`, not in the prompt.
- **Beam size.** 5 is the default; 10 measured no better.
- **Audio cleanup is off by default and mostly harmful.** `dynaudnorm` induced a
  hallucinated かっこいい and turned 記憶 into 供給; denoising was a wash (`--denoise`
  if you want it anyway). The one cleanup that helps is a plain gain lift on
  near-inaudible recordings, applied automatically below -30 LUFS, because the
  VAD returns *zero* speech on very quiet audio (`--no-normalize` to skip).
- **Silence is stripped before decoding** (`--keep-silence` to disable). Meeting
  tape is mostly dead air — the 23-minute sample holds ~3.5 minutes of speech —
  and a silent window gives the decoder nothing to condition on, so it recites
  its own prompt into the transcript. Dropping the gaps cut that file's runtime
  from ~4 min to ~50 s and recovered two utterances that were previously lost.
  **Cue timestamps still refer to the original recording**: the removed spans are
  remembered and every timestamp is mapped back, so the `.vtt` lines up with the
  source video.
- **Parallelism** (`-j/--workers`, default auto). Chunks are decoded concurrently
  by one shared model — ctranslate2 reuses the weights across workers, so N-way
  parallelism costs no extra memory (a second `large-v3` copy would be ~3 GB).
  Output is byte-identical to a sequential run, timestamps included. Measured on
  a 10-core M4: 163 s → 97 s on the sample file. Raising `cpu_threads` instead
  does nothing — Whisper's decode loop is autoregressive — so each worker gets a
  single thread and `auto` caps at the 4 performance cores.
- **Chunk size** defaults to 2 minutes so a meeting yields several chunks to
  spread across workers. Smaller is *not* better: 1-minute chunks measured
  *slower* (155 s vs 97 s) because every chunk repays Whisper's fixed 30-second
  window and temperature-fallback cost.
- **Chunk boundaries follow removed silence.** Once the gaps are gone, re-running
  silence detection only finds sub-second pauses *inside* sentences, so cuts are
  placed at the joins between speech regions instead — the one set of seams known
  not to fall mid-utterance.
- **Stock hallucinations are filtered.** Whisper's Japanese training data came
  from video, so at a chunk seam it emits sign-offs like ご視聴ありがとうございました.
  Those exact strings are dropped; a plain ありがとうございました is kept.

## Development

```bash
git clone https://github.com/milky-way-66/metting-minutes
cd metting-minutes
uv sync
uv run mm --help

uv run pytest tests/unit          # fast, no external deps
uv run pytest tests/integration   # needs ffmpeg on PATH; skips cleanly if missing
uv run pytest                     # both
```

## License

MIT — see [LICENSE](LICENSE).
