ToolsActive
video-studio
A Claude Code plugin that turns a long Hebrew video into vertical shorts with correct burned-in Hebrew captions, and I mainly use it to cut shorts for my Jewish philosophy site. Most of what I learned came from captions that looked fine and were wrong.

My Hebrew Jewish philosophy site, religousphilosophy.com, is built around long recorded talks and debates. Most people meet that kind of content as a short vertical clip, not as a ninety-minute video. So the main reason video-studio exists is simple: I use it to cut shorts for that site. The site has its own page here, jewish-philosophy.
video-studio is a Claude Code plugin (an add-on that gives Claude Code new skills). You give it a long Hebrew video. It transcribes it, helps pick complete thoughts, renders each one as a 1080x1920 vertical clip with Hebrew captions burned in, writes an .srt caption file beside it, and can publish the result to YouTube.
The repo is yodem/video-studio. It started on 2026-08-30 under the name hebrew-shorts, has 99 commits, and is at version 0.8.1 as of 2026-10-28. This page is mostly about the things that did not work, because those are where the code came from.
I did not want to cut shorts by hand. A talk has many good thoughts, and finding each one means scrubbing a timeline, then adding captions in an editor that handles Hebrew badly. Hebrew is written right to left, and many video tools still get the order of words wrong when they mix it with numbers or English.
So the first version was a packaging job. The README says it plainly: it is a pipeline that already ran successfully by hand, and every constant in the config file was checked against a real render. I kept that rule. If a number sits in config/defaults.json, it came from looking at an actual frame or measuring an actual file.
Before building anything I checked what already existed for the part I was least sure of, which was the captions. Hebrew speech recognition is the first gate. I use ivrit-ai/whisper-large-v3-turbo-ct2 through faster-whisper, with word-level timestamps. On one earlier run it took 35.9 minutes to transcribe 92.8 minutes of audio, about 2.6 times real time.
The model still mishears vocabulary that matters on a philosophy channel. The generic small model wrote one common word for commandments wrongly, and even the turbo model spelled the name of a Jewish legal device wrong. So the repo has a glossary with two parts. One is a list of terms passed to the decoder as a hint. The other is a whole-word find-and-replace applied after transcription, and every replacement is written to correction_log.json, so nothing is changed silently.
The second thing I looked at was my own memory. Language models guess timestamps badly: on a 90-minute source, a guessed time for a quote was off by 2 to 7 minutes. So locate takes the exact words of a quote and finds them in the real word timestamps. The agent never guesses a time.
There are three skills. shorts is the main one: transcribe, locate, render, verify. edit cleans a long recording into a tighter 16:9 upload, with proposed cuts for silences and filler words that a person accepts or rejects. make-video builds a video from an idea with no source footage. I use the first one most, and this page is about it.
A run looks like this. Each run gets its own dated folder under runs/, created automatically so a second run on the same video never overwrites the first.
python3 scripts/hshorts.py transcribe path/to/video.mp4
python3 scripts/hshorts.py --work-dir runs/video_2026-09-07 locate \
--quote "the exact quote from the transcript"
python3 scripts/hshorts.py --work-dir runs/video_2026-09-07 check-clips
python3 scripts/hshorts.py --work-dir runs/video_2026-09-07 render \
--clips clips.json --speaker-crop 604:339:25:12
python3 scripts/hshorts.py verify runs/video_2026-09-07/renders/
Claude does the judgment part: reading the transcript and choosing clips. The scripts do everything that can be decided by code. That split matters for what follows, because each time Claude or my code got something wrong, the fix was a check in a script, not a sentence asking the model to be careful.
The first release, 0.1.0, reversed the order of the words in each caption line before handing it to libass, the library ffmpeg uses to draw subtitles. I had read that libass shapes letters right to left inside a word but never reorders words across a line. It sounded right, and I wrote it into the docs as fact.
Then I looked at real output. Commas and periods showed up at the start of a phrase, and words that belonged at the end of a line appeared at the beginning. A viewer comment would have said "your captions are backwards", and they were.
I rendered probe frames with the exact ffmpeg build I use and looked at them. libass does apply the full Unicode bidi algorithm, so plain Hebrew in logical order renders correctly with no help. My reversal was a double reversal. In 0.2.0 on 2026-09-07 I deleted the function that did it, visual_token_order, and rewrote the docs to say the earlier claim was wrong, in the file itself, so the next reader or agent does not bring it back.
The lesson was not about libass. It was that my "verified" note had been verified against a different build than the one in production. I now keep a test, bidi-check, that renders a probe line and measures whether the wide word lands on the left. It runs before every render and fails loudly. I added it because this assumption had already broken silently once, when the ffmpeg build changed underneath it.
I wanted the word being spoken to light up, like a karaoke line. That is where the next bug lived. libass applies bidi per run, and a colour tag in the middle of a line splits it into separate runs that are laid out left to right in memory order. Putting a colour tag in logical order scrambled the Hebrew.
What worked was to split the line into before, current and after, emit the three parts in reversed order, keep the words inside each part in logical order, and wrap each part in a right-to-left mark. Without those marks, a comma attached to a word detaches and flies to the edge of the line. The function is line_with_highlight in scripts/bidi_ass.py, and tests/test_bidi.py pins the order, the marks and the timing of each per-word caption event.
The .srt file gets none of this. It stays plain logical order with no tags, because YouTube does its own reordering.
On 2026-10-04 I found two more by watching rendered shorts, not by reading code.
The first was numbers split apart. faster-whisper emits a number like "2,000" as several tokens with no space between them, and my caption code joined every token with a space, so it came out as "2 ,000". Hebrew writes a number after a hyphenated prefix, and that split the same way. The fix, merge_continuation_tokens, joins a token that starts with a hyphen, apostrophe, percent sign, or a comma or period before a digit onto the word before it.
The second was English scrambled under the highlight. The highlight logic reversed every segment, which is right for Hebrew and wrong inside a run of English. This line is the example from the changelog, a Hebrew word followed by an English phrase:
באו״ם, No Jews, no news.
With the highlight on, it showed up as "no news. Jews, " then the Hebrew word, then "No". A run of Latin-script words is now treated as one block that keeps its logical order, so the layout is the same whichever word is lit. Lines that are Hebrew only produce the same output as before.
Both fixes came from one habit: do not judge captions from the code. Render the clip and look at the frame.
The shorts crop the speaker into the frame, so I measure where the face is. The command detect-focus samples 24 frames per clip with OpenCV and writes the position to focus.json. In 0.5.0 I made render refuse to run without a measured focus, so there is no silent fallback to a default.
Then I used it on a two-person lecture. On 2026-10-28 I noticed it had centred the guest in 3 of 4 clips. It finds faces. It does not know who is speaking. For one person in frame that is enough, and for two it was a coin flip that I had not noticed because the output looked confident.
In 0.8.1 it stopped guessing. A clip with several people is reported AMBIGUOUS with each person's horizontal position, gets no entry in focus.json, and exits with code 1. Since render needs that entry, it refuses the clip. A person looks at one frame and then passes --speaker-side left or --speaker-side right once. The fix is small, and I would take a tool that asks over one that is right three times in four.
The last part is YouTube. There is no simple API path for what I wanted, which includes linking each short to the full video, so scripts/youtube_publish.py drives YouTube Studio through Playwright, attached to a Chrome I have already started in debug mode. It attaches the file, fills the details, schedules, and reads everything back to check it.
On 2026-10-28 it exited with code 3 after the upload on every single clip. Code 3 is the one that means a step failed after the upload and hands over a fallback.json so the rest can be finished by hand. The failing step was "related video": the dialog that lets me pick the full-length video was empty.
I tried opening the edit page by hand and found out why. Studio's edit page, opened directly by URL, ran the video-list request as my personal channel, not the channel I publish for, and got back zero videos. The fix was to pin the channel in the URL with a ?c=<channel_id> parameter. The picker then loaded the channel's videos. I also changed the step to choose from the loaded list first and search only after that, using the dialog's own input, because the search label I had used matched an icon in the top bar.
A smaller one from the same day: the debug Chrome was answering on IPv6 only, so the code now tries the machine's IPv4 and IPv6 loopback addresses and its local name, and uses whichever answers.
The repo has 33 test files. Some are pure logic, such as the caption order, run folders and Drive upload. Some run tiny real ffmpeg renders when ffmpeg is installed. The publisher tests use mocks with no network.
Most of the checking is in the pipeline itself, not in the tests. render first runs bidi-check, then check-clips, which prints the five words on each side of every clip boundary and hard-fails a clip that ends or starts with an orphan fragment of the next sentence. Then verify probes the finished file: audio stream present, resolution, duration, and that the face sits within 110 pixels of the horizontal centre. A short may not run over 180 seconds.
One check came from a bug that looked good. With -filter_complex, ffmpeg turns off automatic audio selection, so a render could produce a plausible video with no audio at all. Now render maps the audio explicitly and probes the output, deleting it if the stream is missing. Later I also found the limiter was leaving every short about 1 LU too loud, measured at -15.1 LUFS and -0.3 dBTP on a test short. After the fix it measured -16.2 LUFS and -1.7 dBTP, and verify now re-measures loudness on every output.
There is one limit I state in the docs, not hide. check-clips cannot tell that a clip ends on a clean sentence and still leaves a thought unfinished. In a review round on the channel, a clip like that was called "ends in the middle, it's not good as it is". A script cannot judge that, so the boundary report is printed for a person to read, and an optional Claude review is advisory only and never blocks a render.
Verify against the build you run in production. My worst bug came from a note that was true somewhere else.
Do not let the model guess what a script can measure. Timestamps, face positions and loudness all became measurements, and each guess I removed was a bug I stopped having.
Make the tool refuse when it cannot be sure. detect-focus and render now stop on an ambiguous face, and the publisher stops with a fallback file on a failed step. A loud exit code is easier to fix than a confident wrong clip.
Yes, and recently. The runs/ folder, which is not in the repo, has 14 run folders, most of them dated between 2026-09-06 and 2026-10-28, one per video I have processed. The last commit is from 2026-10-28, and the detect-focus and publishing fixes above came from using it that week.
It has rough edges. Only Hebrew ships and is tested. The heavy steps, transcription and rendering, are set up to run on my dev server, because my laptop has no GPU, so on a single machine you need the --local flag and patience. The Drive upload depends on a Google client ID that rclone is retiring this year. And the AI review is only advice.
You need Claude Code, ffmpeg, and Python. Install it from the repo's marketplace:
/plugin marketplace add yodem/video-studio
/plugin install video-studio@video-studio
If you installed it under its old name, hebrew-shorts, reinstall it:
/plugin uninstall hebrew-shorts@hebrew-shorts
/plugin marketplace add yodem/video-studio
/plugin install video-studio@video-studio
The skill still answers to the words "hebrew-shorts". You can also use scripts/hshorts.py directly as a command-line tool, with no plugin at all. The YouTube publisher needs one more package:
pip install playwright
Then ask Claude Code to make shorts from a video, or run the commands above. To add another language, write one file in config/languages/, since the Hebrew settings live there and not in the code. I have only tested Hebrew, so treat any other language as untried.
