SLOPSHOPPER

Wyzowl Video Studio

Make professional videos with Claude Code on your Mac: a guided brief, a script and storyboard you approve, reusable brand kits, and animated or footage-based…

bandrowsguardtoastprocesstimer
★ 1v0.9.1LicenseRef-Wyzowl-Video-Studioupdated 2026-10-21wyzowl/video-studio/plugins/video-studio
A shopper browsing a rack in a slop shop
README

Wyzowl Video Studio

Make professional videos with Claude Code, on your own Mac. Claude writes the script, designs a storyboard you approve scene by scene, builds the video, adds its sound, and renders it, with a review page at every step where your feedback counts.

Works in Claude Code on macOS (tested on Apple silicon). It doesn't run in Claude's chat apps. It needs about 6 GB of disk space for HyperFrames' browser and models, plus 0.3 to 1.5 GB for each video.

What it does

  • A guided brief. A few questions about the video (type, brand, size, where it's shown, its length), then a script with the voiceover beside what happens on screen, for your approval.
  • Storyboards you approve. Real frames in your brand at the video's size. Approve each scene, or leave a note.
  • Videos built and checked for you. Animated videos, and videos cut from your own footage (transcribed, trimmed and assembled), reviewed against a seven-point rubric before you see them.
  • Brand kits. Colours, fonts, logos, motion and a logo ident, read from your website and files, reused across every video.
  • Sound. Sound effects from SFX Kit placed on their moments, music, and the voiceover you bring (your own recording, or one made with any voice service), with captions.
  • A library of ready-made animated items (widgets, layouts, text effects, transitions and data), dressed in your brand.
  • Delivery. A 60 fps master, a web copy, a poster and captions, rendered on your Mac. When a video is finished, Video Studio offers to tidy it up, to free the space its drafts and history take.

Get started

  1. Install the plugin: in Claude Code, type /plugin install video-studio --marketplace wyzowl/video-studio. Start a new session and run /video-studio:setup. It says what it will install and download (about 1 GB), installs it, creates your studio folder and makes a test render.
  2. Open Claude Code in the studio folder.
  3. Add a brand with /video-studio:new-brand, then start a video with /video-studio:new-video.
CommandWhat it does
/video-studio:setupChecks this Mac, installs what Video Studio needs, creates a studio folder and makes a test render
/video-studio:new-videoAsks a few questions, sets up the project, then writes a script and makes a storyboard, each for your approval
/video-studio:new-brandBuilds a reusable brand kit, with a logo ident and its own brand page
/video-studio:dashboardShows the studio's pages: videos, brands, scripts, storyboards, video reviews and the library

What Video Studio runs, fetches and sends

Video Studio works on your Mac. Your scripts, footage, voiceovers, brand kits and videos stay there. Here is everything it runs, downloads and sends, and when.

What it runs

  • Its own scripts, in Node.js, from the plugin's folder: the studio helper (scripts/studio.mjs), the page server, and the tools for review, footage, captions and sound. They draw storyboard frames and PDFs in HyperFrames' headless browser.
  • HyperFrames 0.8.149, through npx --yes hyperframes@0.8.149, to make, check, render and take stills of videos, and transcribe speech, all on your Mac. Videos made on an earlier version keep it, through their own scripts.
  • ffmpeg and ffprobe, for audio and video; macOS's open, to open a page in your browser or show a render or PDF in Finder; and unzip, to unpack SFX Kit. Setup's checks also run git --version, brew --version and sw_vers.
  • Setup's installs: Node.js, ffmpeg and git through Homebrew (you install Homebrew itself, with your Mac password), and two skills for Claude Code in your studio folder, through npx --yes skills@1.7.4.
  • A page server for the studio's pages, at http://127.0.0.1:4747 (or the next free port up to 4756). It answers only this Mac, serves only your studio's files, and refuses requests from websites.

What runs by itself

All of this happens only in a session inside a studio folder. Anywhere else, Video Studio stays silent.

  • When a session starts (scripts/session-start.sh), Video Studio tells Claude about the studio. If anything it needs isn't on your Mac yet (after an update, or a download that didn't finish), it starts downloading it in the background and logs the download in ~/.wyzowl-video-studio/install.log. Once a day, if you've entered a licence key, it checks the full library (below).
  • After Claude writes a file (scripts/page-ready.sh), Video Studio checks whether it's the file behind a studio page (a brand, script, storyboard or video review). If it is, it starts the page server if needed and tells Claude to show the page.
  • The band above the message box, and the answer box (hooks/index.tsx):
  • The band runs Video Studio's helper (node <plugin>/scripts/studio.mjs ready, and url to open a page, each with your studio's folder) to see what's waiting for your review. It does this when the session starts, after each of Claude's turns, and after each Video Studio tool Claude runs. It looks at Claude's Bash commands only to notice those tools.
  • When you press the band's button, it opens the studio page (a 127.0.0.1 address) in the desktop app's browser pane, through the browser pane's own tool, or else in your web browser, with open.
  • The answer box reads the text of Claude's final answer, and keeps a short fingerprint of it for the session, to draw that answer in a yellow box.
  • Nothing the band or the answer box reads leaves your Mac.

