Imported from ethereumclassic/community-calls (
.claude/skills/call-markdown/SKILL.md). Install upstream withnpx skills add ethereumclassic/community-calls --skill call-markdown. Copyright stays with the author.
name: call-markdown description: Turn a call transcript into the committable call markdown — cleaned transcript + chapters + AI summary — without rendering any video. Content-aware: it inspects calls/.md and runs only the stages not yet done (or the one you ask for). This is the "update the markdown" half of the call pipeline; use it directly when you want to process or fix a call's text without making a video.
Call-Markdown Skill
Builds the one committable text artifact for a call — calls/<id>.md — from a
transcript: a cleaned ## Full Transcript (with a NOTE chapters block) plus a
# AI Summary and ## Key Points Discussed. It does no audio and no
video; that is render-call. Run this whenever you want to update a call's
markdown — first processing, or fixing one part later.
Content-aware. Inspect calls/<id>.md first and run only what's missing, or
the specific stage the user asks for. Stages and their done-markers:
| Stage | Produces | Done when the markdown has… |
|---|---|---|
| A — Clean transcript + speakers | ## Full Transcript ```webvtt block |
a ```webvtt block |
| B — Chapters | NOTE chapters inside that block |
NOTE chapters in the block |
| C — Summary + key points + blurb | # AI Summary, ## Key Points, summary: |
a # AI Summary section |
Operate one stage at a time and CONFIRM before moving on. Show proposed changes, let the user edit, then proceed. Never auto-run all stages silently.
Inputs and the transcript drop location
- Call id
<id>= thecalls/<id>.mdbasename, e.g.20260612_054. The agenda markdown usually already exists. - A transcript. The recommended, canonical place to drop the original
transcript is
transcripts/<id>.vtt(top-leveltranscripts/, a sibling ofcalls/). It is not underpublic/so it is never served or built — it is pure reference input that this skill reads to populate the markdown. If the user has a transcript file elsewhere, prompt them to drop (a copy of) it attranscripts/<id>.vtt. Both the original and any diarized version are committed (text reference of record); never overwrite the original on re-runs. - If the call needed diarization (a shared-mic / shared-account call, handled
by the separate
diarizeskill), readtranscripts/<id>.diarized.vttinstead — it is the corrected, cue-split reference. Prefer the.diarized.vttwhen present.
Throughout, "the raw transcript" means whichever of these you consumed
(transcripts/<id>.diarized.vtt if present, else transcripts/<id>.vtt).
Conventions (ground truth)
- The committable artifact is
calls/<id>.md. Preserve its existing frontmatter and agenda; this skill only adds/edits the transcript, chapters, and summary sections. - The page derives job/meta/transcript from the markdown via the dev-only
endpoints in
src/pages/videogen/[call]/. Do not writepublic/videogen/<NN>/{job,meta,transcript}files — the markdown is the single source of truth. - Speaker registry (
speakers/speakers.yaml): maps a key →{ displayName, aliases, avatar, github }.resolveSpeaker()(src/lib/videogen/speakers.ts) matches a label against key/displayName/aliases. Every label in the cleaned transcript must resolve. - Committable:
calls/<id>.md,transcripts/<id>*.vtt,speakers/speakers.yaml- avatars. Only commit when the user asks.
Stage A — Clean the transcript + speakers (review, confirm)
The original at transcripts/<id>.vtt is the immutable reference; never edit it.
The cleaned version goes into the markdown.
-
Deterministic pre-pass — BEFORE any agents (plain code, no LLM). Three mechanical, repeatable transforms that also make the agents' job easier:
- Normalize known speaker labels against
speakers/speakers.yaml— map every label that resolves (key / displayName / alias, case-insensitive) to the canonicaldisplayName. A label that does not resolve is left untouched and surfaced in item 3; never guess here. - Apply the glossary in
speakers/glossary.yaml— for each canonical entry, replace any listedaliases(known ASR mishearings, e.g.Firo→Phyro,CoreGet→Core-Geth) with the canonical spelling, word-boundary-matched, longest alias first. This bakes in corrections the user has already approved so the agents don't re-litigate them. - One subtitle per sentence. Two sub-steps that together give
sentence-level granularity (never giant multi-sentence walls, never a
sentence split mid-word):
- Split each cue at every internal hard sentence terminal —
.?!(optionally a closing quote) followed by whitespace. A cue can hold several complete sentences (the ASR/diarizer emits long cues); break them. The split time is interpolated within the cue by character position, so every boundary lands inside a real raw cue (the validator's parity allows a boundary strictly inside a raw cue, not only on its edges). - Stitch across cues: merge a piece into the running block while that
block does not yet end at a hard terminal (
[.?!]). As soon as it ends at.?!— or the speaker changes — close it. An ellipsis…is a trailing-off pause, NOT a sentence end — never split on it and always stitch through it. A speaker who breaks up a thought with "So… if… it's…" must become one subtitle, not a column of one-word fragments. (This was a hard lesson: treating…as a terminal over-splits badly.) Do not apply a "~N sentences per block" cap. The validator'sTERMINALis[.?!…], so a…-ended block (e.g. a speaker trailing off before another takes the floor) is still considered complete and does not warn; only a block ending mid-word before a same-speaker block warns.
- Split each cue at every internal hard sentence terminal —
- Normalize known speaker labels against
-
Analyze the blocks with a dynamic workflow — chunked, not sentence-by-sentence. A single whole-file pass lets homophones through (
miners/minors), leaves garbled orphans in, and spells one name three ways. Fan out over theWorkflowtool (this skill is your opt-in — you do not need the user to say "ultracode"):- Chunk the grouped blocks by range into ~50 contiguous chunks
(
ceil(totalBlocks / 50)blocks each), never splitting a block; each chunk carries its real block numbers + timestamps so every fix maps back. - One agent per chunk (
model: 'haiku'default;'sonnet'for a dense technical call),pipeline()/parallel(). Give every agent the same shared context: registry display names + aliases, the proper-noun glossary loaded fromspeakers/glossary.yaml(each canonical spelling + its optionaldescription, so the agent can judge whether an uncertain match fits the context), and the fix rules. Each agent returns structured findings (schema), not rewritten prose. - Fix rules (keep timestamps untouched):
- Mis-heard proper nouns — "Sim Classic" → "Ethereum Classic", "theoremclassic.org" → "ethereumclassic.org", "DAP" → "dApp", "AMP pool" → "Antpool", "two miners" → "2Miners", a name slip ("Astora" → "Istora").
- Homophones in context — "base fee refund for minors" → "miners".
- Garbled / orphan fragments — a stray one-word cue ("bomb.") or mis-segmented phrase ("real estateless one" → "real stateless one") gets flagged for repair or merge, not left verbatim.
- Stutters / repetitions — collapse "why, why are we, why are we assuming that? Why are we assuming that?" → "Why are we assuming that?".
- Light punctuation/casing. Preserve meaning; don't paraphrase substance.
- Flag (don't relabel) speaker labels — the pre-pass normalized known
ones. Surface anything still unresolved or a label that looks
mis-attributed for item 3; do not silently relabel cues. (Acoustic
attribution is the
diarizeskill's job, upstream — not this one's.) - Spoken name mentions stay as spoken — when a name appears in the
dialogue body (not the speaker label), transcribe it as said; only fix a
clear ASR error to its glossary canonical. Do not swap a spoken name
for a participant's Zoom handle (e.g. keep "Wiedergarten" if that's what was
said, even though the speaker label for that person is
weder).
- Cross-chunk consistency pass (required). After aggregating, run one
reconciliation over the union of findings to make every recurring
name/term uniform across the whole call (e.g.
Wego/WeGo/Wigo→ one). This is the class of error chunking exists to catch. Each chunk agent splits findings into Confident (clear ASR errors, obvious proper nouns, contextual homophones, plain de-stutter — apply directly) and Flagged (any name you can't verify, any repair that risks changing meaning, any inconsistently-spelled term with no obvious canonical form — collect, do NOT auto-apply; record cue/time, raw line, proposed fix, reason).
Sketch:
const FINDINGS = { type: 'object', properties: { confident: { type: 'array', items: { type: 'object', properties: { cue:{type:'number'}, time:{type:'string'}, raw:{type:'string'}, fixed:{type:'string'}, kind:{type:'string'} } } }, flagged: { type: 'array', items: { type: 'object', properties: { cue:{type:'number'}, time:{type:'string'}, raw:{type:'string'}, proposed:{type:'string'}, reason:{type:'string'} } } }, } } const results = await pipeline(chunks, c => agent(`${SHARED_CONTEXT}\nAnalyze these cues, return findings:\n${c.text}`, {label:`clean:${c.firstCue}-${c.lastCue}`, model:'haiku', schema:FINDINGS})); // aggregate -> consistency-reconciliation agent over the union -> apply confident / present flagged - Chunk the grouped blocks by range into ~50 contiguous chunks
(
-
New speakers: for every label that doesn't resolve via the registry, highlight it and ask the user, per speaker:
- a mis-transcription of a known speaker (→ add as an
alias), or a real new person? - if new, the display name defaults to the Zoom label verbatim — that
string is the participant's own self-chosen display name, so register it
as-is (e.g. handles like
w1g0,weder). Do not "correct" it to a guessed proper spelling or expand it to a real name unless the user tells you to; only fold it into an existing speaker as analiaswhen it is clearly an ASR garble of a known person. Then prompt for an avatar image URL (skip → initials), download it next to the yaml (curl -L -o speakers/<key>.<ext> "<url>"), and add aspeakers/speakers.yamlentry (displayName= the label, plusaliases/avataronly if applicable).
- a mis-transcription of a known speaker (→ add as an
-
Show a representative sample of the confident cleanup (not all blocks) and the resolved speaker list, and present the flagged fixes — block/time, raw line, proposed fix, reason — for the user to confirm/correct/reject one by one. Apply their decisions before writing. When a flagged item is a proper noun the user confirms (a name/term not yet in
speakers/glossary.yaml), append it tospeakers/glossary.yaml— the canonical spelling as the key, the ASR variant(s) seen asaliases, and a shortdescriptionif useful — so future cleanups apply it automatically in the pre-pass. This is how the glossary grows. -
Propose the roster — the distinct people who actually spoke, as canonical display names. Curate out noise (someone merely quoted, a chat-only relay with no cues). Confirm the list; it goes to frontmatter in Stage D and drives the call-page roster (and, later, the video).
-
Write the cleaned transcript into the markdown — a
## Full Transcriptsection with the cleaned, sentence-grouped cues in a```webvttblock (noNOTE chaptersyet) — and have the user review it rendered at/calls/<N>(the call number, e.g./calls/54— not the<id>basename; renders flat viaremark-webvtt; keep it inline). Intermediate gate: get the transcript right before chapters. -
Validate — required gate, before the rendered review. Resolve every ERROR:
node scripts/videogen-validate.mjs calls/<id>.md --raw transcripts/<id>.vtt(Use
transcripts/<id>.diarized.vttfor--rawif the call was diarized.) It parses the```webvttblock as the site does and checks: WEBVTT header; every timestamp well-formed withstart <= end; non-decreasing order; no empty cues; timestamp-boundary parity against the raw — every cleaned block's start/end is a real raw boundary and the run covers the same span with no gaps/overlaps (proves no timestamp was invented, no span dropped); and every speaker label resolves viaspeakers/speakers.yaml. It also WARNs on a same-speaker block ending mid-sentence and continuing next — those are mid-sentence cuts the pre-pass should have merged. Errors fail the gate; re-run until clean. (No chapters yet, so chapter checks are skipped here.)
Stage B — Chapters (discuss, then confirm)
- From the cleaned transcript, draft chapters as a
NOTE chaptersblock,[h:]mm:ss Title, one per line on the displayed timeline. Aim for ~10 max. Favour few coherent chapters; merge short adjacent topics (housekeeping, a brief aside, two halves of one debate); don't carve a 30-second aside into its own chapter. - Sanity-check every boundary against real cues — enforced by the validator. Each chapter start MUST be the exact start timestamp of a cue, and the first cue of a speaker's turn that opens the topic. Never a round-number guess, never mid-turn, never mid-sentence (a round minute almost never coincides with a real cue start — if it's round, it's wrong).
- Iterate against the validator until zero chapter errors:
For each off-grid boundary it reports the nearest real cue (time + speaker + text); snap to the cue that actually opens the topic — not blindly the nearest, which is often mid-thought; the right one may be a few cues away. A mid-turn WARN is acceptable only for a deliberate host pivot (same host monologuing across a topic change); otherwise move to a turn-opening cue.node scripts/videogen-validate.mjs calls/<id>.md - Offer ~3 grouping options at different granularities — not a single draft.
First explain the rationale (chapters drive both the website's chaptered
transcript and the video TOC; they bookend coherent topics, open on a real
turn, stay scannable), then present three already-validated options:
Coarse (~5), Balanced (~7, usually mirrors the AI-summary subsections,
the default), Fine (~10). Each must pass the validator first. Give a
one-line rationale + the full
mm:ss Titlelist, let the user pick or mix, then iterate as a conversation (retitle, merge, move splits), re-running the validator after any edit. Only write the finalNOTE chaptersblock once the user has chosen and the validator is clean.
Stage C — AI summary + key points + blurb (review, confirm)
From the cleaned transcript, generate, matching the most recent completed
call's format exactly (read one with a # AI Summary + webvtt transcript as the
style template — including section order: ## Key Points Discussed near the top
with a link down to #ai-summary, then the existing agenda, then # AI Summary
with ### subsections that mirror the chapters, ending in an ### Action Items
subsection):
# AI Summary— structured subsections with Details / Conclusion.## Key Points Discussed— bullets.- A 1–2 sentence
summary:blurb for the video's summary slide. - Referenced projects / links — scan the transcript for any project, app,
article, repo, ECIP/EIP, person's site, or tool that was named or pointed
to ("I posted an article", "check the repo", "add it to the show notes"), and
weave them in as markdown links in the summary / key points / an Action Items
line. Reuse a URL already present in the call's agenda frontmatter/body if it's
there. Never fabricate a URL — if you can't verify the exact link (e.g. an
ECIP number with no canonical URL to hand), list the reference and ask the
user for the link rather than guessing. This is how the show-notes links get
built.
Show each; get approval. Follow the writing style in
AGENTS.md.
Stage D — Write the markdown
By now the cleaned transcript (A) and NOTE chapters (B) are in calls/<id>.md.
Add the rest, preserving existing frontmatter + agenda:
summary:blurb +roster:(confirmed in A.5) → frontmatter; ensureyoutube:is set (11-char id; if not uploaded yet, note links are inert until it is).- The
# AI Summaryand## Key Points Discussedsections. Tell the user it's saved (all text, no media) and offer to iterate. Do not commit unless asked.
Hand-off
The markdown is complete (transcript + chapters + summary). If the user also wants
a video, that's the render-call skill. If they only wanted to update the text,
you're done.