
Synthetic Session Generator
- 56 installs
- 339 repo stars
- Updated August 4, 2026
- glebis/claude-skills
Generate realistic, persona-consistent synthetic coaching or therapy session transcripts for evals, demos, and training data, exportable in several formats.
About
Produces fictional but believable practitioner/client dialogue grounded in a chosen modality with persona consistency and modality fidelity, watermarked as synthetic. A developer uses it to build eval datasets, product demos, or few-shot examples for session-analysis tools without real client data.
- Modality fidelity (ICF/GROW, CBT, IFS, ACT/MI) and persona arcs
- Exports to Fathom/Granola, plain dialogue, JSON, or Obsidian markdown
Synthetic Session Generator by the numbers
- 56 all-time installs (skills.sh)
- Ranked #1,185 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/glebis/claude-skills --skill synthetic-session-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 339 |
| Last updated | August 4, 2026 |
| Repository | glebis/claude-skills ↗ |
What it does
Generate realistic, persona-consistent synthetic coaching or therapy session transcripts for evals, demos, and training data, exportable in several formats.
Files
Synthetic Session Generator
Purpose
Generate fictional but believable coaching/therapy session transcripts that read like real recorded sessions, while remaining clearly synthetic. Outputs feed three jobs: eval datasets (with ground-truth labels to benchmark summarizers and analyzers), product demos (realistic sessions without exposing real client data), and training/prompt examples (few-shot material for a coaching or therapy assistant).
Realism comes from two disciplines: persona consistency (a client speaks the same way, carries the same history and presenting issues across a session arc) and modality fidelity (the practitioner uses the techniques, question forms, and pacing of the chosen framework). Every output is watermarked as synthetic so it can never be mistaken for a real clinical record.
When to Use
Use when a user asks for fake/synthetic/mock/demo coaching or therapy transcripts, eval or test data for session-analysis tools (e.g. the coaching-session-summarizer), few-shot dialogue examples, or persona-consistent session series. Do not use to analyze or summarize a real transcript — that is the job of coaching-session-summarizer or transcript-analyzer.
Workflow
Step 0 — Setup mode (configure defaults)
When the user wants to configure the skill ("setup", "set my defaults", "always use Russian / IFS / 50-minute sessions"), run setup mode. Offer the three choices via AskUserQuestion, then persist them:
- Language — output language for the transcript (
en,ru,de,es,fr,pt,it,nl). - Modality — default framework (
icf-grow,cbt,ifs,act-mi). - Session duration — minutes (e.g. 25 / 50 / 80); mapped to a turn budget (~0.6 turns/min).
python3 scripts/setup_config.py --language ru --modality cbt --duration 50 --show
python3 scripts/setup_config.py --show # view current defaultsThis writes config.json in the skill directory. Later scaffold_session.py runs inherit these defaults, so the user only specifies what differs (e.g. persona and session position). Per-run flags always override the saved config.
Step 1 — Gather the generation spec
Honour the setup-mode defaults (Step 0); only ask for parameters the user hasn't already fixed.
Collect (or infer sensible defaults for) these parameters. Ask only for what materially changes the output; default the rest.
- Use case: eval / demo / training (drives whether ground-truth labels are emitted).
- Modality:
icf-grow,cbt,ifs, oract-mi. Seereferences/modalities.mdfor the
technique cheat-sheet, signature moves, and vocabulary of each.
- Persona: pick an existing persona from
references/personas.md, or generate a new one and
persist it back into that file so a session series stays consistent. A persona = name, demographics, presenting issue, history, speech register, defenses/resistances, goals.
- Session position: intake / early / mid-arc / breakthrough / rupture-and-repair / closing.
This sets emotional tone and what prior material is referenced.
- Format:
fathom,plain,json, ormarkdown(see Step 3). Markdown is always produced. - Language: defaults from setup config; pass
--language. Author all dialogue, persona
voice, and the watermark-adjacent text in that language; keep eval tag keys in English.
- Duration / length:
--duration <minutes>(preferred — maps to a turn budget) or the coarse
--length (short ~15 / standard ~30 / long ~50+).
Step 2 — Build the session skeleton, then write the dialogue
Run the scaffolding script to turn the spec into a structured skeleton (phases, beat list, turn budget, JSON shell, and the synthetic watermark):
python3 scripts/scaffold_session.py --modality cbt --persona maya --position mid-arc \
--length standard --format json --out /tmp/session_skeleton.jsonThen write the actual dialogue by hand (model-authored), filling each beat. The script provides structure and guardrails; Claude provides the natural, non-templated language. Key realism rules (full list in references/realism_guide.md):
- Open with logistics/check-in small talk; never jump straight to deep work.
- Give the client disfluencies, hedges, self-interruption, and at least one moment of resistance or
avoidance. Real clients don't deliver clean insights on cue.
- Keep the practitioner in-modality: CBT uses thought records and Socratic questioning; IFS uses
parts language and "How do you feel toward that part?"; GROW moves Goal→Reality→Options→Will; ACT/MI uses values, defusion, and change talk. Avoid mixing modalities unless depicting eclectic practice deliberately.
- Maintain persona voice: vocabulary, sentence length, and recurring metaphors stay stable.
- End with a summary, a between-session task/experiment, and scheduling.
Step 3 — Render formats (always include markdown)
Author once in the JSON turn structure, then convert. Always render the markdown format (it is the canonical, human-readable artifact); add any other formats the user asked for.
# markdown is always produced:
python3 scripts/convert_format.py --in /tmp/session.json --to markdown --auto-timestamps --out session.md
# plus any requested extras:
python3 scripts/convert_format.py --in /tmp/session.json --to fathom --auto-timestamps --out session.txt- markdown (always) — Obsidian note with YAML frontmatter (persona id, modality, session
position, synthetic flag) above the transcript.
- fathom — speaker-labeled, timestamped lines matching the Fathom/Granola export style, so the
transcript flows through existing skills (coaching-session-summarizer, transcript-analyzer).
- plain — simple
Coach:/Client:turn-taking markdown. - json — the source itself: turns with
speaker,timestamp,text, and eval tags
(technique, emotion, phase); for evals, also the ground_truth block.
Timestamps. Do not hand-invent timestamps. Pass --auto-timestamps so the converter emulates them from each turn's word count (~150 wpm + a short inter-turn gap), keeping timing internally consistent. Tune pace with --wpm. See assets/templates/ for a reference example of each format.
Step 4 — (Optional) Case-conceptualization card with portrait
When the user wants a card summarizing the case (for demos, persona bibles, or eval context), build it from the same session JSON and pair it with a generated portrait:
python3 scripts/make_card.py --in /tmp/session.json --out /tmp/card.md # scaffold
python3 scripts/make_card.py --in /tmp/session.json --print-prompt # portrait prompt1. Run make_card.py to emit the card scaffold (modality-aware formulation skeleton + themes/goals pulled from ground_truth + a watermark + a ready portrait prompt). 2. Fill the <!-- FILL: ... --> blocks with the clinical formulation (model-authored). 3. Generate the portrait with the `gpt-image-2` skill using the prompt from --print-prompt. Keep it illustrative, not photoreal — a stylized image cannot be mistaken for a photo of a real person. Then re-run with --image <path> (or edit the card) to embed it.
Step 4b — (Optional) Render the card as an HTML page via tufte-report
When the user wants a shareable HTML page of the case card (portrait + conceptualization), hand the filled card to the `tufte-report` skill, which produces a standalone Tufte-style HTML file.
1. Build and fill the card (Step 4), including the embedded portrait. 2. Invoke the tufte-report skill with the card's conceptualization as the narrative content and the portrait as a figure. Map card sections to the report: Snapshot/Presenting issue → intro narrative; Formulation → the main 2-column narrative+data section; Working themes and Goals & experiments → a status/dashboard panel; Emotional arc → a sparkline or labelled sequence. Pass the portrait path so it renders as the hero figure. 3. Keep the synthetic watermark visible in the HTML (header or footer), and confirm the output path (default: current working directory) before writing the .html.
The portrait must remain the illustrative, non-photoreal image from Step 4 — the HTML page is for demos and persona bibles, never presented as a real client record.
Step 5 — Watermark and save
Always apply the synthetic watermark — this is non-negotiable. The scaffold script injects it; verify it survived format conversion. Each output must carry, in a location appropriate to its format (frontmatter, JSON metadata, or a header/footer comment):
⚠️ SYNTHETIC — AI-generated fictional session. Not a real person, not clinical advice.Confirm the save location before writing. Ask the user where to save and state the default — the current working directory (.). Only fall back to /tmp/ for throwaway intermediate scaffolds the user will not keep. Use clear filenames (e.g. <persona>_<modality>_<position>.md). For eval batches, write one file per session into the chosen directory plus a manifest listing personas, modalities, and label coverage.
Limitations and Constraints
- Synthetic only. Never present output as a real session, real person, or clinical record. The
watermark is mandatory and must never be stripped, even for demos (use the optional clean-body variant only when the user explicitly confirms, and keep provenance in metadata).
- Not clinical guidance. Generated dialogue is illustrative fiction; it must not be used as a
source of therapeutic technique, diagnosis, or advice for real care. Do not reproduce real protocols verbatim or imply clinical validity.
- No real PII. Do not base personas on identifiable real individuals or copy details from real
transcripts. If given a real transcript as a style reference, abstract patterns only — never names, specifics, or verbatim content (route true anonymization to session-anonymizer).
- Portraits stay illustrative. Generate card portraits as stylized illustrations, never
photorealistic faces — a synthetic illustration cannot be mistaken for a photo of a real person. The card carries its own synthetic watermark; keep it.
- Safety-sensitive content. Crisis, self-harm, abuse, or risk scenarios may be depicted only when
the use case clearly warrants it (e.g. red-team evals), must stay clearly fictional and watermarked, and must depict responsible practitioner handling — never operational harmful detail.
- Stay in scope. This skill generates; it does not analyze real sessions. Hand real-transcript
summarization to coaching-session-summarizer and anonymization to session-anonymizer.
# ⚠️ SYNTHETIC — AI-generated fictional session. Not a real person, not clinical advice.
[00:00] Therapist: Hey Maya, good to see you. How's the connection — can you hear me okay?
[00:08] Client: Yeah, fine. Sorry, just — give me a sec, closing Slack. Okay. I'm here.
[00:20] Therapist: No rush. Last week you were going to try handing the launch checklist to Sam. How'd that land?
[00:34] Client: I mean… I drafted the handoff doc. Then I sort of rewrote his part. It's probably fine, but I figured it'd be faster if I just— yeah.
[00:52] Therapist: So the thought was 'it'll be faster if I do it myself.' What's the evidence for that?
[01:05] Client: Honestly? It wasn't faster. I was up till one. I just— I couldn't stand the idea of it going out with a typo and it being on me.
[01:22] Therapist: That sounds like the 'if it's not perfect it's on me' belief again. Want to put it through a thought record this week?
[01:34] Client: ...Yeah. Okay. I can do that.
⚠️ SYNTHETIC — AI-generated fictional session. Not a real person, not clinical advice.
Session — maya (cbt)
Therapist: 00:00 Hey Maya, good to see you. How's the connection — can you hear me okay?
Client: 00:08 Yeah, fine. Sorry, just — give me a sec, closing Slack. Okay. I'm here.
Therapist: 00:20 No rush. Last week you were going to try handing the launch checklist to Sam. How'd that land?
Client: 00:34 I mean… I drafted the handoff doc. Then I sort of rewrote his part. It's probably fine, but I figured it'd be faster if I just— yeah.
Therapist: 00:52 So the thought was 'it'll be faster if I do it myself.' What's the evidence for that?
Client: 01:05 Honestly? It wasn't faster. I was up till one. I just— I couldn't stand the idea of it going out with a typo and it being on me.
Therapist: 01:22 That sounds like the 'if it's not perfect it's on me' belief again. Want to put it through a thought record this week?
Client: 01:34 ...Yeah. Okay. I can do that.
<!-- ⚠️ SYNTHETIC — AI-generated fictional session. Not a real person, not clinical advice. -->
Therapist: Hey Maya, good to see you. How's the connection — can you hear me okay?
Client: Yeah, fine. Sorry, just — give me a sec, closing Slack. Okay. I'm here.
Therapist: No rush. Last week you were going to try handing the launch checklist to Sam. How'd that land?
Client: I mean… I drafted the handoff doc. Then I sort of rewrote his part. It's probably fine, but I figured it'd be faster if I just— yeah.
Therapist: So the thought was 'it'll be faster if I do it myself.' What's the evidence for that?
Client: Honestly? It wasn't faster. I was up till one. I just— I couldn't stand the idea of it going out with a typo and it being on me.
Therapist: That sounds like the 'if it's not perfect it's on me' belief again. Want to put it through a thought record this week?
Client: ...Yeah. Okay. I can do that.
{
"watermark": "⚠️ SYNTHETIC — AI-generated fictional session. Not a real person, not clinical advice.",
"synthetic": true,
"not_clinical_advice": true,
"spec": {
"modality": "cbt",
"practitioner_label": "Therapist",
"client_label": "Client",
"persona": "maya",
"session_position": "mid-arc",
"length": "short",
"format": "json",
"turn_budget": 15
},
"beats": [
{
"phase": "check-in",
"target_turns": 2,
"notes": ""
},
{
"phase": "agenda",
"target_turns": 3,
"notes": ""
},
{
"phase": "homework-review",
"target_turns": 3,
"notes": ""
},
{
"phase": "main-work",
"target_turns": 3,
"notes": ""
},
{
"phase": "new-homework",
"target_turns": 3,
"notes": ""
},
{
"phase": "summary",
"target_turns": 2,
"notes": ""
}
],
"turns": [
{
"speaker": "Therapist",
"timestamp": "00:00",
"text": "Hey Maya, good to see you. How's the connection — can you hear me okay?",
"technique": "check-in",
"emotion": "neutral",
"phase": "check-in"
},
{
"speaker": "Client",
"timestamp": "00:08",
"text": "Yeah, fine. Sorry, just — give me a sec, closing Slack. Okay. I'm here.",
"technique": null,
"emotion": "distracted",
"phase": "check-in"
},
{
"speaker": "Therapist",
"timestamp": "00:20",
"text": "No rush. Last week you were going to try handing the launch checklist to Sam. How'd that land?",
"technique": "homework-review",
"emotion": "warm",
"phase": "homework-review"
},
{
"speaker": "Client",
"timestamp": "00:34",
"text": "I mean… I drafted the handoff doc. Then I sort of rewrote his part. It's probably fine, but I figured it'd be faster if I just— yeah.",
"technique": null,
"emotion": "defensive",
"phase": "homework-review"
},
{
"speaker": "Therapist",
"timestamp": "00:52",
"text": "So the thought was 'it'll be faster if I do it myself.' What's the evidence for that?",
"technique": "socratic-questioning",
"emotion": "curious",
"phase": "main-work"
},
{
"speaker": "Client",
"timestamp": "01:05",
"text": "Honestly? It wasn't faster. I was up till one. I just— I couldn't stand the idea of it going out with a typo and it being on me.",
"technique": null,
"emotion": "anxious",
"phase": "main-work"
},
{
"speaker": "Therapist",
"timestamp": "01:22",
"text": "That sounds like the 'if it's not perfect it's on me' belief again. Want to put it through a thought record this week?",
"technique": "homework-assignment",
"emotion": "collaborative",
"phase": "new-homework"
},
{
"speaker": "Client",
"timestamp": "01:34",
"text": "...Yeah. Okay. I can do that.",
"technique": null,
"emotion": "reluctant-agreement",
"phase": "new-homework"
}
],
"ground_truth": {
"themes": [
"perfectionism",
"delegation"
],
"action_items": [
"complete one thought record on the 'it's on me' belief"
],
"techniques_used": [
"socratic-questioning",
"homework-assignment"
],
"emotional_arc": "distracted -> defensive -> reluctant openness"
}
}Synthetic Session Generator
Generate realistic, persona-consistent synthetic coaching & therapy session transcripts for evals, demos, and training data — fictional but believable, and always watermarked as synthetic.
Why
Real session transcripts are sensitive and scarce. This skill produces convincing stand-ins for three jobs:
- Eval datasets — labelled transcripts (with a
ground_truthblock) to benchmark summarizers and analyzers - Product demos — realistic sessions without exposing any real client data
- Training / prompt examples — few-shot material for a coaching or therapy assistant
Realism rests on two disciplines: persona consistency (the client speaks the same way and carries the same history across a session arc) and modality fidelity (the practitioner uses the techniques and pacing of the chosen framework).
Features
- 🧩 4 modalities — ICF/GROW coaching, CBT, IFS parts-work, ACT/Motivational Interviewing, each with a technique cheat-sheet
- 👤 Persona bible — reusable personas (Maya, Diego, Priya, …) so a session series stays in voice; invent-and-persist new ones
- 📄 4 output formats — Fathom/Granola style, plain dialogue, structured JSON (with eval tags), Obsidian markdown — markdown always rendered
- ⏱️ Timestamp emulation — timing computed from turn length (~150 wpm), never hand-faked
- 🌍 Setup mode — persist defaults for language (8 languages), modality, and session duration
- 🖼️ Case-conceptualization card — modality-aware formulation + an illustrative portrait via the
gpt-image-2skill - 📊 HTML case page — render the card (portrait + conceptualization) as a Tufte-style page via
tufte-report - ⚠️ Always watermarked — every artifact is marked synthetic; never presentable as a real clinical record
- ✅ Hardened — validated across 8 Codex audit rounds (malformed input, encoding, pipe handling, etc.);
VERDICT: ALL CLEAR
Architecture
synthetic-session-generator/
├── SKILL.md # setup → spec → scaffold → author → render → card → HTML → save
├── references/
│ ├── modalities.md # ICF/GROW, CBT, IFS, ACT-MI cheat-sheets + eval labels
│ ├── personas.md # persona bible + template
│ └── realism_guide.md # disfluency, resistance, arc-by-position, anti-patterns
└── scripts/
├── _common.py # shared watermark, config, validation, timestamps, frontmatter
├── setup_config.py # setup mode (language / modality / duration)
├── scaffold_session.py # spec → JSON skeleton (phases, beats, turn budget)
├── convert_format.py # JSON → fathom / plain / markdown / json (+ --auto-timestamps)
└── make_card.py # session JSON → case-conceptualization card + portrait promptQuick start
# Copy to skills directory
cp -r synthetic-session-generator ~/.claude/skills/
# Configure defaults (optional)
/synthetic-session-generator setup # choose language / modality / duration
# Generate a session
/synthetic-session-generator # asks for persona, modality, position, format
# Under the hood
python3 scripts/scaffold_session.py --modality ifs --persona maya --position mid-arc --out s.json
# ...author the dialogue into s.json's "turns"...
python3 scripts/convert_format.py --in s.json --to markdown --auto-timestamps --out session.md
python3 scripts/make_card.py --in s.json --out card.mdSafety & scope
Output is fictional illustration, not clinical advice. Personas are composites, never real people; portraits are stylized illustrations, never photoreal. The skill generates — it does not analyze real sessions (that's coaching-session-summarizer) or anonymize real data (that's session-anonymizer).
Modality Cheat-Sheets
Use these to keep the practitioner's voice and technique faithful to the chosen framework. Each section lists signature moves, characteristic question forms, vocabulary, pacing, and the ground-truth labels worth emitting for evals.
---
icf-grow — Professional Coaching (ICF + GROW)
- Stance: non-directive, client-as-expert, future/action focused. Coach asks more than tells.
- GROW arc: Goal (what do you want from this session/topic) → Reality (what's true now) →
Options (what could you do) → Will (what will you commit to).
- Signature moves: powerful open questions, reflecting back, holding silence, asking permission
("Can I offer an observation?"), accountability check on prior commitments.
- Question forms: "What would success look like?" · "What's getting in the way?" · "What else?"
(asked repeatedly to widen options) · "On a scale of 1–10, how committed are you?"
- Avoid: diagnosing, giving advice, deep childhood/trauma exploration (that drifts to therapy).
- Eval labels:
grow_phase,powerful_question,commitment/action_item,accountability_check.
cbt — Cognitive Behavioral Therapy
- Stance: collaborative, structured, present-focused, psychoeducational. Agenda-setting at the top.
- Core tools: thought records (situation → automatic thought → emotion → evidence for/against →
balanced thought), Socratic questioning, identifying cognitive distortions, behavioral experiments, homework review.
- Cognitive distortions to surface/label: catastrophizing, mind-reading, all-or-nothing,
overgeneralization, "should" statements, emotional reasoning, discounting the positive.
- Question forms: "What went through your mind right then?" · "What's the evidence for that
thought?" · "If a friend said this, what would you tell them?" · "How could we test that belief?"
- Pacing: check-in → agenda → homework review → main work → new homework → summary.
- Eval labels:
cognitive_distortion,automatic_thought,reframe,homework_assigned.
ifs — Internal Family Systems (Parts Work)
- Stance: gentle, curious, non-pathologizing; every part has positive intent. Therapist helps the
client lead from Self (calm, curious, compassionate).
- Core concepts: parts (managers, firefighters, exiles), Self-energy, unblending, the 8 C's
(calm, curiosity, compassion, clarity, courage, confidence, creativity, connectedness).
- Signature moves: "Let's get to know that part." · asking the client to turn toward a part ·
the unblending question "How do you feel toward that part right now?" (if not open/curious, another part is blended) · asking permission of protectors before approaching exiles.
- Question forms: "Where do you notice it in your body?" · "What does that part want you to
know?" · "How old does it feel?" · "What is it afraid would happen if it stopped?"
- Pacing: slow, somatic, lots of silence; one part at a time. Never rush an exile.
- Eval labels:
part_identified(manager/firefighter/exile),unblending,self_energy,
protector_permission.
act-mi — Acceptance & Commitment / Motivational Interviewing
- Stance: values-driven, accepting of inner experience, change-talk-evoking, non-confrontational.
- ACT tools: cognitive defusion ("I'm having the thought that…"), acceptance/willingness, values
clarification, committed action, present-moment contact, self-as-context.
- MI tools: OARS (Open questions, Affirmations, Reflections, Summaries), evoking and reinforcing
change talk (DARN-C: Desire, Ability, Reasons, Need, Commitment), rolling with resistance, the readiness ruler.
- Question forms: "What matters to you here, underneath the worry?" · "If this thought weren't in
the way, what would you do?" · "What makes you want to change this now?" · "Why a 6 and not a 3?"
- Avoid: arguing for change (evokes the client's own change talk instead), the "righting reflex".
- Eval labels:
change_talk(DARN-C),value_named,defusion,reflection,affirmation.
---
Mixing modalities
Default to a single modality per session for clean evals. Depict integrative/eclectic practice only when the user asks — and if so, label which technique each move belongs to so eval ground truth stays unambiguous.
Persona Library (Persona Bible)
Reusable fictional clients. Pick one for consistency across a session series, or append a new one (use the template at the bottom) so future sessions in the same arc stay in voice. All personas are fictional composites — never identifiable real people.
Persona ids are lowercase, used by scaffold_session.py --persona <id>.
---
maya
- Demographics: 34, product manager, urban, married, no kids.
- Presenting issue: chronic overwork and perfectionism; can't delegate; resentment building.
- History: eldest daughter of immigrant parents, "the responsible one"; tied worth to achievement.
- Speech register: articulate, fast, fond of work metaphors ("sprint", "bandwidth"); intellectualizes
feelings; hedges with "I mean…" and "it's probably fine, but…".
- Defenses / resistance: deflects to logistics, minimizes ("not a big deal"), competence as armor.
- Goals: set boundaries, delegate one project, feel okay resting.
- Best fits: cbt (perfectionism distortions), icf-grow (delegation goal), act-mi (values vs. striving).
diego
- Demographics: 41, recently divorced, two kids part-time, works in construction management.
- Presenting issue: anger and shutdown; conflict with ex over co-parenting; loneliness he won't name.
- History: father was strict and absent; learned anger is the only "allowed" feeling.
- Speech register: short sentences, concrete, guarded; sarcasm under stress; long pauses before vulnerable
material; says "whatever" / "it is what it is" when avoiding.
- Defenses / resistance: stoicism, topic changes, "I'm fine", challenges the process early.
- Goals: stop blowing up at the kids, talk to the ex without escalating.
- Best fits: ifs (anger as protector / lonely exile), act-mi (ambivalence, change talk).
priya
- Demographics: 27, grad student, anxious, first-generation in her field.
- Presenting issue: social anxiety and imposter syndrome; avoidance of presentations.
- History: praised only for grades; high-criticism household; safety in invisibility.
- Speech register: soft, apologetic, lots of qualifiers ("sorry", "maybe", "this is silly but…"); upward
inflection; quick to agree then quietly disagree.
- Defenses / resistance: people-pleasing, self-deprecation, agreeing to avoid friction.
- Goals: give one talk without a panic attack, ask for what she needs.
- Best fits: cbt (catastrophizing, mind-reading), ifs (anxious manager protecting a shamed exile).
---
Template for new personas
## <id>
- **Demographics**: age, role, living situation.
- **Presenting issue**: what brings them in; surface vs. underlying.
- **History**: formative context shaping the issue (no real PII).
- **Speech register**: vocabulary, sentence length, tics, recurring metaphors, filler words.
- **Defenses / resistance**: how they avoid / deflect under pressure.
- **Goals**: explicit session/arc goals.
- **Best fits**: which modalities suit this persona.Realism Guide
What separates a believable transcript from an obvious AI-generated one. Apply when authoring the dialogue in Step 2.
Make it sound recorded, not scripted
- Disfluency: "um", "like", false starts, trailing "…", self-correction ("I was — well, I wasn't
angry exactly"). Sparingly, not every line.
- Overlap & backchannel: short "mm-hm", "right", "yeah" from the listener; occasional interruption.
- Imperfect insight: clients rarely produce the clean realization on the first ask. They circle,
resist, change the subject, then arrive — or don't, this session.
- Mundane texture: a barking dog, a dropped connection on a video call, "sorry, can you hear me?",
running two minutes over.
Persona consistency checklist
- Vocabulary and metaphors stay stable across turns and across sessions in the arc.
- Defenses fire under pressure (deflection, minimization, sarcasm — per the persona).
- Emotional range matches history; growth is incremental, not magical.
- References to prior sessions match the chosen session position.
Modality fidelity checklist
- Practitioner's questions match the framework (see
modalities.md). - The session shape matches the framework's arc (e.g. CBT agenda→homework→work→homework).
- No accidental modality bleed (a CBT therapist suddenly doing IFS parts work) unless intended.
- Practitioner is competent but human: occasional reflection that slightly misses, then repairs.
Arc by session position
- intake: history-gathering, goal-setting, building rapport, lots of open questions, no deep work yet.
- early: establishing the working model, psychoeducation, first small experiments.
- mid-arc: references prior homework/parts, deeper work, possible resistance.
- breakthrough: an emotional shift earned over prior sessions — set up, don't hand it over free.
- rupture-and-repair: misattunement, client withdrawal or pushback, practitioner repairs.
- closing: review of progress, consolidation, relapse-prevention, ending feelings.
Length targets
- short ≈ 12–18 turns, standard ≈ 24–36, long ≈ 45–60. A "turn" is one speaker block.
Anti-patterns to avoid
- Therapist monologues / over-explaining. Keep practitioner turns shorter than client turns on average.
- Every client statement being a tidy, labeled feeling.
- Robotic perfect turn-taking with no overlap or tangents.
- Wikipedia-style technique exposition inside the dialogue.
- Resolution that's too fast or too complete for the session position.
#!/usr/bin/env python3
"""Shared helpers for the synthetic-session-generator scripts.
Centralizes the mandatory watermark, config loading/validation, turn allocation, timestamp
emulation, and YAML-safe frontmatter so the four CLI scripts stay consistent.
"""
import argparse
import json
import os
import sys
# --- Single source of truth for the mandatory watermark (exact-match downstream checks rely on it).
WATERMARK = ("⚠️ SYNTHETIC — AI-generated fictional session. "
"Not a real person, not clinical advice.")
LANGUAGES = {
"en": "English", "ru": "Russian", "de": "German", "es": "Spanish",
"fr": "French", "pt": "Portuguese", "it": "Italian", "nl": "Dutch",
}
MODALITIES = ["icf-grow", "cbt", "ifs", "act-mi"]
DEFAULTS = {"language": "en", "modality": "cbt", "duration_minutes": 50}
CONFIG_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")
# --- argparse validators -----------------------------------------------------
def positive_int(value):
try:
ivalue = int(value)
except (TypeError, ValueError):
raise argparse.ArgumentTypeError(f"expected a positive integer, got {value!r}")
if ivalue <= 0:
raise argparse.ArgumentTypeError(f"must be a positive integer, got {ivalue}")
return ivalue
# --- config ------------------------------------------------------------------
def load_config():
"""Return validated defaults merged with config.json. Invalid values fall back to DEFAULTS."""
cfg = dict(DEFAULTS)
if os.path.exists(CONFIG_PATH):
try:
with open(CONFIG_PATH, encoding="utf-8") as fh:
raw = json.load(fh)
except (OSError, ValueError): # ValueError covers JSON decode, bad UTF-8, oversized ints
return cfg
if isinstance(raw, dict):
# isinstance(str) guards both type and hashability (a list value would raise TypeError).
if isinstance(raw.get("language"), str) and raw["language"] in LANGUAGES:
cfg["language"] = raw["language"]
if isinstance(raw.get("modality"), str) and raw["modality"] in MODALITIES:
cfg["modality"] = raw["modality"]
dur = raw.get("duration_minutes")
# bool is an int subclass — exclude it so `true` is not accepted as a duration.
if isinstance(dur, int) and not isinstance(dur, bool) and dur > 0:
cfg["duration_minutes"] = dur
return cfg
def run_cli(main):
"""Run a CLI main(), swallowing BrokenPipeError when a downstream consumer closes the pipe."""
try:
main()
# Flush inside the guard: buffered stdout to a closed pipe otherwise raises at shutdown (exit 120).
sys.stdout.flush()
except BrokenPipeError:
# Redirect stdout to devnull so the interpreter's shutdown flush doesn't re-raise.
devnull = os.open(os.devnull, os.O_WRONLY)
os.dup2(devnull, sys.stdout.fileno())
sys.exit(0)
except KeyboardInterrupt:
sys.exit(130)
def write_text(path, text):
"""Write text, exiting cleanly (code 2) on any filesystem error instead of a traceback."""
try:
with open(path, "w", encoding="utf-8") as fh:
fh.write(text)
except OSError as e:
print(f"ERROR: could not write {path} ({e})", file=sys.stderr)
sys.exit(2)
def save_config(cfg):
write_text(CONFIG_PATH, json.dumps(cfg, ensure_ascii=False, indent=2))
def duration_to_length(mins):
"""Map clock minutes to a turn budget (~0.6 turns/min, clamped 8–80) and a length bucket."""
# Clamp the input first so absurdly large ints can't overflow float conversion in `mins * 0.6`.
mins = max(1, min(int(mins), 100_000))
turns = max(8, min(80, round(mins * 0.6)))
bucket = "short" if turns <= 20 else "standard" if turns <= 38 else "long"
return turns, bucket
def allocate_turns(weights, budget):
"""Distribute `budget` turns across phases by `weights`, summing exactly to budget.
Uses the largest-remainder method; guarantees at least 1 per phase when budget >= len(weights).
"""
n = len(weights)
total = sum(weights) or n
raw = [budget * w / total for w in weights]
floor = [int(x) for x in raw]
remainder = budget - sum(floor)
order = sorted(range(n), key=lambda i: raw[i] - floor[i], reverse=True)
for k in range(max(0, remainder)):
floor[order[k % n]] += 1
# Ensure no phase is 0 when we can afford it, by borrowing from the largest.
if budget >= n:
for i in range(n):
if floor[i] == 0:
donor = max(range(n), key=lambda j: floor[j])
if floor[donor] > 1:
floor[donor] -= 1
floor[i] += 1
return floor
# --- session JSON validation -------------------------------------------------
class SessionError(ValueError):
pass
def validate_session(data):
"""Validate and normalize a session dict in place. Returns the spec and turns."""
if not isinstance(data, dict):
raise SessionError("session JSON root must be an object")
spec = data.get("spec")
if spec is None:
spec = {}
if not isinstance(spec, dict):
raise SessionError("'spec' must be an object")
turns = data.get("turns")
if turns is None:
turns = []
if not isinstance(turns, list):
raise SessionError("'turns' must be a list")
norm = []
for i, t in enumerate(turns):
if not isinstance(t, dict):
raise SessionError(f"turn {i} must be an object")
t["speaker"] = "" if t.get("speaker") is None else str(t.get("speaker"))
t["text"] = "" if t.get("text") is None else str(t.get("text"))
if t.get("timestamp") is not None:
t["timestamp"] = str(t["timestamp"])
norm.append(t)
data["spec"] = spec
data["turns"] = norm
return spec, norm
def as_str_list(value):
"""Coerce a ground_truth field to a list of strings (a bare string becomes a 1-item list)."""
if value is None:
return []
if isinstance(value, str):
return [value]
if isinstance(value, list):
return [str(v) for v in value]
return [str(value)]
# --- timestamps --------------------------------------------------------------
def fmt_ts(seconds):
seconds = int(round(max(0, seconds)))
h, rem = divmod(seconds, 3600)
m, s = divmod(rem, 60)
return f"{h}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}"
def emulate_timestamps(turns, wpm=150, gap=1.5):
"""Compute internally consistent timestamps from turn length (overwrites authored values)."""
wps = max(0.1, wpm / 60.0)
t = 0.0
for turn in turns:
turn["timestamp"] = fmt_ts(t)
words = max(1, len(str(turn.get("text", "")).split()))
t += words / wps + max(0.0, gap)
return turns
# --- YAML-safe frontmatter ---------------------------------------------------
def yaml_scalar(value):
"""Render a scalar safely for YAML frontmatter (JSON strings are valid YAML)."""
if isinstance(value, bool):
return "true" if value else "false"
if value is None:
return "null"
if isinstance(value, (int, float)):
return str(value)
return json.dumps(str(value), ensure_ascii=False)
def frontmatter(pairs):
"""Build a YAML frontmatter block from an iterable of (key, value)."""
lines = ["---"]
for k, v in pairs:
lines.append(f"{k}: {yaml_scalar(v)}")
lines.append("---")
return "\n".join(lines)
#!/usr/bin/env python3
"""Convert an authored session JSON into a target transcript format.
Reads a session JSON (the scaffold with its `turns` array filled in by the model) and renders it to
`fathom`, `plain`, `markdown`, or `json` (pass-through). The synthetic watermark is re-emitted in
every target so it can never be dropped during conversion.
Usage:
python3 convert_format.py --in /tmp/session.json --to markdown --auto-timestamps
python3 convert_format.py --in /tmp/session.json --to fathom --out /tmp/session.txt
"""
import argparse
import json
import sys
from _common import (WATERMARK, validate_session, emulate_timestamps, frontmatter,
positive_int, SessionError, write_text, run_cli)
def load(path):
try:
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
except FileNotFoundError:
raise SessionError(f"file not found: {path}")
except (OSError, ValueError) as e: # ValueError: JSON decode / bad UTF-8 / oversized ints
raise SessionError(f"could not parse JSON ({e})")
spec, turns = validate_session(data) # raises SessionError on bad shape
if not turns:
print("WARNING: 'turns' is empty — rendering a watermark-only document.", file=sys.stderr)
return data
def _label(turn, spec):
sp = (turn.get("speaker") or "").strip()
low = sp.lower()
if low in ("practitioner", "coach", "therapist"):
return spec.get("practitioner_label", "Practitioner")
if low == "client":
return spec.get("client_label", "Client")
return sp or "Speaker"
def to_fathom(data):
spec = data["spec"]
lines = [f"# {WATERMARK}", ""]
for t in data["turns"]:
ts = t.get("timestamp") or "00:00"
lines.append(f"[{ts}] {_label(t, spec)}: {t.get('text','')}")
return "\n".join(lines) + "\n"
def to_plain(data):
spec = data["spec"]
lines = [f"<!-- {WATERMARK} -->", ""]
for t in data["turns"]:
lines.append(f"{_label(t, spec)}: {t.get('text','')}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def to_markdown(data):
spec = data["spec"]
out = [
frontmatter([
("persona", spec.get("persona", "")),
("modality", spec.get("modality", "")),
("session_position", spec.get("session_position", "")),
("language", spec.get("language", "")),
("synthetic", True),
("not_clinical_advice", True),
]),
"",
f"> {WATERMARK}",
"",
f"# Session — {spec.get('persona','')} ({spec.get('modality','')})",
"",
]
for t in data["turns"]:
ts = t.get("timestamp")
prefix = f"`{ts}` " if ts else ""
out.append(f"**{_label(t, spec)}:** {prefix}{t.get('text','')}")
out.append("")
return "\n".join(out).rstrip() + "\n"
def to_json(data):
# Pass-through: emit the (validated, possibly re-timestamped) session JSON.
# Enforce the mandatory watermark/flags so the JSON target can never lose provenance.
data["watermark"] = WATERMARK
data["synthetic"] = True
data["not_clinical_advice"] = True
return json.dumps(data, ensure_ascii=False, indent=2) + "\n"
RENDERERS = {"fathom": to_fathom, "plain": to_plain, "markdown": to_markdown, "json": to_json}
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--in", dest="inp", required=True, help="authored session JSON")
p.add_argument("--to", required=True, choices=sorted(RENDERERS), help="target format")
p.add_argument("--out", help="output path (default: stdout)")
p.add_argument("--auto-timestamps", action="store_true",
help="recompute timestamps from turn length instead of using authored ones")
p.add_argument("--wpm", type=positive_int, default=150, help="speaking rate for --auto-timestamps")
args = p.parse_args()
try:
data = load(args.inp)
except SessionError as e:
print(f"ERROR: invalid session JSON — {e}", file=sys.stderr)
sys.exit(2)
if args.auto_timestamps:
emulate_timestamps(data["turns"], wpm=args.wpm)
rendered = RENDERERS[args.to](data)
if args.out:
write_text(args.out, rendered)
print(f"Wrote {args.to}: {args.out}", file=sys.stderr)
else:
sys.stdout.write(rendered)
if __name__ == "__main__":
run_cli(main)
#!/usr/bin/env python3
"""Build a case-conceptualization card from an authored session JSON.
Emits a markdown card scaffold that summarizes the (synthetic) client: a snapshot, the
modality-appropriate formulation skeleton, themes and goals pulled from the session's `ground_truth`,
and a slot for a portrait image. The clinical narrative (the `<!-- FILL: ... -->` blocks) is authored
by the model; this script supplies structure, watermark, and a ready-to-use portrait prompt for the
`gpt-image-2` skill.
Usage:
python3 make_card.py --in /tmp/maya_ifs.json --out /tmp/maya_card.md
python3 make_card.py --in /tmp/maya_ifs.json --image assets/maya_portrait.png --out card.md
python3 make_card.py --in /tmp/maya_ifs.json --print-prompt
"""
import argparse
import json
import sys
from _common import (WATERMARK, validate_session, as_str_list, frontmatter, SessionError,
write_text, run_cli)
# Card-specific provenance line, shown in addition to the mandatory base watermark.
CARD_NOTE = "Case card for a fictional persona — not a real clinical record."
FORMULATIONS = {
"cbt": [
("Core belief / schema", "the central 'I am…' belief driving the pattern"),
("Key automatic thoughts", "recurring hot thoughts and the situations that trigger them"),
("Cognitive distortions", "named distortions evidenced in the session"),
("Maintenance cycle", "thought → emotion → behaviour → consequence loop"),
],
"ifs": [
("Parts map", "managers / firefighters / exiles identified, and their roles"),
("Protective intent", "what each protector is guarding against"),
("Burdens", "the beliefs/feelings the exile carries and where they came from"),
("Self-energy & access", "how much Self was available; what unblended access"),
],
"icf-grow": [
("Goal", "what the client wants, in their words"),
("Current reality", "the honest present-state picture"),
("Options & resources", "paths considered and strengths in play"),
("Will / commitment", "what they committed to and their readiness level"),
],
"act-mi": [
("Values", "what matters to the client underneath the problem"),
("Ambivalence", "the change-vs-status-quo tension and sustain talk"),
("Fusion / avoidance", "thoughts they are fused with; experiences avoided"),
("Change talk & commitment", "DARN-C evidence and committed action"),
],
}
def portrait_prompt(persona, modality):
"""A safe, clearly-synthetic, courtroom-sketch line-art prompt for gpt-image-2.
Deliberately loose and gestural — a reportage sketch reads unmistakably as hand-drawn, never a
photo of a real person, and captures mood/posture/environment over literal likeness.
"""
persona = str(persona) if persona not in (None, "") else "client"
modality = modality if isinstance(modality, str) and modality else "coaching"
return (
f"Loose courtroom-sketch / reportage line art of a fictional therapy-client persona "
f"named '{persona}'. Quick gestural pen-and-ink linework with a few light marker or "
f"colored-pencil washes, expressive and unfinished, sketched on the go. Capture mood, "
f"posture, and the session environment of a {modality.upper()} session rather than a "
f"literal likeness. Clearly a hand-drawn sketch — not a photograph, not a polished "
f"portrait. No text, no logos. Synthetic/illustrative character — not a real individual."
)
def build_card(data, image_path=None):
spec, _ = validate_session(data)
gt = data.get("ground_truth")
if not isinstance(gt, dict):
gt = {}
# Coerce to scalars: malformed JSON may carry list/dict here, which would break .get()/f-strings.
persona = spec.get("persona")
persona = str(persona) if persona not in (None, "") else "client"
modality = spec.get("modality")
modality = modality if isinstance(modality, str) else ""
formulation = FORMULATIONS.get(modality, FORMULATIONS["cbt"])
L = [
frontmatter([
("persona", persona),
("modality", modality),
("session_position", spec.get("session_position", "")),
("type", "case-conceptualization-card"),
("synthetic", True),
("not_clinical_advice", True),
]),
"",
f"> {WATERMARK}",
f"> {CARD_NOTE}",
"",
f"# Case Card — {persona} ({modality})",
"",
]
if image_path:
L += [f"", ""]
else:
L += [
"<!-- PORTRAIT: generate with the gpt-image-2 skill, then embed the path here. -->",
f"<!-- Suggested prompt:\n{portrait_prompt(persona, modality)}\n-->",
"",
]
L += ["## Snapshot", "<!-- FILL: one-paragraph who-they-are, in clinical-summary voice -->", ""]
L += ["## Presenting issue", "<!-- FILL: surface complaint and what's underneath -->", ""]
L += [f"## Formulation ({modality or 'general'})"]
for heading, hint in formulation:
L += [f"### {heading}", f"<!-- FILL: {hint} -->", ""]
themes = as_str_list(gt.get("themes"))
L += ["## Working themes"]
L += [f"- {t}" for t in themes] if themes else ["<!-- FILL: 2–4 themes -->"]
L += [""]
actions = as_str_list(gt.get("action_items"))
L += ["## Goals & between-session experiments"]
L += [f"- [ ] {a}" for a in actions] if actions else ["<!-- FILL: agreed experiments -->"]
L += [""]
arc = gt.get("emotional_arc")
arc = str(arc) if arc else ""
L += ["## Emotional arc", arc if arc else "<!-- FILL: arc across the session -->", ""]
return "\n".join(L).rstrip() + "\n"
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--in", dest="inp", required=True, help="authored session JSON")
p.add_argument("--image", help="path to an already-generated portrait to embed")
p.add_argument("--out", help="output path (default: stdout)")
p.add_argument("--print-prompt", action="store_true",
help="print only the gpt-image-2 portrait prompt and exit")
args = p.parse_args()
try:
with open(args.inp, encoding="utf-8") as fh:
data = json.load(fh)
except FileNotFoundError:
print(f"ERROR: file not found: {args.inp}", file=sys.stderr)
sys.exit(2)
except (OSError, ValueError) as e: # ValueError: JSON decode / bad UTF-8 / oversized ints
print(f"ERROR: could not parse JSON ({e})", file=sys.stderr)
sys.exit(2)
try:
if args.print_prompt:
spec, _ = validate_session(data)
print(portrait_prompt(spec.get("persona"), spec.get("modality")))
return
card = build_card(data, image_path=args.image)
except SessionError as e:
print(f"ERROR: invalid session JSON — {e}", file=sys.stderr)
sys.exit(2)
if args.out:
write_text(args.out, card)
print(f"Wrote card: {args.out}", file=sys.stderr)
else:
sys.stdout.write(card)
if __name__ == "__main__":
run_cli(main)
#!/usr/bin/env python3
"""Scaffold a synthetic coaching/therapy session skeleton.
Turns a generation spec (modality, persona, session position, length/duration, language, format)
into a structured JSON skeleton with phases, a beat list, a turn budget, an empty turn array for the
model to fill, and the mandatory synthetic watermark. The model authors the actual dialogue into the
`turns` array; this script only builds the guardrails.
Defaults for modality / language / duration come from setup_config.py (config.json). Precedence:
explicit CLI flag > config value > built-in default.
Usage:
python3 scaffold_session.py --modality cbt --persona maya --position mid-arc \
--length standard --format json --out /tmp/session_skeleton.json
"""
import argparse
import json
import sys
from _common import (WATERMARK, LANGUAGES, MODALITIES, load_config, duration_to_length,
allocate_turns, positive_int, write_text, run_cli)
MODALITY_META = {
"icf-grow": {"practitioner": "Coach",
"phases": ["check-in", "goal", "reality", "options", "will", "close"]},
"cbt": {"practitioner": "Therapist",
"phases": ["check-in", "agenda", "homework-review", "main-work", "new-homework", "summary"]},
"ifs": {"practitioner": "Therapist",
"phases": ["check-in", "find-target-part", "unblend", "befriend-part", "integration", "close"]},
"act-mi": {"practitioner": "Practitioner",
"phases": ["check-in", "evoke-values", "explore-ambivalence", "defusion", "commit", "summary"]},
}
POSITIONS = {"intake", "early", "mid-arc", "breakthrough", "rupture-and-repair", "closing"}
LENGTHS = {"short": 15, "standard": 30, "long": 50}
FORMATS = {"fathom", "plain", "json", "markdown"}
def resolve_turn_budget(length, duration, config):
"""Apply precedence: explicit --duration > explicit --length > config duration > default length."""
if duration is not None:
turns, bucket = duration_to_length(duration)
return turns, bucket, duration
if length is not None:
return LENGTHS[length], length, None
cfg_duration = config.get("duration_minutes")
if isinstance(cfg_duration, int) and cfg_duration > 0:
turns, bucket = duration_to_length(cfg_duration)
return turns, bucket, cfg_duration
return LENGTHS["standard"], "standard", None
def build_skeleton(modality, persona, position, length, duration, fmt, language, config):
m = MODALITY_META[modality]
phases = m["phases"]
turn_budget, bucket, duration_used = resolve_turn_budget(length, duration, config)
weights = [1] * len(phases)
for i in range(1, len(phases) - 1):
weights[i] = 2
targets = allocate_turns(weights, turn_budget)
beats = [{"phase": p, "target_turns": tt, "notes": ""} for p, tt in zip(phases, targets)]
return {
"watermark": WATERMARK,
"synthetic": True,
"not_clinical_advice": True,
"spec": {
"modality": modality,
"practitioner_label": m["practitioner"],
"client_label": "Client",
"persona": persona,
"session_position": position,
"length": bucket,
"format": fmt,
"turn_budget": turn_budget,
"language": language,
"language_name": LANGUAGES.get(language, language),
"duration_minutes": duration_used,
},
"beats": beats,
"turns": [], # model fills: [{speaker, timestamp, text, technique, emotion, phase}, ...]
"ground_truth": {
"themes": [],
"action_items": [],
"techniques_used": [],
"emotional_arc": "",
},
}
def main():
cfg = load_config()
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--modality", default=cfg["modality"], choices=sorted(MODALITIES),
help="default from setup config")
p.add_argument("--persona", required=True, help="persona id from references/personas.md")
p.add_argument("--position", required=True, choices=sorted(POSITIONS))
# length/duration default to None so we can detect whether the user passed them explicitly.
p.add_argument("--length", default=None, choices=sorted(LENGTHS),
help="coarse length; overridden by --duration")
p.add_argument("--duration", type=positive_int, default=None,
help="session minutes; overrides --length and config (e.g. 25/50/80)")
p.add_argument("--language", default=cfg["language"], choices=sorted(LANGUAGES),
help="output language (default from setup config)")
p.add_argument("--format", dest="fmt", default="json", choices=sorted(FORMATS))
p.add_argument("--out", help="output path (default: stdout)")
args = p.parse_args()
skeleton = build_skeleton(args.modality, args.persona, args.position, args.length,
args.duration, args.fmt, args.language, cfg)
payload = json.dumps(skeleton, ensure_ascii=False, indent=2)
sp = skeleton["spec"]
if args.out:
write_text(args.out, payload + "\n")
print(f"Wrote skeleton: {args.out}", file=sys.stderr)
print(f" modality={sp['modality']} persona={sp['persona']} position={sp['session_position']} "
f"language={sp['language_name']} length={sp['length']} (~{sp['turn_budget']} turns) "
f"format={args.fmt}", file=sys.stderr)
print(f"Author the dialogue in {sp['language_name']} into 'turns', then run convert_format.py.",
file=sys.stderr)
else:
print(payload)
if __name__ == "__main__":
run_cli(main)
#!/usr/bin/env python3
"""Setup mode — persist default generation settings for the skill.
Writes a `config.json` next to the skill so later `scaffold_session.py` runs inherit defaults for
language, modality, and session duration without re-specifying them. Run this when the user wants to
configure the skill ("setup", "set my defaults", "always use Russian / CBT / 50-minute sessions").
Usage:
python3 setup_config.py --language ru --modality cbt --duration 50
python3 setup_config.py --show # print current config (no writes)
python3 setup_config.py --reset # restore built-in defaults
"""
import argparse
import json
import sys
from _common import (LANGUAGES, MODALITIES, DEFAULTS, load_config, save_config,
duration_to_length, positive_int, run_cli)
def describe(cfg):
turns, bucket = duration_to_length(cfg["duration_minutes"])
return {
"language": f"{cfg['language']} ({LANGUAGES.get(cfg['language'], cfg['language'])})",
"modality": cfg["modality"],
"duration_minutes": cfg["duration_minutes"],
"derived_turn_budget": turns,
"derived_length": bucket,
}
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--language", choices=sorted(LANGUAGES), help="default output language")
p.add_argument("--modality", choices=MODALITIES, help="default modality")
p.add_argument("--duration", type=positive_int, help="default session minutes (e.g. 25/50/80)")
p.add_argument("--show", action="store_true", help="print current config without modifying it")
p.add_argument("--reset", action="store_true", help="restore built-in defaults")
args = p.parse_args()
# --show is read-only: print and exit, no writes (unless combined with changes/reset).
will_change = bool(args.language or args.modality or args.duration or args.reset)
if args.show and not will_change:
print(json.dumps(describe(load_config()), ensure_ascii=False, indent=2))
return
cfg = dict(DEFAULTS) if args.reset else load_config()
if args.language:
cfg["language"] = args.language
if args.modality:
cfg["modality"] = args.modality
if args.duration:
cfg["duration_minutes"] = args.duration
save_config(cfg)
print(f"Saved config → config.json", file=sys.stderr)
print(json.dumps(describe(cfg), ensure_ascii=False, indent=2))
if __name__ == "__main__":
run_cli(main)