What it downloads

  • HyperFrames 0.8.149 and its dependencies, from npm. HyperFrames then fetches its headless browser (Chrome for Testing, from Google) and the two models setup downloads, from their releases on Hugging Face and GitHub: transcription (Parakeet, about 640 MB) and presenter cut-outs (about 168 MB).
  • Two skills, at setup:
  • HyperFrames' skills, from github.com/heygen-com/hyperframes, at its v0.8.149 release;
  • LottieFiles' motion-design skill, from a fixed commit of github.com/LottieFiles/motion-design-skill.
  • SFX Kit, the studio's sound effects (about 55 MB, CC0), from github.com/wyzowl/sfx-kit's release, checked against its checksum and then file by file.
  • The GSAP animation library, from cdn.jsdelivr.net, whenever a browser plays a video (to preview, check or render it) or one of the library's preview pages.
  • Fonts a video names without including, which HyperFrames fetches from Google Fonts as it renders (asking for the font and the characters the video uses), and keeps in its cache.
  • Your brand's website, when you ask for a brand kit from it: its pages, styles, fonts and logo files. Logo files and Google Fonts are downloaded only with your OK.
  • With a licence key: the full library's items, from Wyzowl's library service, checked against their checksum.

What it sends

  • With a licence key, once a day, Video Studio sends Wyzowl's library server (app.wyzowl.com):
  • your key;
  • an anonymous, random ID for this Mac (a key works on up to three Macs);
  • the version of the plugin, and of the library you have.

The server says whether the key is valid and whether a newer library is available. Without a key, nothing is sent.

  • Nothing else. Video Studio never sends your work anywhere.
  • For everything it runs, it turns off the anonymous usage data that HyperFrames, the skills tool and Homebrew would otherwise send, and HyperFrames' daily check for a newer release, since Video Studio chooses the version (HYPERFRAMES_NO_TELEMETRY=1, HYPERFRAMES_NO_UPDATE_CHECK=1, DISABLE_TELEMETRY=1 and HOMEBREW_NO_ANALYTICS=1).
  • It takes HyperFrames' stills with --describe false, so frames are never sent to a service to describe them.
  • Claude sees what you share in the chat, as usual in Claude Code.

What it keeps, and where

  • Your studio folder, wherever setup made it: brands, videos, renders, and the pages' notes.
  • ~/.wyzowl-video-studio:
  • your licence key and this Mac's anonymous ID;
  • the full library's items, and SFX Kit;
  • a list of the sounds SFX Kit didn't have;
  • setup's notes and the download log.
  • HyperFrames' own folders: its settings and logs (~/.hyperframes), and its cache (~/.cache/hyperframes): its browser, models and fonts, and each video's edit history.
  • npm's cache, with HyperFrames and the skills tool.

Tidying up. When a video is finished, Video Studio offers to tidy it up. Once you've said yes, it deletes the video's draft renders, review stills, replaced storyboards and footage previews, and its edit history in HyperFrames' cache. It keeps everything the video needs to be changed and rendered again.

Apart from setup's installs, Video Studio changes nothing outside those folders: no shell settings and no system settings.

Support and privacy

Licence

Video Studio is free to use to make videos, commercial or not, client work included, under the Wyzowl Video Studio Licence. Share links to it rather than copies, and don't release your own versions. SFX Kit is CC0; the bundled fonts are under the SIL Open Font License; and HyperFrames, its skills and the other tools setup installs are under their own licences.

Full documentation: https://github.com/wyzowl/video-studio

