video-hosting-draft
Code drafts. A human decides. Code publishes. This skill owns the draft step. It never publishes anywhere — the publish step is a separate skill (
/video-hosting-publish) that runs only after a human has approved a row.
What this skill does (one paragraph)
For every raw clip in the input folder that is not already represented in the editorial Google Sheet, this skill produces exactly three title variants, three description variants, and three different PNG thumbnails (one thumbnail per title) and appends a single row for that clip to the editorial sheet. It does not call the video hosting API. It does not mark anything as approved. Its only job is to give the editor three principled options to choose from, in the place where the editor already works.
Inputs
videos/— folder of raw.mp4clips from the production team. Each file is named arbitrarily (e.g.clip_2026-05-03_v3.mp4) — filenames are not trusted for content/category signal.transcripts/— optional.txtnext to each.mp4. If absent, the skill runs Whisper locally to produce one before any LLM call.thumbnails/case/template.pngandthumbnails/educational/template.png— design-provided template files. The skill renders text on top of the matching template; it never publishes a bare template.- Editorial Google Sheet (id in
config.yaml). The sheet is the single source of truth for what has been drafted, what is approved, and what has been published.
Output: one row per video in the editorial Google Sheet
Sheet columns (in this exact order — /video-hosting-publish reads the same columns):
| Column | Type | Written by | Notes |
|---|---|---|---|
video_id |
string (sha256[:12]) | draft | Stable id derived from the file content hash. Used for dedup. |
source_path |
string | draft | Path to the source .mp4. |
category |
enum: case / educational | draft | Detected from transcript content (see below). |
title_1 |
string | draft | First title variant. |
title_2 |
string | draft | Second title variant. |
title_3 |
string | draft | Third title variant. |
description_1 |
string | draft | First description variant. |
description_2 |
string | draft | Second description variant. |
description_3 |
string | draft | Third description variant. |
thumbnail_1_url |
string (Drive link) | draft | PNG rendered with title_1 on top of the category template. |
thumbnail_2_url |
string (Drive link) | draft | PNG rendered with title_2. |
thumbnail_3_url |
string (Drive link) | draft | PNG rendered with title_3. |
final_title |
string | editor | The title to actually publish. Editor pastes/edits here. May be a copy of title_N or freely edited. |
final_description |
string | editor | The description to publish. |
final_thumbnail_url |
string (Drive link) | editor | The thumbnail PNG to publish. |
approved |
checkbox (boolean) | editor | The approval mechanism. Publish reads only rows where approved = TRUE. |
published_clip_id |
string | /video-hosting-publish |
Marker written after a successful publish. Empty until then. |
published_at |
ISO timestamp | /video-hosting-publish |
Marker written after a successful publish. |
The approval element is a Google Sheets checkbox in column approved (data validation: checkbox, default unchecked). It is the only signal of human consent. A row with approved = FALSE is invisible to the publish step, no matter how complete it looks.
Three principled variants — what "principled" means
Three random rephrasings of the same headline are useless to the editor: she'd still have to write the title herself. The three variants must differ along an explicit axis so each one gives the editor a real choice. The skill picks three different angles per clip and asks the LLM to write one variant for each angle:
For case videos (customer story / before-and-after):
- Outcome-first — leads with the measurable result. ("We cut onboarding from 14 days to 3.")
- Protagonist-first — leads with who did it and at what company. ("How Acme's ops team killed their busywork.")
- Tension-first — leads with the problem that triggered the change. ("87 clips in the queue and no one to package them — until…")
For educational videos (explainer / how-to):
- Question-as-title — frames the lesson as the question it answers. ("What is human in the loop, actually?")
- Tactic-first — names the concrete technique. ("Three variants beat one prompt, every time.")
- Anti-pattern-first — leads with the mistake the lesson cures. ("Stop letting your script auto-publish.")
The same axes drive the description variants (matched 1↔1↔1 with the titles so each row in the sheet — title_N + description_N + thumbnail_N — is internally coherent). The thumbnails are three different PNGs, one per title (see Rendering below). The skill records the chosen axis for each variant in the prompt log so the editor can see why each option exists.
Category detection from content (not filename)
The skill never reads category from the file name. The flow is:
- Get the transcript (either the supplied
.txt, or run Whisper on the audio track). - Send the first ~600 tokens of the transcript to the LLM with a two-label classifier prompt: case vs educational, with a one-sentence definition of each.
- Use the returned label to pick
thumbnails/{category}/template.pngfor rendering. - Persist the label to the
categorycolumn. The editor can override by editing the cell — the publish step does not re-read category from the transcript.
This is deliberate: the same clip should land on the same template regardless of how the file is named, and regardless of what folder it sat in.
Thumbnail rendering (code, not just the template)
For each variant, the skill renders a fresh PNG using Pillow (PIL):
Image.open("thumbnails/<category>/template.png").copy()— load the design template.ImageDraw.Drawwith the design-system font (assets/fonts/Inter-Bold.ttf), font size from the design spec, color from the spec.- Use
textbboxto measure the title and shrink the font size until the title fits the design's safe area; wrap onto two lines if needed. - Draw the title in the spec's text box. Do not touch the template's graphics.
img.save(out_path, "PNG", optimize=True).- Upload
out_pathto the Drive thumbnails folder and put the share link inthumbnail_N_url.
The skill never uploads a bare template — every cell in thumbnail_N_url points to a PNG that has the corresponding title drawn on it. The three PNGs for one video differ because the three titles differ.
Dedup on re-run (idempotency)
The skill must be safe to re-run after new clips drop into videos/. The dedup mechanism is:
- Compute
video_id = sha256(<file bytes>)[:12]for each.mp4in the input folder. - Read the existing sheet (one API call). Build the set of
video_ids already present in thevideo_idcolumn. - For each input clip whose
video_idis not in that set, run the full pipeline and append one row. - For each clip whose
video_idis already in that set, skip it entirely. The skill does not regenerate variants, does not overwrite a row, does not touch what the editor has typed.
Why content-hash rather than filename: the production team renames clips constantly. Hash is stable across rename. Why a column rather than a separate state file: the sheet is the single source of truth — if the row exists, the row exists; no out-of-band state can lie about it.
Failure modes the skill handles explicitly
- No transcript and Whisper fails — log the file, skip the row, exit non-zero so it shows up in scheduling.
- LLM returns 2 variants instead of 3 — retry once; if still wrong, write
[draft-incomplete]placeholder so the editor sees the row is broken rather than silently dropping the clip. - Drive upload fails for a thumbnail — leave that
thumbnail_N_urlempty; the editor sees a missing thumb and knows to nudge. - Sheet write conflict — exponential backoff up to 30s.
What this skill never does
- Never publishes to the video hosting (that is
/video-hosting-publish). - Never writes to
final_title,final_description,final_thumbnail_url,approved,published_clip_id,published_at— those are editor or publish-skill territory. - Never reads
approved. - Never re-runs on a
video_idthat already has a row.