Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ None of these is required. Each one changes what the workflow does when it is pr
|---|---|---|
| Jira (an Atlassian MCP server, plus any `JIRA_*` or `ATLASSIAN_*` variable) | Work is tracked on a ticket, and branches are named after it | No ticket; branches are named from a short description |
| Notion (a Notion MCP server, plus `NOTION_PAGE_URL`) | Design docs are published to Notion | Design docs stay as local files |
| ElevenLabs (`ELEVEN_LABS_API_KEY`) | Each architecture doc's walkthrough video is narrated | The video is silent, with the narration as subtitles |
| A macOS voice (`WALKTHROUGH_MAC_VOICE`) | The walkthrough video is narrated by that voice, when ElevenLabs isn't configured | With `ELEVEN_LABS_API_KEY` also unset, the video is silent, with the narration as subtitles |
| ElevenLabs (`ELEVEN_LABS_API_KEY`) | Each architecture doc's walkthrough video is narrated | Kokoro or a macOS voice narrates when selected or set, else the video is silent, with the narration as subtitles |
| Kokoro (`WALKTHROUGH_VOICE=kokoro`, needs `uv`) | Each architecture doc's walkthrough video is narrated locally, instead of ElevenLabs or a macOS voice even when they are configured | ElevenLabs narrates when configured, else a macOS voice, else the video is silent, with the narration as subtitles |
| A macOS voice (`WALKTHROUGH_MAC_VOICE`) | The walkthrough video is narrated by that voice: always with `WALKTHROUGH_VOICE=mac`, otherwise when `WALKTHROUGH_VOICE` is unset or empty and ElevenLabs isn't configured | `WALKTHROUGH_VOICE=mac` fails the render; otherwise ElevenLabs or Kokoro narrates when selected, else the video is silent, with the narration as subtitles |
| The board (`ai-workflow-app`) | Questions and approvals go to the board | The workflow asks in the terminal |
| CodeRabbit | Its review is part of the pull request loop | Only the agent review runs |