Source 2 files
hooks/index.tsx 171 lines
1// Wyzowl Video Studio's hooks module (one per plugin), both parts only in a session inside a studio (a folder with
2// studio.json): the band above Claude's message box, and the answer box around Claude's final answer.
3//
4// The band: when a script, storyboard or video is waiting for the person's review it says so, with a button that opens
5// that page; while Claude makes something it says what, and the step it's on, with "Watch", which opens the video's
6// overview (where it shows as it happens); otherwise "Working on a video?" with "Open dashboard". The button opens the
7// page beside the chat (the desktop app's browser pane, opened if it's closed) or in the web browser. What's waiting is
8// checked when the session starts, after each of Claude's turns and after each studio tool Claude runs (they mark the
9// steps), never on a timer. Dismissed, the band stays away until something new is waiting.
10//
11// The answer box: Claude's final message of a turn that did some work (used a tool) is drawn in a rounded box with a
12// yellow border and a faint yellow ground, so after a long run of tool calls it's plain where the answer starts. A turn
13// that was only a reply draws as usual, and so does every message before the final one. The final message is told by
14// its text (turn.complete's answer), each answer's fingerprint a member of its own, so a new answer redraws its own
15// message alone.
16//
17// Everything that uses the engine (`$`) or registers a hook (`on`) is in this one file, as Anthropic's plugin directory
18// asks of a mod.
19import { atom, read, update } from 'claude-code'
20import type { EngineInterface, Register } from 'claude-code'
21import type { ReadyPage } from '../types'
22
23const studioAt = atom({ plugin: 'video-studio', key: 'studio' } as const, '')
24const readyAt = atom({ plugin: 'video-studio', key: 'ready' } as const, null as ReadyPage | null)
25const dismissedAt = atom({ plugin: 'video-studio', key: 'dismissed' } as const, '')
26// one member per boxed answer, by its fingerprint
27const BOXED = { plugin: 'video-studio', key: 'boxed' } as const
28
29// Homebrew's node on Apple silicon and Intel Macs, ahead of the system's own folders
30const PATH = '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
31// the studio's own tools, which move a stage under way on: what the band says may have changed
32const STUDIO_TOOL = /studio\.mjs|render-frames\.mjs|sheets\.mjs|footage\.mjs/
33
34const YELLOW = '#F9B336'
35// the same yellow at 10%, where the surface paints with transparency (the terminal can't: there, the border alone)
36const TINT = '#F9B3361A'
37
38/** The studio this session works in: the nearest folder at or above its root that holds studio.json, or ''. */
39async function findStudio($: EngineInterface): Promise<string> {
40  let dir = (await $.session.root()).replace(/\/+$/, '') || '/'
41  for (;;) {
42    if (await $.fs.exists(`${dir === '/' ? '' : dir}/studio.json`)) return dir
43    if (dir === '/') return ''
44    dir = dir.replace(/\/[^/]+$/, '') || '/'
45  }
46}
47
48/** Runs the studio helper (scripts/studio.mjs) for the studio; its output, or '' when it fails. */
49async function helper($: EngineInterface, studio: string, args: string[]): Promise<string> {
50  const argv = ['/usr/bin/env', `PATH=${PATH}`, 'node', `${$.plugin.root}/scripts/studio.mjs`, ...args, '--studio', studio]
51  const run = await $.process.run(argv, { timeoutMs: 20000 }).catch(() => null)
52  return run?.exitCode === 0 ? run.stdout.trim() : ''
53}
54
55/** What's waiting for the person's review now, or else what Claude is making (studio.mjs ready), into the band's state. */
56async function checkReady($: EngineInterface, studio: string): Promise<void> {
57  let page: ReadyPage | null = null
58  try {
59    const now = JSON.parse(await helper($, studio, ['ready']))
60    page = now.page ?? now.making ?? null
61  } catch {}
62  await update($, readyAt, () => page)
63}
64
65/** Starts the studio's page server if needed and opens a page: beside the chat, or in the web browser. */
66async function openPage($: EngineInterface, studio: string, path: string): Promise<void> {
67  const url = (await helper($, studio, ['url', path])).split('\n').pop() ?? ''
68  if (!/^http:\/\/127\.0\.0\.1:\d+\//.test(url)) {
69    $.ui.toast(`Couldn't open the studio's pages. In Claude, run /video-studio:dashboard to see why.`)
70    return
71  }
72  const beside = await $.mcp.call('Claude_Browser', 'preview_start', { url }).catch(() => null)
73  if (beside && !beside.isError) return
74  await $.process.run(['/usr/bin/open', url]).catch(() => null)
75}
76
77/** A short, stable name for a message's text: its length and a hash of it, whitespace evened out. */
78function fingerprint(text: string): string {
79  const s = text.replace(/\s+/g, ' ').trim()
80  let hash = 0x811c9dc5
81  for (let i = 0; i < s.length; i++) hash = Math.imul(hash ^ s.charCodeAt(i), 0x01000193)
82  return `${s.length.toString(36)}-${(hash >>> 0).toString(36)}`
83}
84
85export const register: Register = on => {
86  // ───────── the band ─────────
87  on('session.start', async ($, e, next) => {
88    const started = await next(e)
89    const studio = await findStudio($)
90    await update($, studioAt, () => studio)
91    if (studio) await checkReady($, studio)
92    return started
93  })
94
95  // after each of Claude's own turns (not a subagent's), see what's waiting now: a new storyboard, a reviewed video
96  on('turn.complete', async ($, e, next) => {
97    const studio = await read($, studioAt)
98    if (studio && e.agentId === undefined) $.clock.after(0, () => void checkReady($, studio).catch(() => {}))
99    return next(e)
100  })
101
102  // after each studio tool Claude runs, see what it's making now ("Claude is making storyboard v3: make the frames")
103  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
104    const ran = await next(e)
105    try {
106      const studio = await read($, studioAt)
107      if (studio && STUDIO_TOOL.test(String(e.command ?? ''))) $.clock.after(0, () => void checkReady($, studio).catch(() => {}))
108    } catch {}
109    return ran
110  })
111
112  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
113    const studio = await read($, studioAt)
114    const ready = await read($, readyAt)
115    const shows = ready?.path ?? 'idle'
116    if (!studio || e.props.hasSurvey || (await read($, dismissedAt)) === shows) return next(e)
117    const { Box, Text, Button } = $.ui.resolve(e)
118    return (
119      <Box key="studio-bar" flexDirection="row" justifyContent="space-between" alignItems="center" gap={2}>
120        <Text>{ready ? ready.text : 'Working on a video?'}</Text>
121        <Box flexDirection="row" alignItems="center" gap={1}>
122          <Button
123            key="open"
124            variant="primary"
125            label={ready ? ready.button : 'Open dashboard'}
126            onPress={() => openPage($, studio, ready ? ready.path : '/')}
127          />
128          <Button key="dismiss" role="dismiss" label="Dismiss" onPress={() => update($, dismissedAt, () => shows)} />
129        </Box>
130      </Box>
131    )
132  })
133
134  // ───────── the answer box ─────────
135  // did this turn do any work? (a module value: a reload mid-turn just leaves that one answer unboxed)
136  let worked = false
137
138  on('turn.start', ($, e, next) => {
139    worked = false
140    return next(e)
141  })
142
143  on('tool.call', ($, e, next) => {
144    worked = true
145    return next(e)
146  })
147
148  // a turn that ended with an answer: the main loop's, not a subagent's report (turn.start starts each one afresh)
149  on('turn.complete', { reason: 'answer' }, async ($, e, next) => {
150    const done = await next(e)
151    if (e.agentId === undefined) {
152      if (worked && e.answer.trim() && (await read($, studioAt))) await update($, { ...BOXED, id: fingerprint(e.answer) }, () => true)
153      worked = false
154    }
155    return done
156  })
157
158  on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
159    if (e.props.isSummary || !e.props.text.trim() || !(await read($, studioAt))) return next(e)
160    if (!(await read($, { ...BOXED, id: fingerprint(e.props.text) }))) return next(e)
161    const drawn = await next(e)
162    const { Box } = $.ui.resolve(e)
163    const ground = e.surface === 'terminal' ? {} : { backgroundColor: TINT }
164    return (
165      <Box key="answer-box" flexDirection="column" borderStyle="round" borderColor={YELLOW} paddingX={1} {...ground}>
166        {drawn}
167      </Box>
168    )
169  })
170}
171
types/index.d.ts 15 lines
1// The values Wyzowl Video Studio's band (in hooks/index.tsx) keeps for the session: the studio it works in ('' outside
2// one), the page waiting for the person's review (from `studio.mjs ready`), and what the person dismissed (a page's
3// path, or 'idle' for the dashboard band). The answer box (also in hooks/index.tsx) keeps which of Claude's answers to
4// draw boxed, each by a fingerprint of its text.
5export type StudioPath = string
6
7/** A script, storyboard or video waiting for review: its page path, the band's words and its button's label. */
8export type ReadyPage = { path: string; text: string; button: string }
9
10declare module 'claude-code' {
11  interface PluginState {
12    'video-studio': { studio: StudioPath; ready: ReadyPage | null; dismissed: string; boxed: StateFamily<boolean> }
13  }
14}
15