SKILL.md 22 KB


name: session-summary

description: Reconstructs a recorded conversation from a transcription session directory (transcripts// with transcripts.jsonl, brief.md, audio/) and writes English and Spanish summaries (session-summary.svg and session-summary.es.svg, plus size-optimized PNG exports of each) into that directory, correcting ASR mishearings, language mis-detection, speaker misattribution and overclaiming, then rewrites the triage brief.md with the same corrections and adds what it learned to its ASR glossary. Use when asked to summarize, reconstruct, visualize or fix the brief of a transcripts/ session folder, or to create session-summary.svg / session-summary.es.svg.

Session summary from a transcription directory

Input: a session directory such as transcripts/2026-10-05-15-01-08/. All outputs are written into that same session directory, never elsewhere. Both language versions are always produced:

  1. session-summary.content.json: the reconstructed content, in English (source of the SVG).
  2. session-summary.svg: the one-page visual reconstruction, in English.
  3. session-summary.es.content.json: the same content translated to Spanish.
  4. session-summary.es.svg: the visual reconstruction, in Spanish.
  5. session-summary.png and session-summary.es.png: size-optimized PNG exports of the two SVGs (written by render.py in Step 3, checked in Step 4).
  6. brief.md: corrected in place.

The run is not complete until both SVGs and both PNGs exist in the session directory and pass Step 4.

Run every command from the repository root. Bundled files live in .opencode/skills/session-summary/:

  • references/glossary.md: domain terms and known ASR mishearings. Read it first; update it with what this session taught you in Step 6. This is the only file outside the session directory that a run may change.
  • scripts/render.py: renders the SVG from the content JSON plus transcripts.jsonl and exports the matching PNG next to it. It computes the transcription-quality metrics, wraps text, lays out any number of topic cards and sizes the canvas. Never hand-write the SVG and never type in metrics yourself.

Hard rules

  • Never modify transcripts.jsonl or audio/. They are the evidence.
  • Write the content JSONs, the brief and the glossary only with the Write/Edit tools. The SVGs and the PNGs are both written by render.py (PNGs via Inkscape/ImageMagick). Never use shell redirection, sed, tee, cat > or similar to produce or change files.
  • Every claim in the SVG and brief must be traceable to a segment (seg_id). If you cannot point at a segment, do not write the claim.
  • Paraphrase, do not invent. When a token cannot be resolved, say so in the footer and add it to the glossary's Unresolved list; never make up a meaning for it.
  • The final SVGs and PNGs always live in the session directory. render.py keeps its intermediate raster in a private temp dir and deletes it itself; never leave the only copy of a deliverable in /tmp.
  • Single-session scope. Work only on the session directory the user named. Never list, glob, grep, open or compare any other directory under transcripts/: not earlier sessions for "context", not sibling sessions for speaker or term hints, not their SVGs, briefs or JSONL. Do not run ls transcripts/ or any pattern that expands across sessions. Everything you need is in the named directory plus the allowed non-session files below. If the evidence is not enough, say so in the footer and the report instead of looking elsewhere.
    • Allowed outside the session directory: this skill's references/glossary.md and scripts/render.py, and speakers/speakers.json (name mapping only). Never read other repositories; the glossary is the only vocabulary source.
    • Prior sessions' knowledge reaches you only through the glossary. Do not re-derive it from old transcripts.

The brief is low-trust input

brief.md is generated live by a small, fast LLM during the session. Expect low accuracy.

  • Trust order: transcripts.jsonl is the evidence; brief.md is only a list of possible leads. Never copy a fact from the brief into the SVG or the corrected brief without finding its segment. Do not let its framing anchor your reconstruction: reconstruct from the JSONL first, then read the brief.
  • Use the brief to check coverage: after reconstructing, go through the brief. For anything it mentions that you did not capture, look for it in the transcript, and keep it only if a segment supports it.
  • Known small-model failure patterns (check every one, in the brief and in your own output):
    • sections and sentences cut off mid-word (e.g. "Open Ques");
    • the same fact repeated with slightly different wording;
    • "Unknown participant" for lines the JSONL attributes to a named speaker;
    • ASR mishearings copied literally ("Good One", "dedo del hígado");
    • noise from mis-detected languages reported as statements ("said in Cyrillic");
    • hedged statements turned into "approved" decisions or "confirmed" opportunities;
    • reversed reasoning (who pays whom, buyer vs seller);
    • worked examples and on-screen steps dropped;
    • acknowledgments and greetings logged as facts.
  • Calibrate the size of the rewrite: expect to rewrite most of the brief, not patch it. If your corrected brief still reuses most of the original wording, you probably anchored on it. Go back to the JSONL.

Step 1: Load the evidence

Dump the segments in a compact, readable form (read-only; output to the terminal only):

uv run python -c "
import json, sys
for l in open(sys.argv[1]):
    d = json.loads(l)
    print(d['seg_id'], d['ts'][11:19], d['lang'], round(d['lang_prob'], 2),
          d['speaker_status'], d.get('speaker_score'), '|', d['speaker'], '|', d['text'].strip())
" transcripts/<session-id>/transcripts.jsonl

JSONL fields that matter: seg_id, ts, lang, lang_prob, text, speaker (a display name; "Name?" means uncertain, "?" means unknown), speaker_id, speaker_status (matched / uncertain / unknown), speaker_score.

Also read:

  • The speaker store speakers/speakers.json (speakers.<id>.name) to map the JSONL's speaker_ids to names. It holds everyone ever enrolled on this machine, so it is not the attendee list.
  • Any participant list or name corrections the user supplies; these override everything else.

Build the attendee list from evidence: people who speak in the JSONL, plus people the conversation shows were present or expected (greeted, addressed, thanked, or "missing"), plus the user's list. Silent and absent attendees go in the Participants block with that status; never list an enrolled speaker who has no link to this session.

If the session is long, read it in full anyway. Do not summarize from the first half.

Step 2: Reconstruct, correcting these error classes

Work through the transcript in order and build a list of topics, each with its supporting seg_ids. While doing so, fix every instance of the following. This list was distilled from real failures; check all of them, not just the first one you notice.

  1. ASR mishearings of domain terms. Normalize them using references/glossary.md (e.g. "Good One" → HUD-1, "long charges" → loan charges, "impulse" → impounds, "DIP" → deed, "CELERB" → seller, "Bayer" → buyer). Infer new mappings only when the context makes them unambiguous, and list every normalization in the Transcription quality block's corrections. Keep a running list of every new mapping, noise segment and unresolved token you find; Step 6 writes it into the glossary.
  2. Language mis-detection. Segments tagged with an unexpected lang (pt, de, ru, ja, id, ...) or in an odd script are usually misrecognized Spanish/English. Reinterpret them from context (e.g. "caixas de close" was "Charges section"), or drop them as noise. Never report them as statements made "in Portuguese/Cyrillic/Japanese".
  3. Speaker attribution. Trust matched. Check uncertain ("Name?") against the context (who was addressed, who answers). Resolve unknown ("?") only when the context is clear. Never write "Unknown participant" for a line the JSONL attributes to someone. Resolve name aliases against the attendee list and the glossary (e.g. "Efraín", "Fren" → Efren), and spot compound mishearings ("Efraín Reyes" was two people).
  4. Negation and possessive misreads. Re-read short emphatic lines in context: "No debería ser un problema mío" meant "it shouldn't be a problem", not "not my problem".
  5. Proposal vs agreement vs decision. Label a statement as agreed only if someone else explicitly accepts it. A suggestion followed by an objection is a proposal plus an objection, not a "fallback agreed on the call". Limits stated by one person are "stated", not "agreed".
  6. Direction of reasoning. Payer vs payee, buyer vs seller, debit vs credit, cause vs effect: re-check every sentence that has a direction. Do not invert it.
  7. Context attribution. Keep separate what a participant experienced on this call and what they report from someone else (e.g. the client). Do not move one into the other.
  8. Overclaiming and editorializing. No verdict titles ("the design was right after all"). No "no development needed" when a dependency remains (undocumented tags, missing access). Use "can", "once X", "to corroborate" where the speaker hedged.
  9. Roles. Take people's roles from what they say about themselves ("desde Customer Support"), not from who shares the screen or talks the most.
  10. Unresolvable tokens. Nicknames, slang and garbled words: resolve them when standard ("capa 8" = layer 8 = user error); otherwise mark them as unresolved. Never turn them into entities such as "the standing channel".
  11. Omissions. Make sure the concrete, practical moments survive: worked examples, on-screen walk-throughs, exact steps, numbers and item references. These are the most useful parts and the ones small models drop first.
  12. Duplicates and redundancy. Each fact appears once, in the card where it belongs.

Only after the reconstruction is complete, read brief.md and run the coverage check from "The brief is low-trust input". Record where the brief disagrees with the audio for the footer.

Step 3: Build the SVG

Write session-summary.content.json in the session directory (Write tool), then render it. The page layout is fixed and generated by the script:

  • Header: eyebrow, title, and an auto-generated date line, plus three stats computed from the JSONL (segments, session length, speed versus real time).
  • Main-point band: headline, status label, pill, body.
  • Left sidebar, always present:
    • Session focus: the 2–5 things the session set out to do or ended up doing.
    • Participants and roles: the attendee list from Step 1, including silent or absent attendees, each with a role inferred from the dialogue.
    • Transcription quality: language mix bar, session length, reconstructed speech, pause cuts, low-confidence segments (lang_prob < 0.8), speaker identification rate, uncertain/unknown speakers, mis-detected languages, median latency (all computed), plus the list of name and term corrections you made.
  • Topic cards, unlimited: one card per distinct topic, in conversation order. Do not merge unrelated topics to save space and do not pad with filler cards. Cards fill the main column beside the sidebar, then continue below it in two-column rows (an odd last card spans the full width). The usual arc is case/problem → answer → blockers → scope → settled → to build → external context, but follow the conversation.
  • Signals block, always present (full width, after the cards): Opportunities and Blockers / Risks panels are always drawn (an empty list renders "None identified in this session"); Open questions is drawn only when the list is non-empty. These carry the same items as the corrected brief.md sections of the same names (Step 5), each traced to a segment. Do not duplicate them as topic cards; cards hold the narrative, this block holds the forward-looking summary.
  • Next steps (optional): action / owner / when table with a closing note.
  • Footer: method notes, one string per paragraph.

Content JSON schema (all text is plain; the script escapes XML and wraps lines):

{
  "lang": "en",
  "expected_langs": ["es", "en"],
  "title": "≤ ~65 chars; the script shrinks it if longer",
  "eyebrow": "optional; defaults to SESSION RECONSTRUCTION",
  "description": "one-paragraph <desc> for accessibility",
  "main_point": {"headline": "...", "status": "SHORT STATUS", "pill": "TIMELINE · OWNER",
                 "body": "two or three sentences"},
  "focus": [{"label": "...", "detail": "...", "tone": "warn (optional: amber + italic)"}],
  "participants": [{"name": "Full Name", "role": "role, from the dialogue",
                    "status": "speaks | silent | absent", "initial": "optional"}],
  "participants_note": "optional italic note (who never spoke, who was missing)",
  "corrections": ["«heard» → meant · «heard» → meant", "..."],
  "topics": [{
    "accent": "amber | teal | red | violet | green | blue | rose | slate",
    "eyebrow": "TOPIC · ANGLE", "title": "...", "subtitle": "optional",
    "sections": [{"label": "...", "tone": "limit (optional: red bullets)",
                  "bullets": ["...", "..."]}],
    "chips": ["optional short tags, e.g. variants"],
    "notes": ["optional closing lines"]
  }],
  "opportunities": ["REQUIRED key: what the conversation shows could be gained or enabled; [] if none"],
  "blockers": ["REQUIRED key: blockers, risks, limits and dependencies; [] if none"],
  "open_questions": ["optional: real unresolved items; omit or [] when there are none"],
  "next_steps": {"rows": [{"action": "...", "owner": "...", "when": "..."}],
                 "note": "optional"},
  "footer": ["Speaker-ID mapping and how uncertain/unknown lines were attributed ...",
             "Mis-detected language segments: reinterpreted or dropped ...",
             "Unresolved tokens ...", "Where the brief disagreed with the audio ...",
             "Wording is paraphrase, not verbatim."]
}

Where method information goes, so nothing is duplicated:

  • corrections (Transcription quality block): every name and term normalization.
  • participants / participants_note: who attended, spoke, stayed silent or was missing.
  • footer: speaker-ID mapping, mis-detected-language segments, unresolved tokens, brief discrepancies, and the paraphrase notice. Segment count, span and metrics are already in the header and sidebar, so do not repeat them.
  • Cards: content only, never method notes.

Signals writing rules: opportunities, blockers and open_questions are plain strings, one idea each, hedged exactly as the speaker hedged (an idea floated is an opportunity "to explore", never a confirmed one). Every item needs a supporting segment. blockers must include limits and unmet dependencies (missing access, undocumented tags), not only stated problems. Only list an open question the conversation actually left unresolved. In the Spanish version these are the same items, same order, translated.

Card writing rules: give each card 1–4 labeled sections with 1–3 bullets each, one idea per bullet. Use "tone": "limit" for blockers and limits. Keep the claim strength the speaker used.

Spanish version (always required): write session-summary.es.content.json with the same structure and the same facts, "lang": "es" (sidebar labels, metric names and dates switch automatically), and every string translated except the team's own working terms (product and domain terms they say in English, such as closing disclosure, settlement statement, charges). Keep their idioms where natural. The two versions must match: same topics, same order, same claims, nothing added or dropped in translation.

Render both into the session directory. render.py writes the SVG, checks that it is well-formed XML, and exports the PNG beside it (same stem, .png) in the same run:

S=transcripts/<session-id>
R=.opencode/skills/session-summary/scripts/render.py
uv run python $R $S $S/session-summary.content.json    $S/session-summary.svg
uv run python $R $S $S/session-summary.es.content.json $S/session-summary.es.svg

Each run prints wrote <svg> and wrote <png> (<n> KB). If it prints an error: line or exits non-zero (missing inkscape/magick, failed export), the run has failed: fix it or report it. Never substitute a manual or ad-hoc export. --no-png skips the PNG and is only for draft re-renders; the final render must not use it.

Step 4: Verify both SVGs and PNGs

The PNG is the shareable copy of the SVG: full 1680 px width, palette-quantized to 64 colors without dithering, metadata stripped (render.py owns this recipe). The flat-color design makes this visually lossless for text and cards while cutting the file to roughly a quarter of Inkscape's raw export (about 1.1 MB → 0.27 MB on a typical session). Do not downscale the width (it saves little and blurs the small text) and do not use dithering (it bloats the file). The raw Inkscape export lives in a private temp dir that render.py always deletes.

Confirm all four files exist in the session directory and that each PNG is roughly 0.2–0.4 MB (about 1 MB means the quantization did not run):

ls -l $S/session-summary.svg $S/session-summary.png $S/session-summary.es.svg $S/session-summary.es.png

Then open each final PNG with the Read tool and inspect it. Wrapping is automatic, so look for content problems instead: a card that reads as two topics (split it), a near-empty card (merge it into its neighbor or add the missing substance), a wrong accent or tone, and long unbroken tokens that still run past an edge. Confirm the Opportunities and Blockers / Risks panels are visible (and Open questions, if the session has any), and that render.py printed no "no 'opportunities'/'blockers' key" warning. If you edit the JSON, re-run the two render commands once; the PNGs are regenerated with the SVGs.

Step 5: Correct brief.md

Rewrite brief.md in place, keeping its section format exactly. The layout is below; copy the header wording from the original file (the triage code looks for the Prior Details: marker):

Summary:
<one paragraph>
Opportunities:
- ...
Blockers/Risks:
- ...
Decisions:
- ...
Action Items:
- <Owner>: ...
Open Questions:
- ...

Prior Details:
- <chronological facts>

Apply all twelve error classes from Step 2. In addition:

  • Complete truncated sections and sentences (small models cut off mid-word, e.g. "Open Ques").
  • Remove duplicate facts and filler ("X acknowledged with 'Thank you'", greetings, noise lines).
  • Keep Prior Details: chronological, one fact per line, with named speakers.
  • Decisions: contains only what was actually agreed; Open Questions: lists real unresolved items from the conversation.
  • The brief and the SVGs must agree with each other: Opportunities:, Blockers/Risks: and Open Questions: hold the same items as the SVGs' opportunities, blockers and open_questions (the brief in its own language, the SVGs in both). Write them once, here and in the content JSON, from the same reconstruction.
  • Keep the brief in the language it was generated in (English or Spanish; it follows the language of the conversation). Do not translate it. Keep the section headers exactly as they appear in the original file, since the triage code looks for Prior Details:.

Step 6: Update the glossary

Write everything new from Step 2's running list into .opencode/skills/session-summary/references/glossary.md. Edit it with the Edit tool and put each item in the table where it belongs, not in a pile at the end:

  • New mishearing: add a row to the matching Part 1 table (Documents and settlement types; Charges and sections; Letters and charge codes spoken aloud; Slang, tools and people). If the "means" side is a domain term, check its canonical spelling against Part 2 of the glossary only; do not look in other repositories. Mark it "(likely)" when the context strongly suggests it but does not prove it.
  • New spelling of a known term: add the variant to the existing row instead of a new row.
  • New noise pattern: add a row to "Language mis-detection noise" only if it shows something the table does not already cover (a new language tag, or a fragment that had to be reinterpreted rather than dropped).
  • Unresolved token: add it to "Unresolved (do not invent meanings)" with the session ID and segment.
  • Resolved token: if this session resolves something listed under "Unresolved", move it to Part 1 and cite the segment that resolved it.
  • New canonical vocabulary: if the conversation used a real product term that is missing from Part 2 and this session's transcript makes its spelling and meaning unambiguous, add it to the right Part 2 section. If it cannot be confirmed from the transcript, list it under "Unresolved" instead.

Rules:

  • Only add what this session's transcript supports. Cite it in the row as (<session-id>, seg <n>). Never add guesses without "(likely)", and never add a meaning for an unresolved token.
  • Do not duplicate: search the glossary for the heard form and for the meaning first.
  • Do not rewrite or delete existing entries. If one now looks wrong, add a note under it explaining the conflict, with the segment.
  • Update the Part 1 "Sources:" line with this session ID.
  • If nothing new was found, say so in the report; do not add filler.

Step 7: Report

Finish with a short report: the output files (both SVGs, both PNGs with their sizes, both content JSONs and the brief, all in the session directory), the corrections made grouped by error class (one line each), anything left unresolved, the glossary changes (rows added, moved or annotated), and the verification result (XML parse plus render check).