To use Kokoro, install [`uv`](https://docs.astral.sh/uv/) (`brew install uv` on macOS) and set `WALKTHROUGH_VOICE` to `kokoro` in the `env` block of a settings file (see [Environment variables](#environment-variables)). The first narrated render downloads Python 3.12, PyTorch and the Kokoro model, so it needs network access and takes longer than later renders, which reuse that download.

The board is a separate application and is not published. Every variable the plugin reads is listed
under [Environment variables](#environment-variables) below.

Expand Down Expand Up @@ -90,9 +93,11 @@ These are Claude Code's own settings. The workflow depends on what they do.
|---|---|
| `JIRA_*`, `ATLASSIAN_*` | Any variable whose name starts with either prefix and whose value is not blank turns Jira on. What the value says doesn't matter. Unset: no tickets. |
| `NOTION_PAGE_URL` | The Notion page design docs are published under. Unset: design docs stay as local files. |
| `ELEVEN_LABS_API_KEY` | ElevenLabs API key used to narrate an architecture doc's walkthrough video. Unset: a macOS voice narrates it if `WALKTHROUGH_MAC_VOICE` is set; otherwise the video is silent, with the narration as subtitles. |
| `ELEVEN_LABS_API_KEY` | ElevenLabs API key used to narrate an architecture doc's walkthrough video. A blank value counts as unset. Unset: with `WALKTHROUGH_VOICE` unset or empty a macOS voice narrates it if `WALKTHROUGH_MAC_VOICE` is set, otherwise the video is silent, with the narration as subtitles; with `kokoro` or `mac`, that voice narrates; with `elevenlabs`, the render fails. |
| `ELEVEN_LABS_VOICE_ID` | The ElevenLabs voice for that narration. Default: the voice the CLI pins. |
| `WALKTHROUGH_MAC_VOICE` | A macOS voice, named as `say -v '?'` lists it, that narrates the walkthrough video when `ELEVEN_LABS_API_KEY` is unset. Unset (and `ELEVEN_LABS_API_KEY` unset too): the video is silent, with the narration as subtitles. Premium voices (System Settings → Accessibility → Read & Speak → System voice → Manage Voices…; Spoken Content before macOS 26) sound far better than the built-in ones. |
| `WALKTHROUGH_VOICE` | The voice that narrates walkthrough videos. `kokoro` narrates locally with Kokoro through `uv`. `elevenlabs` narrates with ElevenLabs and fails the render when `ELEVEN_LABS_API_KEY` is not set. `mac` narrates with a macOS voice and fails the render when `WALKTHROUGH_MAC_VOICE` is not set. Any other non-empty value fails the render. Unset or empty: ElevenLabs when its key is set, else a macOS voice when `WALKTHROUGH_MAC_VOICE` is set, else silent. To add a voice, see "Adding a voice" in `skills/architecture/references/walkthrough.md`. |
| `KOKORO_VOICE` | The Kokoro voice for that narration. It must be an American or British English voice (prefix `a` or `b`). Default: `af_heart`. |
| `WALKTHROUGH_MAC_VOICE` | A macOS voice, named as `say -v '?'` lists it. A blank value counts as unset. `WALKTHROUGH_VOICE=mac` narrates with it, and fails the render when this is unset. `WALKTHROUGH_VOICE` unset or empty: it narrates when `ELEVEN_LABS_API_KEY` is unset, and with that key set ElevenLabs narrates and this is ignored; with both unset, the video is silent, with the narration as subtitles. Any other `WALKTHROUGH_VOICE` ignores it. Premium voices (System Settings → Accessibility → Read & Speak → System voice → Manage Voices…; Spoken Content before macOS 26) sound far better than the built-in ones. |
| `AI_WORKFLOW_APP_BIN` | Path to the board's `ai-workflow-app` binary. Default: `ai-workflow-app` found on `PATH`. If the path you give does not exist, the board is treated as absent; `PATH` is not searched instead. |
| `PERMISSION_HOLD_DEADLINE_S` | When a tool call needs permission, the prompt is also posted to the board, and whichever answer comes first wins. This is how many seconds the board side waits. Default: `570`. A value that is not a whole number below `600` uses the default. |
| `PERMISSION_POLL_INTERVAL_S` | Seconds between checks for the board's answer to that prompt. Default: `3`. |
Expand Down
86 changes: 59 additions & 27 deletions skills/architecture/references/walkthrough.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Walkthrough Videos

The Architecture skill gives each architecture doc a walkthrough video at the top of its hosted
Notion page: narrated by ElevenLabs or a macOS voice when one is configured, and silent with the
narration as subtitles when neither is. The video tells the story of the work in a few minutes, so
the user can review the design by watching it. The skill writes the script after the doc and
renders it before hosting. Whenever a revision changes what the video says, it re-renders before
re-hosting.
Notion page: narrated by Kokoro, ElevenLabs or a macOS voice when one is configured, and silent
with the narration as subtitles when none is. The video tells the story of the work in a few
minutes, so the user can review the design by watching it. The skill writes the script after the
doc and renders it before hosting. Whenever a revision changes what the video says, it re-renders
before re-hosting.

## Files

Expand All @@ -15,7 +15,7 @@ All of these sit beside the doc in `docs/plans/`. `<doc-basename>` is the doc's
| File | What it is |
|---|---|
| `<doc-basename>.walkthrough.json` | The script. You write it. |
| `<doc-basename>.walkthrough.cache/` | Narration audio, cached by text and voice, and by the model and speed the CLI pins for ElevenLabs or by the scene's cues for a macOS voice. `walkthrough render` writes it; keep it between renders. |
| `<doc-basename>.walkthrough.cache/` | Narration audio, cached by text, voice, and the speed the CLI pins, plus the ElevenLabs model it pins, the Kokoro package versions it pins (Kokoro's model is not pinned), or the scene's cues for a macOS voice. `walkthrough render` writes it; keep it between renders. |
| `<doc-basename>.walkthrough.mp4` | The video. |

The doc's first line under its title embeds the video: `![Walkthrough](<doc-basename>.walkthrough.mp4)`.
Expand All @@ -24,20 +24,50 @@ Hosting turns that line into a Notion video (`skills/workflow-guide/references/n

## Choosing the voice

Run `check-tools --require elevenlabs,mac-voice` first. A `failed` result for either one: stop
and print the reason. Otherwise the first configured voice narrates the video:
- `elevenlabs` configured: ElevenLabs narrates it. `ELEVEN_LABS_VOICE_ID` overrides the voice the
CLI pins, per project.
- else `mac-voice` configured: the macOS voice `WALKTHROUGH_MAC_VOICE` names narrates it, free and
local. The name is as `say -v '?'` lists it. Premium voices, downloaded under System Settings →
Accessibility → Read & Speak → System voice → Manage Voices… (Spoken Content before macOS 26),
Run `check-tools --require elevenlabs,kokoro,mac-voice` first. Only the voice that narrates reads
`configured`: `WALKTHROUGH_VOICE` names it (`elevenlabs`, `kokoro` or `mac`), and when it is unset or
empty, ElevenLabs narrates if `ELEVEN_LABS_API_KEY` is set, else the macOS voice if
`WALKTHROUGH_MAC_VOICE` is set.
- any `failed`: stop and print the reason. A non-empty `WALKTHROUGH_VOICE` that names no voice fails
all three tools; `WALKTHROUGH_VOICE=elevenlabs` without `ELEVEN_LABS_API_KEY` fails `elevenlabs`
and `WALKTHROUGH_VOICE=mac` without `WALKTHROUGH_MAC_VOICE` fails `mac-voice`, not a missing one;
`walkthrough render` exits 1 on each.
- `kokoro` `configured` (`WALKTHROUGH_VOICE=kokoro`): go on. Kokoro narrates locally through `uv`,
and neither ElevenLabs nor a macOS voice is used even when configured. The first run downloads
the model and a Python environment with PyTorch.
- `elevenlabs` `configured`: go on. ElevenLabs narrates. With `WALKTHROUGH_VOICE` unset or empty and
`WALKTHROUGH_MAC_VOICE` set too, ElevenLabs wins and `walkthrough render` says on stderr that the
macOS voice is ignored.
- `mac-voice` `configured`: go on. The macOS voice `WALKTHROUGH_MAC_VOICE` names narrates it, free
and local. The name is as `say -v '?'` lists it. Premium voices, downloaded under System Settings
→ Accessibility → Read & Speak → System voice → Manage Voices… (Spoken Content before macOS 26),
sound far better than the built-in compact ones.
- neither: `walkthrough render` makes the video without a voice: each scene lasts as long as
reading its narration takes, and the narration shows as subtitles. Tell the user in one line
that the walkthrough is silent and subtitled because no voice is configured.

With both configured, ElevenLabs wins and `walkthrough render` says on stderr that the macOS voice
is ignored.
- none configured: go on. `walkthrough render` makes the video without a voice: each scene lasts as
long as reading its narration takes, and the narration shows as subtitles. Tell the user in one
line that the walkthrough is silent and subtitled because no voice is configured.

Each voice defaults to the one the CLI pins: `KOKORO_VOICE` overrides Kokoro's, and
`ELEVEN_LABS_VOICE_ID` overrides ElevenLabs'. `KOKORO_VOICE` must be an American or British English
voice (prefix `a` or `b`); any other voice gives no word timings, and the render exits 1.

### Adding a voice

- Add its name to the `VoiceName` union in `tools/cli/src/walkthrough/voice.ts`.
- Implement `Voice` in its own module under `tools/cli/src/walkthrough/`, saying in
`chargedPerCharacter` whether it charges for each character it voices.
- List it in `VOICES` in `tools/cli/src/walkthrough/voices.ts`. It joins the order used when
`WALKTHROUGH_VOICE` is unset or empty only through `selectDefaultVoice` there.
- Give it a `TOOL_SPECS` entry in `tools/cli/src/check-tools/specs.ts`.
- Add its tool name to the `check-tools --require` line above. `check-tools` reports only the tools
that line names, so a voice left off it never appears: the listed voices all read
`unconfigured` while it is selected, and the video is reported silent.
- Add its name to the `walkthrough render` help text in `tools/cli/src/commands/walkthrough.ts`.
- Check and update every sentence in `README.md` and `skills/` that names the existing voices, and
document the new voice's variables:
`git grep -n -i -e kokoro -e elevenlabs -e mac-voice -e WALKTHROUGH_MAC_VOICE -- README.md skills`.
- Update the tests that hard-code the voice names: the no-voice notice, the unknown-voice message
("elevenlabs, kokoro, mac") and the voice lists in the check-tools expectations, all in
`tools/cli/src/commands/walkthrough.test.ts`.

## Write the script

Expand Down Expand Up @@ -150,17 +180,19 @@ walkthrough render --script "docs/plans/<doc-basename>.walkthrough.json" --out "
problems means fix the script and re-run.
- Exit 2 means a tool it needs isn't usable:
- `ffmpeg -version` doesn't run cleanly, whether `ffmpeg` is missing or broken;
- `ELEVEN_LABS_API_KEY` is unset, `WALKTHROUGH_MAC_VOICE` is set, and `say` or `ffprobe` doesn't
run cleanly, whether it is missing or broken (`say` is macOS-only);
- the macOS voice narrates and `say` or `ffprobe` doesn't run cleanly, whether it is missing or
broken (`say` is macOS-only);
- `hyperframes browser ensure`, run through `npx`, doesn't exit 0. For example: `npx` is missing,
the HyperFrames package can't be fetched, or no Chrome can be found or downloaded.
- An ElevenLabs error exits 1 with its status. Stop and report ElevenLabs as broken, the same as
`check-tools`' `onFailure`. Scenes already voiced stay cached.
- With `ELEVEN_LABS_API_KEY` unset, a `WALKTHROUGH_MAC_VOICE` naming a voice that isn't installed
exits 1 before anything renders.
Stop and report it, the same as `check-tools`' `onFailure` for `mac-voice`.
- A voice error exits 1: ElevenLabs with its status, Kokoro with its exit, the macOS voice with the
`say` or `ffprobe` failure. Stop and report that voice as broken, the same as `check-tools`'
`onFailure`. Scenes already voiced stay cached. With Kokoro selected, a missing `uv` exits 2 like
any other missing tool.
- When the macOS voice narrates, a `WALKTHROUGH_MAC_VOICE` naming a voice that isn't installed exits
1 before anything renders. Stop and report it, the same as `check-tools`' `onFailure` for
`mac-voice`.
- stdout is a JSON summary. `narrated: false` means the video was rendered silent, with subtitles.
`voice` says which voice narrated it: `elevenlabs`, `mac`, or `null`.
`voice` says which voice narrated it: `elevenlabs`, `kokoro`, `mac`, or `null`.
`overNotionLimit: true` means the video is over 20 MiB and won't upload.
Host the doc without the video and tell the user.

Expand Down
13 changes: 10 additions & 3 deletions tools/cli/src/check-tools/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,19 @@ export const runCheckTools = (tools: readonly string[]): void => {
return
}

const classifyTool = (tool: string, env: NodeJS.ProcessEnv): ToolResult => {
export const classifyTool = (tool: string, env: NodeJS.ProcessEnv): ToolResult => {
const spec = TOOL_SPECS[tool]
if (!spec) return unknownTool(tool)

const present = findPresentEnv(spec, env)
const missing = findMissingEnv(spec, env)
const accepted = acceptedEnv(spec)

const problem = spec.envProblem?.(env)
if (problem !== undefined) {
return result(spec, 'failed', present, missing, accepted, problem)
}

if (missing.length > 0 && present.length > 0) {
return result(
spec,
Expand All @@ -36,15 +41,17 @@ const classifyTool = (tool: string, env: NodeJS.ProcessEnv): ToolResult => {
)
}

if (present.length > 0) return result(spec, 'configured', present, missing, accepted)
if (spec.inUse?.(env) ?? present.length > 0) {
return result(spec, 'configured', present, missing, accepted)
}

return result(
spec,
'unconfigured',
present,
missing,
accepted,
`env vars not set: ${accepted.join(', ')}`
spec.unusedReason?.(env) ?? `env vars not set: ${accepted.join(', ')}`
)
}

Expand Down
47 changes: 43 additions & 4 deletions tools/cli/src/check-tools/specs.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
import { elevenlabs } from '../walkthrough/elevenlabs.ts'
import { kokoro } from '../walkthrough/kokoro.ts'
import { mac } from '../walkthrough/mac-voice.ts'
import type { Voice } from '../walkthrough/voice.ts'
import { selectVoice } from '../walkthrough/voices.ts'
import type { ToolSpec } from './types.ts'

// A voice is configured only while it is the one render would narrate with, and fails with the
// problem render would stop on: any selection problem, or its own when it is the one selected.
const voiceSpec = (
voice: Voice,
spec: Omit<ToolSpec, 'envProblem' | 'inUse' | 'unusedReason'>
): ToolSpec => ({
...spec,
envProblem: (env) => {
const selection = selectVoice(env)
return selection.voice === undefined || selection.voice === voice
? selection.problem
: undefined
},
inUse: (env) => selectVoice(env).voice === voice,
unusedReason: (env) => {
const { voice: selected } = selectVoice(env)
if (selected === undefined) {
return undefined
}
if (env.WALKTHROUGH_VOICE) {
return `WALKTHROUGH_VOICE selects ${selected.name}`
}
const ownEnvSet = (spec.requiresAll ?? []).every((name) => env[name]?.trim())
return ownEnvSet ? `${selected.name} is selected ahead of it` : undefined
},
})

export const TOOL_SPECS: Readonly<Record<string, ToolSpec>> = {
jira: {
tool: 'jira',
Expand All @@ -15,18 +47,25 @@ export const TOOL_SPECS: Readonly<Record<string, ToolSpec>> = {
'STOP and tell the user Notion is configured but the Notion MCP tools are not working. Do NOT fall back.',
requiresAll: ['NOTION_PAGE_URL'],
},
elevenlabs: {
elevenlabs: voiceSpec(elevenlabs, {
tool: 'elevenlabs',
use: 'ElevenLabs narration for walkthrough videos',
onFailure:
'STOP and tell the user ElevenLabs is configured but walkthrough render cannot voice the narration. Do NOT fall back.',
requiresAll: ['ELEVEN_LABS_API_KEY'],
},
'mac-voice': {
}),
kokoro: voiceSpec(kokoro, {
tool: 'kokoro',
use: 'Local Kokoro narration for walkthrough videos',
onFailure:
'STOP and tell the user walkthrough render cannot voice the narration, with the reason given. Do NOT fall back.',
requiresAll: ['WALKTHROUGH_VOICE'],
}),
'mac-voice': voiceSpec(mac, {
tool: 'mac-voice',
use: 'A macOS voice narrating walkthrough videos',
onFailure:
'STOP and tell the user a macOS voice is configured but walkthrough render cannot voice the narration with it. Do NOT fall back.',
requiresAll: ['WALKTHROUGH_MAC_VOICE'],
},
}),
}
5 changes: 5 additions & 0 deletions tools/cli/src/check-tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type ToolSpec = {
readonly onFailure: string
readonly requiresAll?: readonly string[]
readonly acceptsAnyPrefix?: readonly string[]
readonly envProblem?: (env: NodeJS.ProcessEnv) => string | undefined
// Replaces env presence in deciding configured or unconfigured.
readonly inUse?: (env: NodeJS.ProcessEnv) => boolean
// Why the tool is unconfigured when `inUse` says no, if env presence does not say it.
readonly unusedReason?: (env: NodeJS.ProcessEnv) => string | undefined
}

export type EnvSummary = {
Expand Down
Loading