# Video Hosting Draft

> For each new video in the input folder, generate three principled title variants, three description variants, and three different PNG thumbnails, then write one row per video to the editorial Google Sheet for a human editor to review and approve. Use this skill when the editorial pipeline needs draft packaging for raw cut clips before a human approves what gets published.

- Skill: `aturilin/video-hosting-draft` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aturilin/video-hosting-draft`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aturilin/video-hosting-draft/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: aturilin (https://skillmd.com/u/aturilin)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aturilin/video-hosting-draft

---


# 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 `.mp4` clips 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 `.txt` next to each `.mp4`. If absent, the skill runs Whisper locally to produce one before any LLM call.
- `thumbnails/case/template.png` and `thumbnails/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):
1. **Outcome-first** — leads with the measurable result. ("We cut onboarding from 14 days to 3.")
2. **Protagonist-first** — leads with who did it and at what company. ("How Acme's ops team killed their busywork.")
3. **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):
1. **Question-as-title** — frames the lesson as the question it answers. ("What is human in the loop, actually?")
2. **Tactic-first** — names the concrete technique. ("Three variants beat one prompt, every time.")
3. **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:

1. Get the transcript (either the supplied `.txt`, or run Whisper on the audio track).
2. 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.
3. Use the returned label to pick `thumbnails/{category}/template.png` for rendering.
4. Persist the label to the `category` column. 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)**:

1. `Image.open("thumbnails/<category>/template.png").copy()` — load the design template.
2. `ImageDraw.Draw` with the design-system font (`assets/fonts/Inter-Bold.ttf`), font size from the design spec, color from the spec.
3. Use `textbbox` to measure the title and shrink the font size until the title fits the design's safe area; wrap onto two lines if needed.
4. Draw the title in the spec's text box. Do not touch the template's graphics.
5. `img.save(out_path, "PNG", optimize=True)`.
6. Upload `out_path` to the Drive thumbnails folder and put the share link in `thumbnail_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:

1. Compute `video_id = sha256(<file bytes>)[:12]` for each `.mp4` in the input folder.
2. Read the existing sheet (one API call). Build the set of `video_id`s already present in the `video_id` column.
3. For each input clip whose `video_id` is **not** in that set, run the full pipeline and append one row.
4. For each clip whose `video_id` **is** 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_url` empty; 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_id` that already has a row.

