--- name: fountain-clip-producer description: Render a clip post into a finished, platform-ready video with framing, captions, and overlays. --- ## Overview This skill turns the clip that a post carries into a video file. It decides nothing about the moment or the span, because the caller settles those before it runs. Module **media** cuts the landscape master, and every module after it works from that one file. The rest shape the picture, put the words and the layers on it, and gate the delivery on one report. ## Input - The `SocialPostMediaSource` of a `SocialPost`, which names the file and the span. The file can carry no video, which most of a podcast catalogue does not. A caller that clipped a video Fountain does not hold gives those fields without `ids`, and the post it made carries no `source` to read them back from. - The word timings of that span, which this skill makes from the clip's own audio. Module **shots** wants the speaker of each word too, which nothing supplies. - A delivery tier, which the words of the request imply. - Or a queue run: module **queue** reads the drafts that wait for media, and derives the inputs above. Optional: - A target shape, which is vertical, square, or landscape. - The name of a caption preset, or the text of an overlay. ## Output - The finished work: a landscape master, and one export for each shape that the request asks for. The master is finished work and not a working, because the user keeps it and cuts from it again. - A `SocialPostUpload` on `content.uploads` of the post, unless the user asked you not to attach it. - Workings: a clip manifest, a crop plan, a caption plan, an overlay plan, a QA report, and a removal report when module **trims** cut the clip. ## Housekeeping You MUST read HOUSEKEEPING.md if you haven't already. ## Requirements - Fountain API. - Python 3.11 or later. - OpenCV 4.8 or later, importable from that same Python, for the face detection of module **framing**. Its model, and the fonts that the presets name, ship in `assets`, so no machine installs either. - ffmpeg and ffprobe, built with libass, drawtext, fontconfig and whisper, or no caption can be burned and no word can be timed. A stock build often carries none of them, and on macOS the Homebrew `ffmpeg-full` formula is the one that does. - A whisper.cpp model file, which the whisper filter takes the path of and does nothing without. `ggml-base.en.bin` in `~/.cache/whisper` is the one this skill looks for first, and it is 141 MB, so the machine installs it one time and the skill does not ship it. - ImageMagick, to measure the width of caption text. - yt-dlp, for a source that ffmpeg cannot seek directly. Keep it current: YouTube changes what a client must send, and a build a few weeks old answers 403 on every download while the captions still come through. - A web search tool, and a way to read a page, for the reference sources of module **brand**. - Skill **fountain-onboarding**, which installs a tool that module **preflight** finds missing. ## Process 1. Read the delivery tier from the request. Do the least work that the tier asks for. 2. Run module **preflight** to check the machine before the first render. One report serves every clip of a run, because the machine does not change between them. 3. Run module **media** to cut the landscape master from `media`, between `ts_start` and `ts_end`. Cut all the clips of the run together, then transcribe them all together. No clip waits for another one. Then transcribe the master with whisper to get the word timings of the clip, and rebase them so the first word starts at zero. Use the binary and the model that module **preflight** names, with `max_len=1`, and put `model=` last in the filter string or the option after it is swallowed. That asks for one token for each segment, and each token is usually a whole word. A token is a continuation of the word before it only when it carries no leading space and the word before it does not end a sentence: join those two, and treat every other token as its own word. Do not join on timing, because whisper butts one word's start against the last one's end. A run of punctuation is its own token: attach it to the word before it rather than drop it, because module **captions** reads it to find a sentence end. These are measured from the audio being cut, so they are the only timings that describe this file. 4. Run module **trims** to survey the pauses and the filler, and report what it found. Cut only when the user asks, because the cut moves every time after it. 5. Run module **framing** to crop the master to each shape that the request asks for. Run module **shots** with it when one shot holds two people and the crop must follow who speaks. Skip both for a source with no video, which has no picture to crop and no face to follow. 6. Run module **brand** to load the look of the show, for a clean final or a publish final. 7. Send the user to https://beta.fountain.fm/docs/styling-clips when the request names no caption style and module **brand** holds none. That page draws every style, and the choice it records comes back as a brand kit. Do not hold the run for an answer: produce with the default and say what it was. 8. Run module **captions** on every portrait export, and on another shape when the request asks for it. Run module **fonts** with it. 9. Run module **overlays** when the request asks for a layer, and always for a source with no video. There the overlay is not polish: an audiogram package is the whole picture, and without one the clip is captions on an empty frame. Load the artwork of the show from `info.image` and give it to the package, which every one of them needs. 10. Run module **qa** as the blocking gate, and deliver nothing until it reports a pass. 11. Confirm on the render, and never on the transcript, that the quote the copy uses is in the clip and that the person it credits is the one who says it. The caller wrote both unseen: the transcript carries sentences and names no speaker. Take the speaker from the camera and from a cutaway that shows a closed mouth. Repair what is wrong with the Social API, and say what you changed: move `ts_start` or `ts_end` when the clip opens or closes inside a word, or when the quote sits outside the span, then write the new words into `transcript` so the two agree; correct the title, the text and the context when the credit is wrong. A moved edge makes the render stale, so go back to step 3 with the new span: the words, the captions and the gate all describe the old cut, and only the gate can say the new one is finished. Move an edge only to repair what you can prove, or to make a change the user asked for, and never to improve the clip - choosing the moment is the caller's job. 12. Attach the video to the post with the Uploads API and the Social API, unless the user asked you not to. 13. Present each finished clip on the clip card of skill **fountain-clip-finder**, with one added line saying the render result and where the video is attached. ## Additional notes A run with more than one clip does the same work on each clip. Move all the clips through one stage, then move them all through the next stage. Most of the time of a render goes between the actions, and not inside them. A render of one clip spends a third of its time in the tools. There are three delivery tiers, each adding to the one before, and the request implies which one. The user names the work they want, not the tier, so read it from their words: - A rough cut is the landscape master alone, with no crop, no captions and no gate. Read it from words about checking a span rather than making a clip. - A clean final is publishable, and a portrait export carries captions, because it is watched muted. Read it from "produce this clip", when the request names neither captions nor packaging. A clean final of a source with no video carries its audiogram package too, for the same reason that a portrait export carries captions: without it there is nothing to watch. - A publish final adds the overlays and the packaging, and the request names one of them. Ask when the words fit none of the three, and you MUST NOT raise the tier on your own: polish is requested work. Captions on a portrait export are not a raise, and a square or a landscape export still waits to be asked. The word timings come from the clip, and never from the episode transcript. That transcript carries sentences and no words, and it is the caller's evidence for the span rather than this skill's evidence for a caption. The words themselves are a different matter: whisper mishears a name or a number that the transcript has right, so correct the wording against the transcript and keep the timings whisper made. Always cut from the tallest rendition: a 9:16 crop keeps the whole height and about a third of the width, so that height is the real resolution of the clip, and module **qa** fails a big upscale. Deliver the shape the tallest rendition can hold when it cannot reach the target, e.g. 720x1280 from a 720p master, and say so - a true smaller resolution passes the gate and an upscale does not. A span that reads wrong but that you cannot prove wrong is a report and not a repair, and a letterbox is requested work too. To change a clip this skill already made, read the manifest and the QA report first, reuse what is still correct, and write each new output under a new name. Touch only the output of the module that changes, and a caption change MUST NOT force a new crop. A post does not have to be approved before this skill runs, and rendering one approves nothing. Never put an API key, a token, or a cookie into a command, a manifest, or a report. These steps make one clip, and several clips of one run are independent. Module **queue** therefore gives each clip its own worker and runs the workers at the same time, each in its own output folder, and a run is finished when the last clip is. They do not finish three times faster, because ffmpeg already uses every core of the machine. The purpose of this skill is a good clip, and not a full set of completed steps. Readability, framing, and sync matter more than procedure.