name: session-summary
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:
session-summary.content.json: the reconstructed content, in English (source of the SVG).session-summary.svg: the one-page visual reconstruction, in English.session-summary.es.content.json: the same content translated to Spanish.session-summary.es.svg: the visual reconstruction, in Spanish.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).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.transcripts.jsonl or audio/. They are the evidence.render.py (PNGs via Inkscape/ImageMagick). Never use shell
redirection, sed, tee, cat > or similar to produce or change files.seg_id). If you cannot
point at a segment, do not write the claim.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.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.
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.brief.md is generated live by a small, fast LLM during the session. Expect low accuracy.
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.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:
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.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.
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.
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.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".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).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.
Write session-summary.content.json in the session directory (Write tool), then render it.
The page layout is fixed and generated by the script:
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.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.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.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.
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.
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:
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.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.Prior Details:.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:
Rules:
(<session-id>, seg <n>). Never add guesses without "(likely)", and never add a meaning for
an unresolved token.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).