Meeting notes assistant: AssemblyAI speaker labels plus Claude in Python
One Python script that transcribes a meeting with speaker labels in AssemblyAI, then has Claude write a summary, decisions and action items to a markdown file.
Create each key on its own page, paste it below, then copy the finished lines into your project's .env file. What you paste stays in this tab. It is never sent to Wirestack or saved.
One Python script, notes.py. You give it a recording, either a URL or a local file. AssemblyAI transcribes it with speaker labels. Claude reads the "Speaker A: ..." transcript and writes meeting notes with a summary, decisions, and action items with owners. The notes are saved as meeting-notes.md next to the script.
Before you start
Set up each piece on its own first, so you know both keys work:
AssemblyAI speaker labels in Python. Env var: ASSEMBLYAI_API_KEY.
Claude through the Anthropic Python SDK. Env var: ANTHROPIC_API_KEY. The SDK reads it automatically.
This bundle does not need FastAPI or uvicorn. Install only the two SDKs, plus python-dotenv to load your keys:
Put both keys in a .env file next to notes.py. The script loads that file itself. If you saved the keys on this computer with npx wirestack keys add <tool>, run npx wirestack keys fill in the project folder and it writes them into .env. Or paste them in by hand:
import osimport sysfrom pathlib import Pathimport anthropicfrom assemblyai import TranscriptStatusfrom assemblyai.prerecorded.v2 import Transcriber, TranscriptionConfigfrom dotenv import load_dotenvload_dotenv()sys.stdout.reconfigure(encoding="utf-8")# 1. Pick the recording: a URL or a local file path.audio = sys.argv[1] if len(sys.argv) > 1 else "https://assembly.ai/wildfires.mp3"# 2. Transcribe with speaker labels.config = TranscriptionConfig(speaker_labels=True)transcriber = Transcriber(api_key=os.environ["ASSEMBLYAI_API_KEY"])transcript = transcriber.transcribe(audio, config=config)if transcript.status == TranscriptStatus.error: raise RuntimeError(f"Transcription failed: {transcript.error}")# 3. Turn the utterances into a "Speaker A: ..." transcript.lines = [f"Speaker {u.speaker}: {u.text}" for u in transcript.utterances]speaker_text = "\n".join(lines)# 4. Ask Claude for meeting notes.instruction = ( "Below is a meeting transcript. Each line starts with the speaker label.\n" "Write meeting notes in markdown with these sections:\n" "## Summary (a few sentences)\n" "## Decisions (a bullet list)\n" "## Action items (a bullet list; start each item with the owner, " "for example 'Speaker A: ...'; write 'Owner unclear' if no one took it)\n" "Only use what is in the transcript.\n\n" "Transcript:\n" + speaker_text)client = anthropic.Anthropic()message = client.messages.create( model="claude-sonnet-5-5", max_tokens=1024, messages=[{"role": "user", "content": instruction}],)notes = "".join(block.text for block in message.content if block.type == "text")# 5. Save the notes next to this script.out_path = Path(__file__).with_name("meeting-notes.md")out_path.write_text(notes, encoding="utf-8")print(f"Saved notes to {out_path}")
The first lines load your keys from .env before any client is created, and make printed text UTF-8, so characters like dashes do not come out garbled, or crash the script, on Windows.
The script takes the recording from the command line. With no argument it uses AssemblyAI's sample clip. A local path works too; AssemblyAI's docs say the SDK uploads the file for you.
TranscriptionConfig(speaker_labels=True) turns on speaker labels. This is the same call as the AssemblyAI piece, minus language detection. You can add language_detection=True back, as the piece does.
Each utterance has a speaker and text. The script joins them into one line per turn, the same format the AssemblyAI piece prints.
This is the only new wiring. It uses the same SDK and model as the Claude piece, claude-sonnet-5-5, but calls client.messages.create(...) instead of streaming, as shown in Claude's Get started page. The sync anthropic.Anthropic() client is used because this is a plain script, not a server.
The reply comes back as content blocks. The script keeps the text blocks and writes them to meeting-notes.md in the same folder as notes.py.
Test it
Run it with the sample clip first:
Shell
python notes.py
You should see Saved notes to .../meeting-notes.md. Open the file. It should have Summary, Decisions and Action items sections.
Then try your own recording:
Shell
python notes.py ./my-meeting.mp3
Not confirmed
This combined script has not been run. Each piece was checked on its own, not together.
max_tokens=1024 is copied from the Claude piece. Long meetings may need a higher value; the right limit was not checked.
Very long transcripts may not fit in one request. Context limits for claude-sonnet-5-5 were not checked in this run.
The prompt wording and the section layout of the notes are our own. Claude may not follow them exactly every time.
Speaker labels are letters (A, B, ...), not real names. The docs fetched here do not show mapping them to names.
load_dotenv() does not replace a key that is already set in the terminal, per python-dotenv's README. If a key is set in both places, the terminal value wins.
In a test of a different Wirestack bundle on Python 3.14, installing its packages took more than 10 minutes, because some dependencies built from source. This bundle's packages were not timed. An earlier Python 3 version may install faster. This comes from one test and is not confirmed.
Passing config=config together with a local file path is not shown in one example on the AssemblyAI page. The page shows the URL call with config and the local file call without it.