# Media Lab > A reference library for making content (Shorts, Reels, carousels, thumbnails, captions, event media) with a procedure, > named style worlds, and patterns whose previews are real output of the repo's own Python + ffmpeg renderers. - **Claude: read https://media-lab-seven.vercel.app/for-claude.md first.** It routes a brief to a world and the patterns to start from. - Structured data: https://media-lab-seven.vercel.app/patterns.json, https://media-lab-seven.vercel.app/worlds.json. Everything in one file: https://media-lab-seven.vercel.app/llms-full.txt - Install as a Claude Code plugin: `/plugin marketplace add HarshSalunkhe2005/Media-Lab` then `/plugin install media-lab@media-lab` ## The skill (procedure, formats, hooks, recipes) - [Media Lab: content with judgement, not templates](https://media-lab-seven.vercel.app/skill/SKILL.md) - [Carousel (Instagram, LinkedIn document)](https://media-lab-seven.vercel.app/skill/formats/carousel.md) - [Launch / promo video (product, app, project)](https://media-lab-seven.vercel.app/skill/formats/launch-video.md) - [Short vertical video (Shorts, Reels, TikTok)](https://media-lab-seven.vercel.app/skill/formats/short-video.md) - [Format specs](https://media-lab-seven.vercel.app/skill/formats/specs.md) - [Text posts: LinkedIn, X, threads](https://media-lab-seven.vercel.app/skill/formats/text-posts.md) - [Thumbnails and covers](https://media-lab-seven.vercel.app/skill/formats/thumbnail.md) - [Hook library](https://media-lab-seven.vercel.app/skill/hooks/hooks.md) - [Fill-in templates](https://media-lab-seven.vercel.app/skill/hooks/templates.md) - [1. Brief](https://media-lab-seven.vercel.app/skill/playbook/01-brief.md) - [2. Package before you produce](https://media-lab-seven.vercel.app/skill/playbook/02-package.md) - [3. Script](https://media-lab-seven.vercel.app/skill/playbook/03-script.md) - [4. Assets and edit](https://media-lab-seven.vercel.app/skill/playbook/04-edit.md) - [5. Verify (bounded, every time)](https://media-lab-seven.vercel.app/skill/playbook/05-verify.md) - [Event coverage (college fests, leagues, launches)](https://media-lab-seven.vercel.app/skill/playbook/06-event-coverage.md) - [Critique rubric](https://media-lab-seven.vercel.app/skill/playbook/critique.md) - [Taste: reads as AI vs reads as human](https://media-lab-seven.vercel.app/skill/playbook/taste.md) - [Voice](https://media-lab-seven.vercel.app/skill/playbook/voice.md) - [Recipes by goal](https://media-lab-seven.vercel.app/skill/recipes/by-goal.md) - [Carousels, posters, cards: HTML/CSS or Pillow](https://media-lab-seven.vercel.app/skill/recipes/carousel.md) - [Choose a tool](https://media-lab-seven.vercel.app/skill/recipes/choose-a-tool.md) - [End-to-end Short (free tools only)](https://media-lab-seven.vercel.app/skill/recipes/end-to-end-short.md) - [ffmpeg recipes (tested on ffmpeg 7/8, Windows Git Bash)](https://media-lab-seven.vercel.app/skill/recipes/ffmpeg.md) - [MoviePy recipes (MoviePy 2.x API)](https://media-lab-seven.vercel.app/skill/recipes/moviepy.md) - [Remotion (React as video)](https://media-lab-seven.vercel.app/skill/recipes/remotion.md) - [Free real photos, sourced legally](https://media-lab-seven.vercel.app/skill/recipes/stock-photos.md) ## Worlds - Quiet Poetry (`quiet-poetry`): Like a page torn from someone's notebook and left on a train seat. - Tech Launch (`tech-launch`): A demo that works, shown fast, with one number that matters. - Campus Event (`campus-event`): The league's own page: consistent, current, a bit proud. - Explainer (`explainer`): A friend who knows the topic, explaining it on a napkin. - Data Story (`data-story`): A poster in a good newspaper: one figure, said plainly. - Build Log (`build-log`): Notes from someone actually making the thing. - Warm Local (`warm-local`): The café or shop posting about its own week. - Faceless Story (`faceless-story`): A story told over pictures, held together by captions. ## Carousels - [Poetry on a graded photo](https://media-lab-seven.vercel.app/docs/poetry-film-photo.md): Real CC0 photo, one muted film grade, plain Garamond set in the darkest edge, small slide counter. [poetry, quote, film grade, garamond, real photo] - [Stanza on paper](https://media-lab-seven.vercel.app/docs/poetry-paper.md): No photo: a warm paper tone, wide margins, one serif, the stanza keeps its own line breaks. [poetry, paper, serif, typography] - [Cover slide with a promise](https://media-lab-seven.vercel.app/docs/carousel-cover-promise.md): Slide 1: one sentence-case promise in the photo's calm area, darkened only behind the text, plus a 1/7 cue. [cover, hook, carousel, linkedin document] - [Numbered point slide](https://media-lab-seven.vercel.app/docs/list-point-slide.md): Middle slide: big number, short headline, two supporting lines, the same grid on every slide. [list, tips, grid] - [One giant number](https://media-lab-seven.vercel.app/docs/data-story-number.md): A single earned statistic, one plain sentence on what it means, and the source in small type. [data, statistic, swiss, report] - [Last slide with one ask](https://media-lab-seven.vercel.app/docs/cta-last-slide.md): Closing slide with a single clear ask (save, send) tied to the goal metric, in the set's photo style. [cta, save, send, ending] ## Event media - [Match result card](https://media-lab-seven.vercel.app/docs/event-result-card.md): Score as the hero over a real court photo, both teams, one moment from the match and the next fixture. [sports, score, result, league] - [Player award card](https://media-lab-seven.vercel.app/docs/event-award-card.md): Photo with the subject uncropped, the award small, the name big, one real stat underneath. [award, mvp, portrait, sports] - [Daily schedule story](https://media-lab-seven.vercel.app/docs/event-schedule-story.md): Plain fixtures table inside the story safe zone, rendered from a data list so each day is one command. [schedule, story, fixtures, table] ## Thumbnails - [Object and three words](https://media-lab-seven.vercel.app/docs/thumb-object-words.md): One real object, three words that add what the title does not say, kept clear of the duration badge. [thumbnail, youtube, object, text] - [Big number thumbnail](https://media-lab-seven.vercel.app/docs/thumb-big-number.md): A huge numeral and one context word over a darkened real photo; still readable at 160 px wide. [thumbnail, number, contrast] ## Hooks - [Result-first hook title](https://media-lab-seven.vercel.app/docs/hook-title-card.md): The hook as on-screen text for the first two seconds: wrapped by hand, one box per line, upper safe zone. [hook, title, shorts, reels] - [Unfinished-number hook](https://media-lab-seven.vercel.app/docs/hook-unfinished-number.md): A countdown promise ("4 ... the last one ...") with a number badge, so viewers wait for the payoff. [hook, countdown, list] ## Captions - [Word-highlight captions](https://media-lab-seven.vercel.app/docs/caption-word-highlight.md): Three-word chunks, uppercase, the spoken word in yellow and slightly larger. The Shorts default. [captions, subtitles, ass, highlight] - [Boxed captions](https://media-lab-seven.vercel.app/docs/caption-boxed.md): Opaque box behind each chunk (ASS BorderStyle 3). The most readable choice on busy, bright footage. [captions, box, readability] - [Quiet lowercase captions](https://media-lab-seven.vercel.app/docs/caption-quiet-lowercase.md): Lowercase serif, no highlight or pop, only a soft shadow. Captions that do not shout. [captions, serif, lowercase, calm] ## Shorts - [Zero-asset voiceover Short](https://media-lab-seven.vercel.app/docs/short-word-captions.md): Script to edge-tts voice, gradient cards, word captions and a hook title. Runs with no footage at all. [short, tts, edge-tts, faceless] - [Real stills with Ken Burns](https://media-lab-seven.vercel.app/docs/short-kenburns-stills.md): One CC0 photo per beat with a slow push, voice and captions on top. Real images make it watchable. [short, ken burns, stills, photos] - [Punch-in on emphasis](https://media-lab-seven.vercel.app/docs/punch-in-zoom.md): Jump to 110% for the key line, then back. Fixed pixel sizes so the crop always fits. [zoom, edit, ffmpeg] ## Reframes - [16:9 to 9:16 with dark panels](https://media-lab-seven.vercel.app/docs/reframe-dark-panels.md): Landscape footage centred on near-black panels. Reads as intentional rather than edited. [reframe, vertical, ffmpeg] - [16:9 to 9:16 with blur fill](https://media-lab-seven.vercel.app/docs/reframe-blur-fill.md): A blurred, darkened copy fills the background behind the centred footage. [reframe, blur, vertical, ffmpeg] # Media Lab for Claude: brief -> world -> patterns Before making any content: write the brief (`/skill/playbook/01-brief.md`), match it to a row below, open the world and its proof patterns, read the listed skill pages, then build. Run the AI-look smell test (https://media-lab-seven.vercel.app/skill/playbook/taste.md) on every visual before showing it. | The brief sounds like | World | Start from | Read | |---|---|---|---| | poetry, quotes, "something reflective" for Instagram | Quiet Poetry | [poetry-film-photo](https://media-lab-seven.vercel.app/docs/poetry-film-photo.md), [poetry-paper](https://media-lab-seven.vercel.app/docs/poetry-paper.md) | [playbook/taste.md](https://media-lab-seven.vercel.app/skill/playbook/taste.md), [recipes/stock-photos.md](https://media-lab-seven.vercel.app/skill/recipes/stock-photos.md), [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md) | | launch my project / app / repo, "show what I built" | Tech Launch | [hook-title-card](https://media-lab-seven.vercel.app/docs/hook-title-card.md), [punch-in-zoom](https://media-lab-seven.vercel.app/docs/punch-in-zoom.md), [short-word-captions](https://media-lab-seven.vercel.app/docs/short-word-captions.md) | [formats/launch-video.md](https://media-lab-seven.vercel.app/skill/formats/launch-video.md), [recipes/end-to-end-short.md](https://media-lab-seven.vercel.app/skill/recipes/end-to-end-short.md) | | college fest, league, tournament, match results, awards | Campus Event | [event-result-card](https://media-lab-seven.vercel.app/docs/event-result-card.md), [event-award-card](https://media-lab-seven.vercel.app/docs/event-award-card.md), [event-schedule-story](https://media-lab-seven.vercel.app/docs/event-schedule-story.md) | [playbook/06-event-coverage.md](https://media-lab-seven.vercel.app/skill/playbook/06-event-coverage.md), [recipes/carousel.md](https://media-lab-seven.vercel.app/skill/recipes/carousel.md) | | explain a concept, tips, "how does X work" as a Short | Explainer | [hook-unfinished-number](https://media-lab-seven.vercel.app/docs/hook-unfinished-number.md), [caption-word-highlight](https://media-lab-seven.vercel.app/docs/caption-word-highlight.md) | [formats/short-video.md](https://media-lab-seven.vercel.app/skill/formats/short-video.md), [hooks/hooks.md](https://media-lab-seven.vercel.app/skill/hooks/hooks.md), [playbook/critique.md](https://media-lab-seven.vercel.app/skill/playbook/critique.md) | | turn a report, survey or numbers into a post | Data Story | [data-story-number](https://media-lab-seven.vercel.app/docs/data-story-number.md), [list-point-slide](https://media-lab-seven.vercel.app/docs/list-point-slide.md) | [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md), [recipes/carousel.md](https://media-lab-seven.vercel.app/skill/recipes/carousel.md) | | dev diary, "what I learned building", progress update | Build Log | [thumb-big-number](https://media-lab-seven.vercel.app/docs/thumb-big-number.md), [punch-in-zoom](https://media-lab-seven.vercel.app/docs/punch-in-zoom.md) | [formats/text-posts.md](https://media-lab-seven.vercel.app/skill/formats/text-posts.md), [playbook/voice.md](https://media-lab-seven.vercel.app/skill/playbook/voice.md) | | a café, shop, studio or small business posting about itself | Warm Local | [carousel-cover-promise](https://media-lab-seven.vercel.app/docs/carousel-cover-promise.md), [cta-last-slide](https://media-lab-seven.vercel.app/docs/cta-last-slide.md) | [playbook/taste.md](https://media-lab-seven.vercel.app/skill/playbook/taste.md), [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md) | | faceless channel, story Shorts, history or facts over pictures | Faceless Story | [short-kenburns-stills](https://media-lab-seven.vercel.app/docs/short-kenburns-stills.md), [hook-title-card](https://media-lab-seven.vercel.app/docs/hook-title-card.md) | [recipes/end-to-end-short.md](https://media-lab-seven.vercel.app/skill/recipes/end-to-end-short.md), [recipes/stock-photos.md](https://media-lab-seven.vercel.app/skill/recipes/stock-photos.md) | | YouTube thumbnail | Explainer | [thumb-object-words](https://media-lab-seven.vercel.app/docs/thumb-object-words.md), [thumb-big-number](https://media-lab-seven.vercel.app/docs/thumb-big-number.md) | [formats/thumbnail.md](https://media-lab-seven.vercel.app/skill/formats/thumbnail.md), [playbook/taste.md](https://media-lab-seven.vercel.app/skill/playbook/taste.md) | | repurpose a landscape video for Reels / Shorts | Explainer | [reframe-dark-panels](https://media-lab-seven.vercel.app/docs/reframe-dark-panels.md), [reframe-blur-fill](https://media-lab-seven.vercel.app/docs/reframe-blur-fill.md) | [recipes/ffmpeg.md](https://media-lab-seven.vercel.app/skill/recipes/ffmpeg.md), [formats/specs.md](https://media-lab-seven.vercel.app/skill/formats/specs.md) | ## Worlds ### Quiet Poetry (`quiet-poetry`) *Like a page torn from someone's notebook and left on a train seat.* - Type: One serif (Garamond), regular weight, sentence case, two sizes at most. - Colour: Taken from the photo; off-white or ink text. - Imagery: Real CC0 photos with one shared film grade; paper slides between them. - Motion: None, or a very slow push. - Rules: Text sits in the photo's darkest or calmest edge; One stanza or thought per slide; Concrete nouns, not "soul" and "journey" - Avoid: Gold italic on black; Glows, moons, bokeh blobs; Centred quote boxes - Proof: [poetry-film-photo](https://media-lab-seven.vercel.app/docs/poetry-film-photo.md), [poetry-paper](https://media-lab-seven.vercel.app/docs/poetry-paper.md), [caption-quiet-lowercase](https://media-lab-seven.vercel.app/docs/caption-quiet-lowercase.md) ### Tech Launch (`tech-launch`) *A demo that works, shown fast, with one number that matters.* - Type: A plain grotesk (Segoe/Inter-like), bold for the hook, regular for support. - Colour: Product colours; one accent. - Imagery: Real UI recorded at 2x, zoomed on the cursor; desk photos only as context. - Motion: Punch-ins on each step, cut on the beat. - Rules: Result in the first 2 seconds; Every claim shown on screen; Name and where to get it at the end - Avoid: Mockup devices floating in gradients; Stock "futuristic" imagery - Proof: [hook-title-card](https://media-lab-seven.vercel.app/docs/hook-title-card.md), [punch-in-zoom](https://media-lab-seven.vercel.app/docs/punch-in-zoom.md), [thumb-big-number](https://media-lab-seven.vercel.app/docs/thumb-big-number.md), [short-word-captions](https://media-lab-seven.vercel.app/docs/short-word-captions.md) ### Campus Event (`campus-event`) *The league's own page: consistent, current, a bit proud.* - Type: A condensed face for scores and days, a plain sans for everything else. - Colour: The event's colours, used the same way on every post. - Imagery: Real photos from the day; faces and hands never cropped. - Motion: Highlight reels: best moment first, fast cuts. - Rules: Data (scores, fixtures) comes from the official source; One template set for the whole event; Names checked before export - Avoid: Pasting patches over old poster text; Stretched photos - Proof: [event-result-card](https://media-lab-seven.vercel.app/docs/event-result-card.md), [event-award-card](https://media-lab-seven.vercel.app/docs/event-award-card.md), [event-schedule-story](https://media-lab-seven.vercel.app/docs/event-schedule-story.md) ### Explainer (`explainer`) *A friend who knows the topic, explaining it on a napkin.* - Type: Bold sans hooks, readable captions. - Colour: Neutral base, one highlight colour for the key word. - Imagery: Diagrams, screen recordings or real stills per beat. - Motion: Something changes every 2-4 s. - Rules: One idea per Short; Facts with sources; opinions labelled; Payoff in the last beat - Avoid: Stock footage that only illustrates nouns; Unsourced stats - Proof: [hook-unfinished-number](https://media-lab-seven.vercel.app/docs/hook-unfinished-number.md), [caption-word-highlight](https://media-lab-seven.vercel.app/docs/caption-word-highlight.md), [short-kenburns-stills](https://media-lab-seven.vercel.app/docs/short-kenburns-stills.md) ### Data Story (`data-story`) *A poster in a good newspaper: one figure, said plainly.* - Type: Condensed numerals, plain sans text, a ruled grid. - Colour: Paper, ink, one accent for the number. - Imagery: Mostly none; the number is the image. - Motion: Count-up only if it adds meaning. - Rules: One giant number per slide, earned; Say what it means in one sentence; Source on the slide - Avoid: 3D charts; Five numbers fighting on one slide - Proof: [data-story-number](https://media-lab-seven.vercel.app/docs/data-story-number.md), [thumb-big-number](https://media-lab-seven.vercel.app/docs/thumb-big-number.md), [list-point-slide](https://media-lab-seven.vercel.app/docs/list-point-slide.md) ### Build Log (`build-log`) *Notes from someone actually making the thing.* - Type: Plain sans; code in a mono face only when showing code. - Colour: Muted, from the screen and the desk. - Imagery: Real desk and screen shots, including the mess. - Motion: Screen recordings with punch-ins. - Rules: Concrete numbers: hours, files, errors; Show the failure before the fix; Sentence-case captions - Avoid: Hustle-culture captions; Perfect staged desks - Proof: [thumb-big-number](https://media-lab-seven.vercel.app/docs/thumb-big-number.md), [hook-unfinished-number](https://media-lab-seven.vercel.app/docs/hook-unfinished-number.md), [punch-in-zoom](https://media-lab-seven.vercel.app/docs/punch-in-zoom.md) ### Warm Local (`warm-local`) *The café or shop posting about its own week.* - Type: A friendly serif or rounded sans; one face. - Colour: Warm, from the place itself. - Imagery: Real photos of the place, people (with permission) and products; natural light. - Motion: Slow stills, ambient sound. - Rules: Useful info on every post (hours, price, date); Photos taken there, not stock when possible - Avoid: Stock smiling people; Neon gradients - Proof: [carousel-cover-promise](https://media-lab-seven.vercel.app/docs/carousel-cover-promise.md), [cta-last-slide](https://media-lab-seven.vercel.app/docs/cta-last-slide.md), [short-kenburns-stills](https://media-lab-seven.vercel.app/docs/short-kenburns-stills.md) ### Faceless Story (`faceless-story`) *A story told over pictures, held together by captions.* - Type: Bold captions, highlighted word, hook title in the first 2 s. - Colour: Graded stills; captions in white and one highlight. - Imagery: Real stills or footage per beat; never another creator's clips. - Motion: Ken Burns on every still, cut per beat. - Rules: Hook is an unresolved situation; Loop the ending into the start; Disclose synthetic voice - Avoid: Re-uploaded clips; Generated faces of real people - Proof: [short-kenburns-stills](https://media-lab-seven.vercel.app/docs/short-kenburns-stills.md), [hook-title-card](https://media-lab-seven.vercel.app/docs/hook-title-card.md), [caption-boxed](https://media-lab-seven.vercel.app/docs/caption-boxed.md), [reframe-dark-panels](https://media-lab-seven.vercel.app/docs/reframe-dark-panels.md) ## Refuse or ask first - Re-uploading or recreating another creator's video, script, thumbnail or art - Real people's faces or names in demos (use fictional ones) or implying an endorsement - Stating numbers without a source - Posting or uploading anything without the owner's explicit go-ahead ===== plugins/media-lab/skills/media-lab/SKILL.md ===== --- name: media-lab description: Use whenever you make social or video content - YouTube Shorts, Instagram Reels, TikToks, carousels, LinkedIn or X posts, threads, launch or promo videos, thumbnails, captions, scripts, hooks, event posters/stories - or edit, caption, reframe or render video with ffmpeg, MoviePy or Remotion. Gives the brief-to-publish procedure, format specs and safe zones, a hook library, a critique rubric and tested tooling recipes. Not for website UI (use ui-lab). --- # Media Lab: content with judgement, not templates Media Lab is a reference library for content creation (GitHub `HarshSalunkhe2005/Media-Lab`). This skill folder holds everything an agent needs; the repo root adds `research/` (sources and evidence levels), `patterns/` (rendered examples) and the browsable site. ## Procedure (do not skip steps) 1. **Brief** (`playbook/01-brief.md`): audience, one idea, platform, goal metric, voice. Write it down first. 2. **Package first** (`playbook/02-package.md`): title, hook line, thumbnail or cover idea before any production. 3. **Script** (`playbook/03-script.md`): pick a hook from `hooks/hooks.md`, follow the format file, then run the critique rubric (`playbook/critique.md`). Revise until it scores "ship". Kill weak ideas instead of polishing them. 4. **Assets and edit** (`playbook/04-edit.md`): pick the tool with `recipes/choose-a-tool.md`, then follow `recipes/ffmpeg.md`, `recipes/moviepy.md` or `recipes/remotion.md`. 5. **Verify** (`playbook/05-verify.md`): watch the output, check safe zones, captions, audio levels, length, file specs. For anything visual, also run the AI-look smell test in `playbook/taste.md`. 6. **Publish and learn**: caption and post copy go through the `humanize` skill; log one metric to compare next time. ## Worlds and rendered patterns The site (repo root, `/for-claude.md` live) routes a brief to one of eight **worlds** (Quiet Poetry, Tech Launch, Campus Event, Explainer, Data Story, Build Log, Warm Local, Faceless Story) and to rendered **patterns** (`patterns/src/.py`, previews in `public/previews/`). Pick a world first, start from its proof patterns, and render with `python patterns/render.py `. ## Read these files for the task (routing) Always: `playbook/01-brief.md` first, `playbook/05-verify.md` last. Not sure? Use `recipes/by-goal.md`. | Task | Read | |---|---| | Short / Reel / TikTok script | `formats/short-video.md`, `hooks/hooks.md`, `playbook/03-script.md`, `playbook/critique.md` | | Render a Short from a script (free tools) | `recipes/end-to-end-short.md` → `scripts/make_short.py` | | Edit real footage (cut, reframe, loudness, captions) | `playbook/04-edit.md`, `recipes/ffmpeg.md`, `formats/specs.md` | | Python-driven edit | `recipes/moviepy.md` → `scripts/moviepy_demo.py` | | Animated type / UI mockups | `recipes/remotion.md` | | Carousel or LinkedIn document | `playbook/taste.md` FIRST, then `formats/carousel.md`, `recipes/stock-photos.md`, `recipes/carousel.md`, `hooks/templates.md` | | Quote / poetry post | `playbook/taste.md` FIRST, then `recipes/stock-photos.md` → `scripts/photo_slide.py` | | LinkedIn / X / thread / launch post | `formats/text-posts.md`, `playbook/voice.md`, then `humanize` | | Launch video for a project | `formats/launch-video.md`, `recipes/by-goal.md` row 1 | | Thumbnail, cover, poster text | `playbook/taste.md`, `formats/thumbnail.md` (incl. overlay checklist), `recipes/carousel.md` Option B | | Free real photos (legal sourcing) | `recipes/stock-photos.md` | | Event coverage / sorting event media | `playbook/06-event-coverage.md` | | Packaging (title, hook, cover) | `playbook/02-package.md` | | Captions only | `scripts/captions.py`, `formats/specs.md` safe zones | ## Hard rules - Original content only. Never clip, re-upload or recreate another creator's video, script, thumbnail or art. Study techniques, rebuild in our own words. - Demos and examples use fictional names and brands. - Never state a number as fact unless it comes from a source you can cite. Vendor "stats" are hypotheses. - AI-generated or synthetic media is disclosed where the platform asks (YouTube's altered/synthetic flag). - Anything posted under Harsh's name goes through `humanize`. No AI tells, no "AI made this" credits in his work. - Do not publish, post or upload without Harsh's explicit go-ahead for that piece. ===== plugins/media-lab/skills/media-lab/formats/carousel.md ===== # Carousel (Instagram, LinkedIn document) | Slide | Job | |---|---| | 1 | Promise. Big headline, max ~8 words, plus a visual cue that there is more (arrow, "1/7"). | | 2 | Second hook. The feed may re-show the post starting here, so it must also stand alone. | | 3 to n-1 | One idea per slide. Same layout every slide; headline + 1-3 short lines or one visual. | | mid | Soft CTA ("save this for later") somewhere around the middle. | | last | Clear CTA tied to the goal metric: save, send to a friend who..., comment a specific answer, follow for part 2. | Design rules: - Same size for every slide (4:5 default). Margins at least 80 px. - Large type: headline 64-90 px, body 36-44 px at 1080 width. - One accent colour; numbers and key words in the accent. - Put the handle or a small logo in the same corner on every slide. - Export PNG for Instagram, a single PDF for LinkedIn. Build: see `recipes/carousel.md` (HTML/CSS to PNG/PDF, or Pillow). ===== plugins/media-lab/skills/media-lab/formats/launch-video.md ===== # Launch / promo video (product, app, project) Length: 20-45 s vertical for social, 60-90 s 16:9 for a README or landing page. | Beat | Time (30 s cut) | Content | |---|---|---| | Hook | 0-3 s | the result or the "wow" screen first, with a 4-6 word headline | | Problem | 3-8 s | one line of the pain, shown not told | | Demo | 8-22 s | 3 fast moments of the product doing the thing; zoom into the cursor/UI; captions name each step | | Proof | 22-26 s | a number, a quote (real, with permission), or a before/after | | CTA | 26-30 s | name + where to get it, on screen long enough to read | Tips: - Record UI at 2x resolution, then crop/zoom in the edit; static full-screen UI is unreadable on phones. - Use the product's real colours and type in the titles. - Music with a clear beat; cut on the beat. - If the `brag` / `brag-slim` skill is installed, it can auto-build a launch video from a repo or URL. Use this file to brief it and to review its output. - Tools: Remotion for animated UI mockups and kinetic type; ffmpeg for screen-recording edits (`recipes/`). ===== plugins/media-lab/skills/media-lab/formats/short-video.md ===== # Short vertical video (Shorts, Reels, TikTok) ## Structures that work (pick one per piece) | Structure | Beats | Good for | |---|---|---| | Problem → twist → fix | hook names a pain, show the wrong way, reveal the right way | tips, dev tricks | | Countdown list | "3 ..." then 3, 2, 1, best last | tools, facts (the countdown itself holds attention) | | Before → after | show result first, rewind to how | builds, edits, transformations | | Myth bust | state the common belief, prove it wrong on screen | science, finance, tech | | Story in 30 s | situation, tension, turn, punchline | faceless story channels, event recaps | | Build log | "I built X in N hours", 3 milestones, demo | dev/builder accounts, launches | | Event recap | best moment first, 4-6 fast highlights, result card | sports, fests | ## Production checklist - Hook spoken + on screen in 2 s; strongest shot first. - Visual change every 2-4 s. - Word-level captions, 1-3 words per block, current word highlighted, inside safe zone. - Loud enough, music ducked under voice. - Ending loops to the start or asks one specific question. - Description: one line restating the promise, 1-3 specific hashtags. - Synthetic/AI visuals or voice: set the platform's disclosure flag. ===== plugins/media-lab/skills/media-lab/formats/specs.md ===== # Format specs Platforms change these often. Check the platform's help page before a big launch; update this file when they change. | Format | Canvas | Length | Notes | |---|---|---|---| | YouTube Shorts | 1080x1920, 9:16, 30 or 60 fps | up to 3 min; sweet spot often 25-45 s (weak evidence) | Title matters; first frame often shown as cover | | Instagram Reels | 1080x1920, 9:16 | up to 3 min in-app; keep most under 60 s | Cover is cropped to 4:5 (1080x1350) in the profile grid | | TikTok | 1080x1920, 9:16 | 15 s to 10 min | Captions and right-side buttons cover a lot of frame | | IG carousel | 1080x1350 (4:5) or 1080x1440 (3:4); up to 20 slides | 6-10 slides typical | Every slide same size; slide 2 is a second hook | | LinkedIn document | PDF, 1080x1350 or 1080x1080 pages | 5-10 pages | Upload as document; title shows on the cover | | LinkedIn / X image | 1200x627 (link-style) or 1080x1350 | | X crops tall images in the feed preview | | YouTube thumbnail | 1280x720, 16:9, under 2 MB | | Readable at 160 px wide; avoid bottom-right (duration badge) | | YouTube long | 1920x1080 or 3840x2160 | | Chapters in description | | IG/WhatsApp story | 1080x1920 | 15 s per frame (IG) | Top 250 px and bottom 300 px get covered | ## Safe zones for 9:16 (1080x1920) Keep captions and key text inside **x 90-990, y 250-1500**. Avoid: - top ~250 px: account name, progress bar, search - bottom ~420 px: caption text, audio name, buttons (TikTok worst, Shorts similar) - right ~120 px: like/comment/share column A caption baseline around y = 1250-1400 sits above the UI on all three apps. ## Export settings that upload cleanly `-c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p -r 30 -c:a aac -b:a 192k -ar 48000 -movflags +faststart` ===== plugins/media-lab/skills/media-lab/formats/text-posts.md ===== # Text posts: LinkedIn, X, threads ## LinkedIn post ``` Line 1-2: concrete result or moment (a number, a name, a thing that happened) (blank line) 3-6 short paragraphs, 1-2 lines each: what happened, what was hard, what changed One specific takeaway, said once A real question, or nothing ``` - Under ~1,300 characters usually; shorter is fine. - Native media (image, short video, PDF document) beats a link. Link in the first comment. - Tag only people who were actually involved. 0-3 hashtags. - Reply to early comments; the thread is part of the post. ## X / thread - Tweet 1 stands alone and carries the whole promise. - One point per tweet; numbers like 1/ are optional. - Visual every 2-3 tweets if you have them. - Last tweet: the summary or the ask; link in a reply. ## Launch post (product, project, event) 1. What it is in one plain sentence. 2. Who it's for and the one thing it does better. 3. Proof: demo clip, screenshot, number. 4. How to try it (link in comment on LinkedIn). 5. Thanks to real people involved, by name, only with their OK. ===== plugins/media-lab/skills/media-lab/formats/thumbnail.md ===== # Thumbnails and covers - 1280x720 for YouTube; Reels cover designed at 1080x1920 with the centre 1080x1350 holding everything important. - One subject, one idea. A face with a clear emotion, or one object/result shown large. - 0-4 words of text. The text adds what the title doesn't say; it never repeats the title. - High contrast between subject and background; test in greyscale. - Test at 160 px wide (`ffmpeg -i thumb.png -vf scale=160:-1 small.png`). If you can't tell what it is, redo it. - Keep clear of the bottom-right corner (duration badge). - Make 2-3 genuinely different variants and use YouTube Test & Compare; it picks by watch-time share. Give each test enough impressions before deciding. - Never use a real person's face or a brand's logo without rights; never imply an endorsement. ## Text overlay checklist (thumbnails, covers, posters, on-video titles) - [ ] 0-4 words on thumbnails, max ~6 words for an on-video hook title, one line per idea - [ ] Font: one bold display face, real font file bundled (fallback fonts change width and break layout) - [ ] Size: thumbnail text readable at 160 px wide; 9:16 titles at least 64 px on a 1080 canvas - [ ] Contrast: outline, shadow or a semi-opaque box behind text on busy images (check in greyscale) - [ ] Placement: inside the safe zone (`specs.md`); nothing in the bottom-right on YouTube; nothing under the like/comment column on vertical - [ ] Wrap long lines yourself (about 20 characters per line at 72 px on 1080 wide), one draw call per line in ffmpeg - [ ] No text over faces or the ball/subject - [ ] Spelling of names, teams, scores and dates checked against the source - [ ] Same corner and style for the handle/logo across a series - [ ] Looks native: matches the template's existing type and colours, not a pasted patch ===== plugins/media-lab/skills/media-lab/hooks/hooks.md ===== # Hook library Hooks are patterns, not lines to paste. Fill them with the real specifics of your piece; a generic fill is worse than no hook. Examples use fictional names. ## Opening hooks (first 2 seconds) | Pattern | Shape | Example (fictional) | |---|---|---| | Result first | "[specific result] in [short time/with constraint]" | "This app found 212 duplicate photos in 9 seconds." | | Unfinished number | "[N] [things], and the last one [surprise]" | "4 ffmpeg flags I use daily. The last one saved a whole edit." | | Contrarian | "[common belief] is wrong. Here's what [proof]." | "Longer captions don't hurt Reels. Our test said the opposite." | | Mid-action | start inside the moment, no setup | "...and that's when the score went to 24-24." | | Visible stakes | show what could go wrong | "One wrong command here deletes the whole drive." | | Specific question | a question only the target viewer cares about | "Why does WhatsApp change your photo dates?" | | Before/after flash | show the after for 1 s, then "here's how" | | | Direct callout | "If you [specific situation], watch this." | "If you run your college fest's Instagram, watch this." | | Mistake reveal | "I did [X] wrong for [time]." | "I sorted 600 event photos by the wrong date." | | Proof object | hold up / show the thing | a printed poster, a trophy, a terminal output | Avoid: "Hey guys", "In this video", "Did you know" (overused), logo stings, any hook the video doesn't pay off. ## Second hooks (around 40-60% through) - "But that's not the weird part." - A new number or a reversal. - Switch the visual style for one beat (zoom, colour change, new location). ## Endings | Ending | Use when | |---|---| | Loop line | the last sentence leads naturally into the first (Shorts rewatches) | | Specific question | goal is comments; ask something with a short, real answer | | Save prompt | list or reference content ("save this for your next edit") | | Send prompt | "send this to the friend who [specific behaviour]" | | Part 2 tease | only if part 2 actually exists or is scheduled | ## More opening hooks (fictional examples) | Pattern | Shape | Example (fictional) | |---|---|---| | Time stamp | "It's [time] and [situation]." | "It's 2 AM and the final starts in nine hours." | | Price / cost reveal | "This cost [small amount]. Here's what it replaced." | "This poster template cost nothing. It replaced four hours a day." | | Wrong answer first | show the common attempt failing, then "do this instead" | a caption overflowing the frame, then the fix | | Comparison | "[A] vs [B] in [N] seconds." | "Blur fill vs dark panels: which looks less edited?" | | Rule of N | "The [N]-second rule for [thing]." | "The 2-second rule for every Short." | | Countdown stakes | "[N] [units] left to [goal]." | "Three sets left, and Ridgeway are down two." | | Insider detail | "Nobody tells you [specific thing] about [topic]." | "Nobody tells you WhatsApp rewrites your photo dates." | | Number in the frame | open on a large number, explain it | "212" on screen, then "duplicates, in one folder" | | Tiny demo | 1 s of the result working, no words, then the title | the app tagging photos live | | Reaction cut | open on a crowd/player reaction, then rewind to the play | | | Challenge | "Can [we/I] [do X] in [constraint]?" | "Can we edit a whole fest recap in one hour?" | | Myth label | on-screen "MYTH:" then the belief | "MYTH: carousels are dead." | ## Caption and post templates (fictional) | Use | Template | Example | |---|---|---| | Short description | [promise restated] + [one detail] + 1-3 tags | "Sorting 600 fest photos without losing one. #eventmedia" | | Event result | [Team] [score] [Team]. [one moment]. [next fixture] | "Ridgeway 3-2 Northfield. Saved two match points in set five. Final is Sunday, 6:30 PM." | | Award post | [Award]: [Name] ([Team]). [one stat or moment] | "Best Setter: Kavya Rao (Harbour FC). 41 assists across the league." | | Launch caption | [what it does, plainly]. [who it's for]. [where] | "Pebble sorts event photos by real capture date. For club media teams. Link in comments." | | Carousel caption | [slide-1 promise in other words]. Save it for [moment]. | "Five checks before you post any Reel. Save it for your next upload." | | LinkedIn opener | [specific result] + [time/constraint] | "We sorted 730 event files across five days and lost none of them." | | Recap story | Day [N] in [number]s: [stat], [stat], [stat] | "Day 3 in numbers: 8 matches, 21 sets, 1 upset." | | Comment prompt | ask for a short, specific answer | "Which set was the best of the day: 2 or 5?" | ===== plugins/media-lab/skills/media-lab/hooks/templates.md ===== # Fill-in templates ## 30 s Short script ``` HOOK (0-2s) spoken: ______________________ on screen: ______ (<=6 words) SETUP (2-8s) the situation / problem in one line: __________ BEAT 1 (8-15s) ________ visual: ________ BEAT 2 (15-22s) ________ visual: ________ (second hook here) PAYOFF (22-28s) ________ visual: strongest proof END (28-30s) loop line / question: ________ Description: ________ Hashtags (1-3): ________ Disclosure needed: yes / no ``` ## Carousel (7 slides) ``` 1 Promise: ________________ (<=8 words) 2 Second hook: ________________ 3 Point 1: headline ______ line ______ 4 Point 2: headline ______ line ______ 5 Point 3: headline ______ line ______ + soft CTA 6 Summary/proof: ______________ 7 CTA: save / send / comment ______ Caption line 1: ________________ ``` ## LinkedIn post ``` [Concrete result or moment, one line] [What happened, 1-2 lines] [The hard part, 1-2 lines] [What changed / the number, 1-2 lines] [One takeaway, once] [Optional real question] -- first comment: link ``` ## Event result card (data file per match) ``` event, stage, date, team_a, score_a, team_b, score_b, sets, player_of_match, photo, tagline ``` ===== plugins/media-lab/skills/media-lab/playbook/01-brief.md ===== # 1. Brief No asset gets made before this exists. Five minutes here saves the whole edit. ``` Piece: Platform: Audience: who exactly, and what they already know One idea: the single thing they should remember (one sentence, no "and") Why now: trend, event, launch, question people keep asking Goal metric: views from non-followers | saves | sends | comments | clicks | signups Voice: Harsh personal | brand/team account (name it) | faceless channel persona Proof: what we can show on screen (footage, numbers, demo, screenshots) and the sources for any facts Constraints: length, deadline, assets we have, things we must not say Done means: e.g. "60 s vertical MP4 + cover + caption, reviewed by Harsh" ``` Checks before moving on: - If the one idea needs "and", it is two pieces. - If there is nothing to show (no proof), the piece will be talk over stock. Find proof or change the idea. - The goal metric picks the format: reach from strangers favours Shorts/Reels; saves favour carousels and lists; comments favour opinions and questions; clicks favour LinkedIn documents and threads with the link in a reply. ===== plugins/media-lab/skills/media-lab/playbook/02-package.md ===== # 2. Package before you produce The packaging is the promise. Write it first, then make the piece keep that promise. For every piece, write three to five options of each, then pick: | Platform | Packaging parts | |---|---| | Shorts / Reels / TikTok | first spoken line, first on-screen text (max ~6 words), cover frame, title (Shorts) | | Carousel | slide 1 headline, slide 2 second hook, caption first line | | LinkedIn / X | first two lines (everything before "see more"), document cover if any | | YouTube long | title + thumbnail as one idea; the thumbnail shows what the title does not say | | Event poster / story | headline, the one number or name that matters, date | Rules: - Title and thumbnail should not repeat each other word for word. - Concrete beats clever: a number, a name, a visible result. - Never promise what the piece does not deliver. Clickbait raises clicks and kills retention. - Read it at phone size. A thumbnail must work at about 160 px wide; a cover must survive the UI overlays (see `formats/specs.md`). ===== plugins/media-lab/skills/media-lab/playbook/03-script.md ===== # 3. Script Write for the ear and the thumb: short sentences, one beat per line, a visual for every line. ## Short vertical (under 60 s) - Line 1 is the hook: under 12 words, lands in under 2 seconds, opens a gap. No intro, no greeting, start mid-action. - 95 to 135 spoken words for ~45 to 55 s at a brisk TTS pace; 70 to 90 words for ~30 s. - Change something on screen every 2 to 4 seconds (cut, zoom, new caption block, overlay). - Payoff in the last beat. End on a line that loops into the start or invites a comment. - Facts only from cited sources; opinions are framed as the take. ## Script table (fill this, it doubles as the edit list) | # | Time | Spoken line | On-screen text | Visual | Sound | |---|---|---|---|---|---| | 1 | 0-2 s | hook | hook text (<= 6 words) | strongest shot | hit / riser | | 2 | ... | | | | | ## Then critique Run `critique.md`. Only "ship" goes to production. Two revise rounds max; if it still fails, kill or change the idea. ===== plugins/media-lab/skills/media-lab/playbook/04-edit.md ===== # 4. Assets and edit ## Order of operations (talking head or screen recording) 1. Cut silences and false starts. 2. Clean the voice (denoise, loudness normalise to about -14 LUFS for social). 3. Lay the A-roll, then B-roll or overlays on every claim. 4. Punch-in zooms (105 to 115%) on emphasis, max every few seconds. 5. Captions from word timestamps (see `recipes/ffmpeg.md#captions`). 6. Music bed at -20 to -25 dB under voice; sound hits on cuts, sparingly. 7. Reframe/export per platform (`formats/specs.md`). ## Order of operations (generated / faceless) 1. Voice first (TTS with word timestamps), because it sets the timing. 2. One visual per beat in a single consistent style; no text or logos inside generated images; no real faces. 3. Motion on stills (slow Ken Burns, alternate directions). 4. Captions, music, export. ## Asset rules - Use footage you shot, licensed stock, or generated visuals. Note the licence of every third-party asset in the job folder. - Fonts: bundle the font file with the job (captions break silently when the font is missing). - Keep a job folder per piece: `brief.md`, `script.md`, `assets/`, `out/`, `notes.md` (what you'd change next time). ===== plugins/media-lab/skills/media-lab/playbook/05-verify.md ===== # 5. Verify (bounded, every time) Watch the exported file, not the timeline. Then check: - [ ] Specs: resolution, fps, aspect, length within the platform limit (`ffprobe -v error -show_entries stream=width,height,r_frame_rate:format=duration out.mp4`) - [ ] First frame works as a cover (many feeds show it before play) - [ ] Hook is spoken AND on screen within 2 s - [ ] Captions: no word hidden under platform UI (safe zones), no line wider than ~80% of frame, no misspelled names - [ ] Audio: voice clearly above music, no clipping, loudness about -14 LUFS (`ffmpeg -i out.mp4 -af loudnorm=print_format=summary -f null -`) - [ ] No black frames or frozen frames at cuts - [ ] Nothing copyrighted or third-party you do not have rights to; names in demos are fictional - [ ] Post copy humanized; facts sourced - [ ] Harsh approved before anything is posted Grab proof frames: `ffmpeg -ss 1 -i out.mp4 -frames:v 1 check_1s.jpg` (repeat at a few timestamps) and look at them. ## Known bugs already solved - **Captions missing in render**: ffmpeg `subtitles=` filter on Windows chokes on absolute paths with `C:`. Run ffmpeg with `cwd` set to the job folder and pass a relative path; pass `fontsdir=` too. - **Zoompan jitter**: render stills at 2x output size before `zoompan`, then scale down. - **WhatsApp-sent media has no camera date**: the send time is not the capture time; sends before ~4 AM belong to the previous day. - **Duplicate detection by video length alone is wrong**: many different clips share a length. Compare frames, or use file checksums. - **drawtext box glyphs at line breaks**: a multi-line `textfile` shows a box at each break (ffmpeg 8, Windows). Draw one `drawtext` per line. - **drawtext uses a monospace fallback / "Fontconfig error"**: Windows ffmpeg has no fontconfig. Pass `fontfile='C\:/Windows/Fonts/arialbd.ttf'` (escape the colon). - **edge-tts voice comes out around -20 LUFS**: add `loudnorm=I=-14:TP=-1.5:LRA=11` on the final mux. - **Old MoviePy installed globally**: MoviePy 2 code fails on 0.2/1.x. Use a venv with `moviepy>=2,<3`. ===== plugins/media-lab/skills/media-lab/playbook/06-event-coverage.md ===== # Event coverage (college fests, leagues, launches) Learned running media for a college sports league. Applies to any multi-day event. ## Before the event - Lock a template set early: schedule story, standings, match result, player award, champions/runners-up, recap. Same fonts, palette, safe zones and date format across all of them. - Build templates as code (Pillow/HTML) with a data file per day, so updates are one command, not a redesign. - Agree on the source of truth for results (official site or sheet) and pull from it, never from memory. ## Templates that look native, not edited - When re-using a previous poster as a template, rebuild text layers in the original font and fill; never paste a patch box over old text. - Photos: frame faces with headroom; never crop hands or heads; remove distracting background people only when it looks untouched. - Do not stretch photos to fill. If the aspect doesn't fit, use dark side panels in the template's style, not blur. - Check date alignment, spelling of names and team names against the source before export. ## During the event - One shared drop folder per day; ask shooters to upload originals (not WhatsApp-compressed) when possible. - Post the schedule and standings stories at the same time each day. ## After: sorting the media - Date by camera EXIF or camera filename first; WhatsApp send time only as a fallback (before ~4 AM counts as the previous day). - Dedupe in this order: exact checksum (safe), then perceptual image hash with a tight threshold, then frame comparison for video. Never dedupe videos by duration alone. Anything uncertain goes to a `not_sure/` folder for a human, never silently dropped. - Stage in your own drive first, verify counts per folder, then copy (not move) to the shared final drive. ===== plugins/media-lab/skills/media-lab/playbook/critique.md ===== # Critique rubric Score honestly, 0 to 10, and list concrete fixes for anything under 7. Pretend you are a tired viewer with the thumb already moving. | Axis | 10 looks like | Fails when | |---|---|---| | Hook | I need the next 3 seconds | greeting, logo, context before the point | | Clarity | one idea, said plainly | two ideas, jargon, vague nouns | | Retention | something changes or escalates every few seconds | a static stretch over 5 s, a list with no countdown | | Payoff | the promise is kept, ideally with a twist | answer withheld, or obvious | | Proof | claims shown or cited | "studies show", unsourced numbers | | Originality | our angle, our footage or our words | rehash of a viral video, borrowed script | | Voice | sounds like the named person or account | AI tells (see `voice.md`) | | Packaging fit | piece delivers exactly what title/cover promise | bait and switch | Verdict: - **ship**: Hook >= 7, Retention >= 7, Originality >= 6, Proof >= 6, no policy or accuracy risk - **kill**: unsafe, defamatory, cannot be made original, or no proof exists - **revise**: everything else (max two rounds) ===== plugins/media-lab/skills/media-lab/playbook/taste.md ===== # Taste: reads as AI vs reads as human Read this before any carousel, quote or poetry post, thumbnail, poster or cover. Structure alone isn't enough: a correct layout with generated glows still looks machine-made, and people scroll past it. ## Do / don't | Reads as AI (don't) | Reads as human (do) | |---|---| | Procedural gradients, glows, bokeh blobs, fake moons and stars | A real photo or a real texture (paper, film grain, fabric, a wall) from `recipes/stock-photos.md` | | Gold italic serif on a black or navy gradient | Off-white or ink-dark text in one plain typeface, colour taken from the photo | | Two or three typefaces, drop shadows, outer glow, ornaments, dividers, flourishes | One typeface, two sizes at most, no effects. Use a shadow only if the text is unreadable without it. | | Text dead-centre on every slide | Text sits in the photo's negative space (sky, wall, table, water) and moves with it | | Perfectly centred, symmetrical subject | Real framing: the subject off-centre, a crop that cuts something, a slight tilt left as shot | | Title Case Headlines Everywhere | Sentence case, like someone typed it. Poetry keeps its own line breaks. | | Every slide uses the same template | A deliberate system with variation: full-bleed photo, then text on paper, then photo again | | Over-smooth, saturated, HDR-looking images | Natural colour, some grain, a shadow that is actually dark | | Emoji bullets, sparkle icons, "✨" accents | No decoration. If something needs emphasis, use the words. | | Filler lines that rhyme by force ("journey", "soul", "embrace") | Specific concrete nouns: the bus stop, the chai cup, 6:40 PM | ## Quote and poetry posts - One poem or quote per post, or one stanza per slide. Credit the author by name if it isn't Harsh's. - Line length: break where a person would pause, not where the box ends. - Match the photo to the feeling, not to the literal word (a poem about waiting gets an empty platform, not a clock). ## Thumbnails and covers - A real face, object or moment, photographed. Generated "epic" scenes read as fake at thumbnail size. - Short words in a bold, plain sans serif. Never neon outlines or chrome text. ## AI-look smell test (run before showing anything; all six must pass) 1. **Source**: is the background a real photo or texture, not a generated gradient or blob? If generated, would a person believe it was shot? 2. **Restraint**: one typeface, no glows, shadows, ornaments or sparkles (or a stated reason for each)? 3. **Placement**: does the text sit in the image's negative space, avoiding faces and the subject? 4. **Cliché**: none of these: gold on black, purple-blue gradients, lens-flare moons, centred italic serif quote boxes? 5. **Voice**: do the words read like a person (sentence case, concrete nouns, no filler rhyme)? Run `humanize` on captions. 6. **Variation**: across the set, at least two layout variants, and nothing that looks like one template stamped five times? If any point fails, fix it before showing Harsh. Say which point you fixed. ===== plugins/media-lab/skills/media-lab/playbook/voice.md ===== # Voice **For anything posted under Harsh's name, run the global `humanize` skill on the final copy.** That skill is the source of truth for his voice and the AI-tell blacklist; this file only adds platform specifics. Platform notes: - **Spoken scripts**: contractions, short sentences, words people actually say. Read it out loud once; cut anything you stumble on. - **Short-video captions/descriptions**: first line repeats the promise in different words; 1 to 3 hashtags max, specific ones. - **LinkedIn**: first line is a concrete result or a specific moment, not a lesson. Short lines, white space, no emoji bullets, no "Here's what I learned:" scaffolding. Link goes in the first comment. - **X/threads**: tweet 1 must stand alone; each tweet one point; last tweet sums up or asks. - **Team/brand accounts** (e.g. a college league page): follow that account's existing tone and templates; no personal "I". Never: fake urgency, invented stats, engagement bait ("comment YES"), fake testimonials, impersonating a real person or brand. ===== plugins/media-lab/skills/media-lab/recipes/by-goal.md ===== # Recipes by goal Pick the goal, read the files in order. Every route starts with `playbook/01-brief.md` and ends with `playbook/05-verify.md`. | Goal | Format | Hooks | Build with | Notes | |---|---|---|---|---| | **Turn a project into a 30 s launch Short** | `formats/launch-video.md`, `formats/short-video.md` | Result first, Before/after flash, Build-log hooks (`hooks/hooks.md`) | `recipes/end-to-end-short.md` with screen-recording stills, or `recipes/ffmpeg.md` on real footage | Record UI at 2x, zoom on the cursor. `brag` skill can draft it; review against launch-video.md. | | **LinkedIn carousel from a report** | `playbook/taste.md`, `formats/carousel.md`, `formats/text-posts.md` | Unfinished number, Contrarian, Proof object | `recipes/carousel.md` (HTML → PDF) | One finding per slide; real numbers from the report only; link in first comment. | | **Event highlight reel** | `formats/short-video.md` (Event recap row) | Mid-action, Visible stakes, Proof object | `recipes/ffmpeg.md` (trim, concat, reframe, music duck) | Best moment first; result card at the end; `playbook/06-event-coverage.md`. | | **Daily schedule / standings story for an event** | `formats/specs.md` (story row) | headline = the one number or name | `recipes/carousel.md` Option B (Pillow + data file per day) | Pull data from the official source, never memory. | | **Award / result poster** | `formats/thumbnail.md` (overlay checklist) | — | `recipes/carousel.md` Option B | Native-looking text, faces not cropped, names checked. | | **Faceless explainer Short** | `formats/short-video.md` | Specific question, Myth bust | `recipes/end-to-end-short.md` | Facts need sources; disclose synthetic voice. | | **Talking-head tip video** | `formats/short-video.md` | Direct callout, Mistake reveal | `playbook/04-edit.md` order + `recipes/ffmpeg.md` (silence cut, loudnorm, punch-in, captions) | | | **YouTube thumbnail** | `formats/thumbnail.md` | text adds what the title doesn't | Pillow or HTML; test at 160 px | 2-3 different variants for Test & Compare. | | **X thread from a blog post** | `formats/text-posts.md` | Result first, Unfinished number | — | Tweet 1 stands alone; link in reply. | | **Reframe a landscape video for Reels** | `formats/specs.md` safe zones | — | `recipes/ffmpeg.md` (centre crop, dark panels, blur) or `scripts/moviepy_demo.py` | Prefer crop when the subject stays centred. | | **Quote / poetry carousel** | `playbook/taste.md`, `formats/carousel.md` | — (the words are the hook) | `recipes/stock-photos.md` → `scripts/photo_slide.py` | Real photos, one typeface, text in negative space; smell test before showing. | | **Burn captions onto an existing clip** | `formats/specs.md` safe zones | — | Whisper word timestamps → `scripts/captions.py` → ffmpeg `subtitles` | Run ffmpeg from the job folder (relative paths). | ===== plugins/media-lab/skills/media-lab/recipes/carousel.md ===== # Carousels, posters, cards: HTML/CSS or Pillow ## Option A: HTML/CSS to PNG/PDF (best type control) 1. One HTML file, each slide a `
` of 1080x1350 px, fonts loaded locally or from Google Fonts. 2. Render with Playwright: ```python from playwright.sync_api import sync_playwright with sync_playwright() as p: b = p.chromium.launch(); pg = b.new_page(viewport={"width": 1080, "height": 1350}, device_scale_factor=1) pg.goto("file:///C:/path/carousel.html"); pg.wait_for_timeout(500) for i, el in enumerate(pg.query_selector_all(".slide"), 1): el.screenshot(path=f"slide_{i:02d}.png") pg.pdf(path="carousel.pdf", width="1080px", height="1350px", print_background=True) # LinkedIn document b.close() ``` CSS for the PDF: `@page { size: 1080px 1350px; margin: 0 } .slide { break-after: page }`. ## Option B: Pillow (posters and result cards from a template image) - Keep the template PNG, a fonts folder and a data file per variant; one script renders all variants. - Draw text with the template's real font; measure with `draw.textbbox` and centre or right-align by the box, not by guesswork. - Text that sits on photos: add a subtle shadow or gradient so it reads, rather than a box. - Photos: crop to the slot with the face in the upper third; never stretch. If the photo is too narrow, add dark side panels in the template's style. ## Checks - Look at every exported slide at phone size. - Spell-check names, scores and dates against the source. ===== plugins/media-lab/skills/media-lab/recipes/choose-a-tool.md ===== # Choose a tool | Job | Use | Why | |---|---|---| | Cut, trim, reframe, concat, loudness, burn captions on real footage | **ffmpeg** (`ffmpeg.md`) | fastest, scriptable, no project setup | | Script or stills + voice into a captioned Short | `scripts/make_short.py` (edge-tts + ffmpeg, see `end-to-end-short.md`) | free, tested, no assets needed | | Python logic over clips (compositing, conditional edits, data-driven) | **MoviePy** (`moviepy.md`) | readable Python; slower renders | | Kinetic type, animated UI mockups, charts, branded templates rendered many times | **Remotion** (`remotion.md`) | React components as video; install Remotion's own Claude Code plugin for API detail | | Carousels, posters, thumbnails, result cards | **HTML/CSS → PNG** or **Pillow** (`carousel.md`) | exact type control; one data file per variant | | Math/diagram animations | Manim | precise geometry and equations | | Captions from speech | Whisper (local `openai-whisper` or `faster-whisper`) for word timestamps; edge-tts gives timestamps for free when we generate the voice | | ===== plugins/media-lab/skills/media-lab/recipes/end-to-end-short.md ===== # End-to-end Short (free tools only) Script → edge-tts voice with word timings → 9:16 visuals → word-highlight captions + hook title → loudness-normalised MP4. Tested 2026-10-25: ffmpeg 8, Python 3.13, edge-tts 7, Windows. No API keys; edge-tts needs internet. ```bash pip install edge-tts python scripts/make_short.py examples/photo-sorter-short.txt out/ # gradient cards, zero assets python scripts/make_short.py script.txt out/ --images s1.jpg s2.jpg s3.jpg # one image per beat (Ken Burns) python scripts/make_short.py script.txt out/ --voice en-IN-PrabhatNeural # list voices: edge-tts --list-voices ``` Script file: one beat per line. Line 1 is the hook and also appears as the on-screen title for the first 2.2 s. The example (`examples/photo-sorter-short.txt`, fictional club) gives a 24 s, 1080x1920 file with 68 captioned words at about -15 LUFS. `examples/photo-sorter-short_frame1s.jpg` shows the frame at 1 s. Output folder: `voice.mp3`, `words.json` (re-usable for other caption styles), `captions.ass`, `short.mp4`. Then run `playbook/05-verify.md` on `short.mp4`, write the description with `humanize`, and set the platform's synthetic-voice disclosure (the voice is TTS). Limits (honest): - Captions are chunked every 3 words and can run across a sentence boundary. - Beat timing comes from word counts, so a beat with long words gets slightly short visuals. - Gradient cards are placeholders. Real footage or images are what make it watchable. ===== plugins/media-lab/skills/media-lab/recipes/ffmpeg.md ===== # ffmpeg recipes (tested on ffmpeg 7/8, Windows Git Bash) Run from the job folder so relative paths work (Windows drive letters break the `subtitles` filter). ## Reframe 16:9 to 9:16 with a blurred fill ```bash ffmpeg -i in.mp4 -filter_complex "[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=24:2,eq=brightness=-0.08[bg];[0:v]scale=1080:-2[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2" -c:a copy -r 30 out_9x16.mp4 ``` Prefer a centre crop when the subject stays in the middle: `-vf "crop=ih*9/16:ih,scale=1080:1920"`. ## Dark side panels instead of blur (looks less "edited") ```bash ffmpeg -i in.mp4 -vf "scale=1080:-2,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=0x0d0d0f" out.mp4 ``` ## Trim without re-encode (cuts on keyframes) / frame-accurate ```bash ffmpeg -ss 00:00:05 -to 00:00:20 -i in.mp4 -c copy cut.mp4 ffmpeg -ss 00:00:05 -to 00:00:20 -i in.mp4 -c:v libx264 -crf 18 -c:a aac cut_exact.mp4 ``` ## Remove silences (talking head first pass) ```bash ffmpeg -i in.mp4 -af "silenceremove=start_periods=1:stop_periods=-1:stop_duration=0.35:stop_threshold=-35dB" -vn voice_tight.wav ``` For video, detect with `silencedetect` and cut both streams with the timestamps (`scripts/` can be extended). ## Loudness to -14 LUFS (two-pass is more accurate; one pass is fine for social) ```bash ffmpeg -i in.mp4 -af loudnorm=I=-14:TP=-1.5:LRA=11 -c:v copy out.mp4 ``` ## Music bed ducked under voice ```bash ffmpeg -i voice.mp4 -i music.mp3 -filter_complex "[1:a]volume=0.25[m];[m][0:a]sidechaincompress=threshold=0.05:ratio=8:attack=20:release=300[duck];[0:a][duck]amix=inputs=2:duration=first[a]" -map 0:v -map "[a]" -c:v copy out.mp4 ``` ## Ken Burns on a still (render big, then scale, to avoid jitter) ```bash ffmpeg -loop 1 -i still.jpg -t 4 -vf "scale=2160:3840,zoompan=z='min(zoom+0.0012,1.15)':x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':d=120:s=1080x1920:fps=30" -pix_fmt yuv420p scene.mp4 ``` ## Concatenate same-format clips ```bash printf "file '%s'\n" scene*.mp4 > list.txt ffmpeg -f concat -safe 0 -i list.txt -c copy joined.mp4 ``` ## Captions Build an ASS file from word timestamps with `scripts/captions.py`, then: ```bash ffmpeg -i joined.mp4 -vf "subtitles=captions.ass:fontsdir=fonts" -c:a copy captioned.mp4 ``` ## Punch-in zoom on a section (110% from 3 s to 5 s, input already 1080x1920) ```bash ffmpeg -i in.mp4 -vf "scale=w='if(between(t,3,5),1188,1080)':h='if(between(t,3,5),2112,1920)':eval=frame,crop=1080:1920" out.mp4 ``` Use fixed pixel sizes: `iw*1.1` with `h=-2` fails on landscape input because the crop no longer fits. ## Checks ```bash ffprobe -v error -show_entries stream=codec_name,width,height,r_frame_rate:format=duration -of compact in.mp4 ffmpeg -i out.mp4 -af loudnorm=print_format=summary -f null - 2>&1 | grep "Input Integrated" ffmpeg -ss 1 -i out.mp4 -frames:v 1 -y check.jpg ``` ## Final export for upload ```bash ffmpeg -i edit.mp4 -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p -r 30 -c:a aac -b:a 192k -ar 48000 -movflags +faststart final.mp4 ``` ===== plugins/media-lab/skills/media-lab/recipes/moviepy.md ===== # MoviePy recipes (MoviePy 2.x API) > Status: tested 2026-10-25 with MoviePy 2.1.2 (Python 3.13, Windows). The runnable version is `scripts/moviepy_demo.py`: > `python moviepy_demo.py landscape.mp4 still1.jpg out.mp4` gives a 1080x1920 MP4 with audio. The snippet below is the same logic, annotated. > Install into its own venv (`pip install "moviepy>=2,<3"`); an old global MoviePy 0.2/1.x will not run it. Use MoviePy when the edit needs Python logic. For plain cuts and filters, ffmpeg is faster. ```python from moviepy import ColorClip, VideoFileClip, ImageClip, CompositeVideoClip, concatenate_videoclips, AudioFileClip, vfx W, H = 1080, 1920 # 1. Reframe landscape footage into 9:16 with dark panels src = VideoFileClip("in.mp4").resized(width=W) bg = ColorClip((W, H), color=(13, 13, 15)).with_duration(src.duration) vertical = CompositeVideoClip([bg, src.with_position("center")], size=(W, H)) # 2. Stills to scenes with a slow zoom def scene(path, dur): return (ImageClip(path).with_duration(dur).resized(height=H) .with_effects([vfx.Resize(lambda t: 1 + 0.03 * t)]).with_position("center")) clips = [scene(p, 3.5) for p in ["s1.jpg", "s2.jpg", "s3.jpg"]] body = CompositeVideoClip([concatenate_videoclips(clips, method="compose")], size=(W, H)) # 3. Voice + music bed voice = AudioFileClip("voice.mp3") final = body.with_duration(voice.duration).with_audio(voice) final.write_videofile("out.mp4", fps=30, codec="libx264", audio_codec="aac", preset="medium") ``` Captions: generate ASS with `scripts/captions.py` and burn with ffmpeg afterwards; MoviePy TextClip captions are slow and need ImageMagick-free font paths. Gotchas: - MoviePy 2 renamed methods (`resized`, `with_duration`, `with_position`, `with_effects`); 1.x tutorials won't run as-is. - Close clips (`clip.close()`) in loops or Windows keeps the files locked. ===== plugins/media-lab/skills/media-lab/recipes/remotion.md ===== # Remotion (React as video) Use for kinetic typography, animated UI mockups, charts, and branded templates rendered many times with different data. ## Setup - Install Remotion's official Claude Code plugin for API-level guidance: see https://remotion.dev/docs/ai/claude-code-plugin (it ships Remotion's own skills for setup, animation, rendering and captions). Media Lab does not duplicate those. - New project: `npx create-video@latest` (pick the blank or TikTok template). Preview: `npx remotion studio`. - Render: `npx remotion render out.mp4 --props=data.json`. ## How it fits our pipeline 1. Brief, package and script as usual (playbook). 2. Make one composition per format at the spec size (1080x1920 at 30 fps for vertical). 3. Drive it with a JSON props file per variant (event result, stat, quote), the same way the poster scripts use a data file per day. 4. Captions: Remotion's caption helpers or our ASS file burned afterwards with ffmpeg. 5. Verify the rendered MP4 with `playbook/05-verify.md`. Licence note: Remotion is free for individuals and small teams; companies above its size threshold need a company licence. Check before using it for a client. ===== plugins/media-lab/skills/media-lab/recipes/stock-photos.md ===== # Free real photos, sourced legally Tested 2026-10-25: Openverse API search, then download and `scripts/photo_slide.py` render. ## Openverse (no API key) ```bash curl -s "https://api.openverse.org/v1/images/?q=rain%20window&license=cc0,pdm&size=large&page_size=20" ``` - `license=cc0,pdm` gives public domain only, so no attribution is legally required. Keep a `credits.txt` anyway (title, creator, `foreign_landing_url`, licence). It's courteous and it proves the licence later. - `size=large` drops tiny images. Check `width`/`height` in the result: you need at least 1080 px wide for a 4:5 slide, ideally 2000+. - Download with a browser-like `User-Agent` header. Some hosts return an HTML page (hotlink protection) to bare clients, so check the first bytes are JPEG/PNG. The fallback is `https://api.openverse.org/v1/images//thumb/?full_size=true`. - Other useful filters: `aspect_ratio=tall` (portrait), `category=photograph`, `source=stocksnap,rawpixel,flickr`. ```python import json, urllib.request H = {"User-Agent": "Mozilla/5.0 media-lab"} res = json.load(urllib.request.urlopen(urllib.request.Request( "https://api.openverse.org/v1/images/?q=empty%20platform&license=cc0,pdm&size=large&aspect_ratio=tall", headers=H))) for r in res["results"][:5]: data = urllib.request.urlopen(urllib.request.Request(r["url"], headers=H), timeout=30).read() if data[:3] == b"\xff\xd8\xff": open(f"{r['id']}.jpg", "wb").write(data) open("credits.txt", "a").write(f"{r['title']} | {r.get('creator')} | {r['license']} | {r['foreign_landing_url']}\n") ``` ## Unsplash and Pexels - Both licences allow free commercial and personal use without attribution (credit is appreciated). - You may not sell the photos unaltered (prints, stock resale), or build a competing photo service with them. - Don't imply that a recognisable person or brand in the photo endorses anything. - The Unsplash API needs a free access key (and asks you to trigger its download endpoint when you use a photo). The Pexels API needs a free key too. Without keys, pick by hand on the website and note the photo URL in `credits.txt`. - Re-check each site's licence page before a commercial client job; these terms change. ## Make the slide (Pillow) ```bash python scripts/photo_slide.py photo.jpg "Some evenings the rain says everything." slide.png --where bottom --focus 0.4 ``` - Crops to 1080x1350 (4:5) by scale-to-cover. `--focus` (0 left, 1 right) keeps the subject in frame. - Darkens only a soft band behind the text (about 45% max), not the whole photo. Never blur the whole image. - Wraps so the last line is never a single word. Default font Georgia, off-white text; pass `--font` for another. ## Picking a photo whose negative space fits the text - Look for a calm third of the frame: sky, wall, water, floor, a blurred background. That's where the text goes. - Portrait (tall) photos crop to 4:5 with the least loss; landscape loses the sides, so check `--focus`. - Busy everywhere (crowds, foliage)? Put the text on a plain paper slide next to it instead of over it. - Shoot your own when you can. A phone photo of the real place beats any stock image. - Before showing anything, run the smell test in `playbook/taste.md`. ===== docs/poetry-film-photo.md ===== # Poetry on a graded photo > Real CC0 photo, one muted film grade, plain Garamond set in the darkest edge, small slide counter. - **Category:** Carousels - **Format:** 1080x1350 (4:5) - **Tags:** poetry, quote, film grade, garamond, real photo - **Worlds:** Quiet Poetry - **Preview:** https://media-lab-seven.vercel.app/previews/poetry-film-photo.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/poetry-film-photo - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/poetry-film-photo.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/poetry-film-photo.py ## When to use - Poetry, quotes or reflective captions - A set that must look shot, not generated - Instagram carousels aimed at saves and sends ## Read first - [playbook/taste.md](https://media-lab-seven.vercel.app/skill/playbook/taste.md) - [recipes/stock-photos.md](https://media-lab-seven.vercel.app/skill/recipes/stock-photos.md) - [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py poetry-film-photo # writes public/previews/poetry-film-photo.jpg ``` ## Source ```python """Poetry slide: real CC0 photo, one film grade, plain Garamond in the photo's darkest edge, slide counter.""" from pathlib import Path from PIL import ImageDraw from lib import film, fit, font, photo, save_jpg, shade, text_block, wrap W, H = 1080, 1350 LINES = "The kettle still clicks off at seven. Nobody comes to the window now." # fictional def render(out: Path) -> Path: img = shade(film(fit(photo("coffee"), W, H, fx=0.5)), "bottom") d = ImageDraw.Draw(img) f = font("serif", 50) lines = wrap(LINES, f, 860) y0 = H - 140 - int(f.size * 1.32) * len(lines) text_block(d, (110, y0), lines, f, (244, 240, 232), 1.32) d.text((W - 110, 100), "3/9", font=font("serif", 28), fill=(220, 214, 204), anchor="ra") return save_jpg(img, out / "poetry-film-photo.jpg") ``` ===== docs/poetry-paper.md ===== # Stanza on paper > No photo: a warm paper tone, wide margins, one serif, the stanza keeps its own line breaks. - **Category:** Carousels - **Format:** 1080x1350 (4:5) - **Tags:** poetry, paper, serif, typography - **Worlds:** Quiet Poetry - **Preview:** https://media-lab-seven.vercel.app/previews/poetry-paper.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/poetry-paper - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/poetry-paper.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/poetry-paper.py ## When to use - Breather slide between photo slides - Longer stanzas that would fight a busy photo ## Read first - [playbook/taste.md](https://media-lab-seven.vercel.app/skill/playbook/taste.md) - [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py poetry-paper # writes public/previews/poetry-paper.jpg ``` ## Source ```python """Poetry slide on warm paper: no photo, generous margins, one serif, a short stanza set with its own line breaks.""" from pathlib import Path from PIL import ImageDraw from lib import font, paper, save_jpg W, H = 1080, 1350 STANZA = ["You left the radio", "on the wrong station,", "and I kept it there", "for a whole winter."] # fictional def render(out: Path) -> Path: img = paper(W, H) d = ImageDraw.Draw(img) f = font("serif", 56) y = 470 for ln in STANZA: d.text((150, y), ln, font=f, fill=(38, 34, 30)) y += 82 d.text((150, y + 60), "from a notebook, page 12", font=font("serif", 30), fill=(120, 112, 100)) d.text((W - 110, 100), "5/9", font=font("serif", 28), fill=(120, 112, 100), anchor="ra") return save_jpg(img, out / "poetry-paper.jpg") ``` ===== docs/carousel-cover-promise.md ===== # Cover slide with a promise > Slide 1: one sentence-case promise in the photo's calm area, darkened only behind the text, plus a 1/7 cue. - **Category:** Carousels - **Format:** 1080x1350 (4:5) - **Tags:** cover, hook, carousel, linkedin document - **Worlds:** Warm Local - **Preview:** https://media-lab-seven.vercel.app/previews/carousel-cover-promise.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/carousel-cover-promise - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/carousel-cover-promise.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/carousel-cover-promise.py ## When to use - First slide of any educational carousel - LinkedIn document covers ## Read first - [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md) - [playbook/02-package.md](https://media-lab-seven.vercel.app/skill/playbook/02-package.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py carousel-cover-promise # writes public/previews/carousel-cover-promise.jpg ``` ## Source ```python """Carousel cover: one promise in sentence case over a real photo, top-left in the calm area, '1/7' cue.""" from pathlib import Path from PIL import ImageDraw from lib import film, fit, font, photo, save_jpg, shade, text_block, wrap W, H = 1080, 1350 def render(out: Path) -> Path: img = shade(film(fit(photo("desk"), W, H, fx=0.4), warmth=0.4, grain=0.04), "top", strength=190, start=0.5) d = ImageDraw.Draw(img) f = font("sans-bold", 84) lines = wrap("Five checks before you post any Reel", f, 820) text_block(d, (90, 120), lines, f, (250, 248, 244), 1.12) d.text((90, H - 110), "1/7 →", font=font("sans", 34), fill=(250, 248, 244)) return save_jpg(img, out / "carousel-cover-promise.jpg") ``` ===== docs/list-point-slide.md ===== # Numbered point slide > Middle slide: big number, short headline, two supporting lines, the same grid on every slide. - **Category:** Carousels - **Format:** 1080x1350 (4:5) - **Tags:** list, tips, grid - **Worlds:** Data Story - **Preview:** https://media-lab-seven.vercel.app/previews/list-point-slide.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/list-point-slide - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/list-point-slide.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/list-point-slide.py ## When to use - List and tip carousels - Turning a checklist into slides ## Read first - [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md) - [hooks/templates.md](https://media-lab-seven.vercel.app/skill/hooks/templates.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py list-point-slide # writes public/previews/list-point-slide.jpg ``` ## Source ```python """List carousel middle slide: number, short headline, two lines of support, same layout every slide.""" from pathlib import Path from PIL import ImageDraw from lib import font, paper, save_jpg, text_block, wrap W, H = 1080, 1350 INK = (28, 28, 30) def render(out: Path) -> Path: img = paper(W, H, (236, 240, 236)) d = ImageDraw.Draw(img) d.text((90, 140), "02", font=font("condensed", 150), fill=(46, 104, 82)) fh = font("sans-bold", 72) y = text_block(d, (90, 380), wrap("Say the hook out loud", fh, 860), fh, INK, 1.12) fb = font("sans", 44) text_block(d, (90, y + 40), wrap("If it takes longer than two seconds to say, it is too long. Cut words until it does not.", fb, 860), fb, (70, 70, 72), 1.35) d.text((W - 90, H - 110), "@pebbleclub", font=font("sans", 30), fill=(110, 110, 110), anchor="ra") return save_jpg(img, out / "list-point-slide.jpg") ``` ===== docs/data-story-number.md ===== # One giant number > A single earned statistic, one plain sentence on what it means, and the source in small type. - **Category:** Carousels - **Format:** 1080x1350 (4:5) - **Tags:** data, statistic, swiss, report - **Worlds:** Data Story - **Preview:** https://media-lab-seven.vercel.app/previews/data-story-number.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/data-story-number - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/data-story-number.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/data-story-number.py ## When to use - Reports and recaps turned into posts - Data stories where one figure carries the point ## Read first - [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md) - [recipes/carousel.md](https://media-lab-seven.vercel.app/skill/recipes/carousel.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py data-story-number # writes public/previews/data-story-number.jpg ``` ## Source ```python """Data-story slide: one earned giant number, one plain line saying what it means, source in small type.""" from pathlib import Path from PIL import ImageDraw from lib import font, paper, save_jpg, text_block, wrap W, H = 1080, 1350 INK, ACCENT = (24, 24, 26), (196, 64, 38) def render(out: Path) -> Path: img = paper(W, H, (246, 244, 240)) d = ImageDraw.Draw(img) d.line((90, 160, W - 90, 160), fill=INK, width=3) d.text((90, 190), "Pebble Club fest, 2026", font=font("sans", 32), fill=INK) d.text((80, 360), "730", font=font("condensed", 330), fill=ACCENT) f = font("sans", 52) text_block(d, (90, 760), wrap("photos and clips from three days, sorted by when they were really taken.", f, 880), f, INK, 1.25) d.text((90, H - 120), "Source: club media folder count (fictional demo data)", font=font("sans", 26), fill=(110, 110, 110)) return save_jpg(img, out / "data-story-number.jpg") ``` ===== docs/cta-last-slide.md ===== # Last slide with one ask > Closing slide with a single clear ask (save, send) tied to the goal metric, in the set's photo style. - **Category:** Carousels - **Format:** 1080x1350 (4:5) - **Tags:** cta, save, send, ending - **Worlds:** Warm Local - **Preview:** https://media-lab-seven.vercel.app/previews/cta-last-slide.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/cta-last-slide - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/cta-last-slide.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/cta-last-slide.py ## When to use - Final slide of every carousel - When the goal is saves or sends ## Read first - [formats/carousel.md](https://media-lab-seven.vercel.app/skill/formats/carousel.md) - [hooks/hooks.md](https://media-lab-seven.vercel.app/skill/hooks/hooks.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py cta-last-slide # writes public/previews/cta-last-slide.jpg ``` ## Source ```python """Last carousel slide: one clear ask tied to the goal (save / send), photo continues the set's look.""" from pathlib import Path from PIL import ImageDraw from lib import film, fit, font, photo, save_jpg, shade, text_block, wrap W, H = 1080, 1350 def render(out: Path) -> Path: img = shade(film(fit(photo("leaves"), W, H), grain=0.05), "bottom", strength=215, start=0.35) d = ImageDraw.Draw(img) f = font("sans-bold", 76) y = text_block(d, (90, 820), wrap("Save this for your next upload.", f, 880), f, (250, 248, 244), 1.12) fs = font("sans", 40) text_block(d, (90, y + 30), wrap("Send it to the friend who posts at 2 AM.", fs, 880), fs, (225, 222, 214), 1.3) return save_jpg(img, out / "cta-last-slide.jpg") ``` ===== docs/event-result-card.md ===== # Match result card > Score as the hero over a real court photo, both teams, one moment from the match and the next fixture. - **Category:** Event media - **Format:** 1080x1350 (4:5) - **Tags:** sports, score, result, league - **Worlds:** Campus Event - **Preview:** https://media-lab-seven.vercel.app/previews/event-result-card.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/event-result-card - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/event-result-card.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/event-result-card.py ## When to use - Sports leagues and tournaments - Any competition result post ## Read first - [playbook/06-event-coverage.md](https://media-lab-seven.vercel.app/skill/playbook/06-event-coverage.md) - [recipes/carousel.md](https://media-lab-seven.vercel.app/skill/recipes/carousel.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py event-result-card # writes public/previews/event-result-card.jpg ``` ## Source ```python """Match result card (4:5): real court photo, score as the hero, both teams, one moment, next fixture. Fictional teams.""" from pathlib import Path from PIL import ImageDraw from lib import film, fit, font, photo, save_jpg, shade W, H = 1080, 1350 WHITE, DIM = (250, 248, 244), (205, 202, 196) def render(out: Path) -> Path: img = shade(film(fit(photo("courtaerial"), W, H, fx=0.5), warmth=0.3), "bottom", strength=225, start=0.3) d = ImageDraw.Draw(img) d.text((90, 110), "FINAL · DAY 3", font=font("sans-bold", 34), fill=WHITE) d.text((90, 690), "Ridgeway", font=font("sans-bold", 64), fill=WHITE) d.text((W - 90, 690), "Northfield", font=font("sans-bold", 64), fill=DIM, anchor="ra") d.text((W // 2, 800), "3 – 2", font=font("condensed", 230), fill=WHITE, anchor="ma") d.text((90, 1110), "Saved two match points in set five.", font=font("sans", 40), fill=WHITE) d.text((90, 1170), "Next: semi-final, Sunday 5:45 PM", font=font("sans", 34), fill=DIM) return save_jpg(img, out / "event-result-card.jpg") ``` ===== docs/event-award-card.md ===== # Player award card > Photo with the subject uncropped, the award small, the name big, one real stat underneath. - **Category:** Event media - **Format:** 1080x1350 (4:5) - **Tags:** award, mvp, portrait, sports - **Worlds:** Campus Event - **Preview:** https://media-lab-seven.vercel.app/previews/event-award-card.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/event-award-card - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/event-award-card.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/event-award-card.py ## When to use - MVP, best player and award announcements - Speaker or winner spotlights ## Read first - [playbook/06-event-coverage.md](https://media-lab-seven.vercel.app/skill/playbook/06-event-coverage.md) - [formats/thumbnail.md](https://media-lab-seven.vercel.app/skill/formats/thumbnail.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py event-award-card # writes public/previews/event-award-card.jpg ``` ## Source ```python """Player award card: photo with the subject uncropped, award name small, player name big, one real stat. Fictional player.""" from pathlib import Path from PIL import ImageDraw from lib import film, fit, font, photo, save_jpg, shade W, H = 1080, 1350 def render(out: Path) -> Path: img = shade(film(fit(photo("player"), W, H, fx=0.5, fy=0.2)), "bottom", strength=230, start=0.4) d = ImageDraw.Draw(img) d.text((90, 960), "Best Setter", font=font("sans", 40), fill=(220, 214, 200)) d.text((90, 1010), "Kavya Rao", font=font("sans-bold", 104), fill=(250, 248, 244)) d.text((90, 1150), "Harbour FC · 41 assists across the league", font=font("sans", 36), fill=(220, 214, 200)) return save_jpg(img, out / "event-award-card.jpg") ``` ===== docs/event-schedule-story.md ===== # Daily schedule story > Plain fixtures table inside the story safe zone, rendered from a data list so each day is one command. - **Category:** Event media - **Format:** 1080x1920 (9:16 story) - **Tags:** schedule, story, fixtures, table - **Worlds:** Campus Event - **Preview:** https://media-lab-seven.vercel.app/previews/event-schedule-story.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/event-schedule-story - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/event-schedule-story.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/event-schedule-story.py ## When to use - Multi-day events posting a daily schedule - Any timetable that changes every day ## Read first - [playbook/06-event-coverage.md](https://media-lab-seven.vercel.app/skill/playbook/06-event-coverage.md) - [formats/specs.md](https://media-lab-seven.vercel.app/skill/formats/specs.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py event-schedule-story # writes public/previews/event-schedule-story.jpg ``` ## Source ```python """Daily schedule story (9:16): plain table of fixtures inside the safe zone, from a data list. Fictional teams.""" from pathlib import Path from PIL import ImageDraw from lib import film, fit, font, photo, save_jpg W, H = 1080, 1920 FIXTURES = [("4:30", "Ridgeway", "Northfield"), ("4:50", "Harbour FC", "Kestrel"), ("5:10", "Lowmoor", "Ridgeway"), ("5:30", "Northfield", "Harbour FC"), ("5:50", "Kestrel", "Lowmoor")] def render(out: Path) -> Path: img = film(fit(photo("courtaerial"), W, H, fx=0.5), warmth=0.2).point(lambda v: int(v * 0.35)) d = ImageDraw.Draw(img) d.text((90, 300), "Day 2", font=font("condensed", 120), fill=(250, 248, 244)) d.text((90, 440), "Saturday · Court A", font=font("sans", 40), fill=(210, 206, 198)) fs, fb = font("sans", 44), font("sans-bold", 44) y = 600 for t, a, b in FIXTURES: d.line((90, y - 24, W - 90, y - 24), fill=(90, 90, 92), width=2) d.text((90, y), t, font=fs, fill=(210, 206, 198)) d.text((240, y), a, font=fb, fill=(250, 248, 244)) d.text((640, y), "vs", font=fs, fill=(150, 150, 150), anchor="ma") d.text((W - 90, y), b, font=fb, fill=(250, 248, 244), anchor="ra") y += 120 d.text((90, 1440), "Standings after every match: link in bio", font=font("sans", 34), fill=(200, 196, 188)) return save_jpg(img, out / "event-schedule-story.jpg", preview_width=405) ``` ===== docs/thumb-object-words.md ===== # Object and three words > One real object, three words that add what the title does not say, kept clear of the duration badge. - **Category:** Thumbnails - **Format:** 1280x720 (16:9) - **Tags:** thumbnail, youtube, object, text - **Preview:** https://media-lab-seven.vercel.app/previews/thumb-object-words.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/thumb-object-words - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/thumb-object-words.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/thumb-object-words.py ## When to use - YouTube long-form and tutorials - When there is no face to use ## Read first - [formats/thumbnail.md](https://media-lab-seven.vercel.app/skill/formats/thumbnail.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py thumb-object-words # writes public/previews/thumb-object-words.jpg ``` ## Source ```python """YouTube thumbnail: one real object, three words that add what the title does not say, clear of the bottom-right badge.""" from pathlib import Path from PIL import ImageDraw from lib import fit, font, photo, save_jpg W, H = 1280, 720 def render(out: Path) -> Path: img = fit(photo("coffee"), W, H, fx=0.5, fy=0.75) d = ImageDraw.Draw(img) f = font("sans-bold", 118) d.text((70, 120), "COLD", font=f, fill=(250, 248, 244), stroke_width=4, stroke_fill=(0, 0, 0)) d.text((70, 250), "BY 9 AM?", font=f, fill=(255, 214, 64), stroke_width=4, stroke_fill=(0, 0, 0)) return save_jpg(img, out / "thumb-object-words.jpg", preview_width=640) ``` ===== docs/thumb-big-number.md ===== # Big number thumbnail > A huge numeral and one context word over a darkened real photo; still readable at 160 px wide. - **Category:** Thumbnails - **Format:** 1280x720 (16:9) - **Tags:** thumbnail, number, contrast - **Worlds:** Tech Launch, Data Story, Build Log - **Preview:** https://media-lab-seven.vercel.app/previews/thumb-big-number.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/thumb-big-number - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/thumb-big-number.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/thumb-big-number.py ## When to use - Results, experiments and counts - Build logs with a measurable outcome ## Read first - [formats/thumbnail.md](https://media-lab-seven.vercel.app/skill/formats/thumbnail.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py thumb-big-number # writes public/previews/thumb-big-number.jpg ``` ## Source ```python """Thumbnail built on one number: huge numeral, a small context word, real photo behind, readable at 160 px.""" from pathlib import Path from PIL import ImageDraw, ImageEnhance from lib import fit, font, photo, save_jpg W, H = 1280, 720 def render(out: Path) -> Path: img = ImageEnhance.Brightness(fit(photo("desk"), W, H, fx=0.5, fy=0.4)).enhance(0.55) d = ImageDraw.Draw(img) d.text((70, 40), "212", font=font("condensed", 420), fill=(255, 255, 255)) d.text((80, 520), "duplicates", font=font("sans-bold", 80), fill=(255, 214, 64)) return save_jpg(img, out / "thumb-big-number.jpg", preview_width=640) ``` ===== docs/hook-title-card.md ===== # Result-first hook title > The hook as on-screen text for the first two seconds: wrapped by hand, one box per line, upper safe zone. - **Category:** Hooks - **Format:** 1080x1920 (first 2 s) - **Tags:** hook, title, shorts, reels - **Worlds:** Tech Launch, Faceless Story - **Preview:** https://media-lab-seven.vercel.app/previews/hook-title-card.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/hook-title-card - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/hook-title-card.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/hook-title-card.py ## When to use - Every Short that opens on a claim or result - Muted autoplay feeds ## Read first - [hooks/hooks.md](https://media-lab-seven.vercel.app/skill/hooks/hooks.md) - [formats/thumbnail.md](https://media-lab-seven.vercel.app/skill/formats/thumbnail.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py hook-title-card # writes public/previews/hook-title-card.jpg ``` ## Source ```python """On-video hook title (first 2 s of a Short): result-first line, wrapped by hand, one box per line, upper safe zone.""" from pathlib import Path from PIL import ImageDraw from lib import film, fit, font, photo, save_jpg W, H = 1080, 1920 LINES = ["THIS STREET SELLS", "212 KINDS OF", "NOODLES"] # fictional claim for the demo def render(out: Path) -> Path: img = film(fit(photo("street"), W, H, fx=0.5), warmth=0.2, grain=0.04) d = ImageDraw.Draw(img) f = font("sans-bold", 76) y = 330 for ln in LINES: x0, y0, x1, y1 = d.textbbox((W // 2, y), ln, font=f, anchor="ma") d.rectangle((x0 - 22, y0 - 14, x1 + 22, y1 + 14), fill=(0, 0, 0)) d.text((W // 2, y), ln, font=f, fill=(255, 255, 255), anchor="ma") y += 112 return save_jpg(img, out / "hook-title-card.jpg", preview_width=405) ``` ===== docs/hook-unfinished-number.md ===== # Unfinished-number hook > A countdown promise ("4 ... the last one ...") with a number badge, so viewers wait for the payoff. - **Category:** Hooks - **Format:** 1080x1920 (first 2 s) - **Tags:** hook, countdown, list - **Worlds:** Explainer, Build Log - **Preview:** https://media-lab-seven.vercel.app/previews/hook-unfinished-number.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/hook-unfinished-number - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/hook-unfinished-number.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/hook-unfinished-number.py ## When to use - List and tips Shorts - Tool roundups ## Read first - [hooks/hooks.md](https://media-lab-seven.vercel.app/skill/hooks/hooks.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py hook-unfinished-number # writes public/previews/hook-unfinished-number.jpg ``` ## Source ```python """Unfinished-number hook: a countdown promise on screen, a small number badge, minimal outlined text.""" from pathlib import Path from PIL import ImageDraw from lib import film, fit, font, photo, save_jpg W, H = 1080, 1920 LINES = ["ffmpeg flags I use daily.", "The last one saved", "a whole edit."] def render(out: Path) -> Path: img = film(fit(photo("desk"), W, H, fx=0.45), warmth=0.3) d = ImageDraw.Draw(img) d.rounded_rectangle((90, 300, 250, 460), 24, fill=(255, 214, 64)) d.text((170, 380), "4", font=font("condensed", 130), fill=(20, 20, 20), anchor="mm") f = font("sans-bold", 70) for i, ln in enumerate(LINES): d.text((90, 520 + i * 92), ln, font=f, fill=(255, 255, 255), stroke_width=3, stroke_fill=(0, 0, 0)) return save_jpg(img, out / "hook-unfinished-number.jpg", preview_width=405) ``` ===== docs/caption-word-highlight.md ===== # Word-highlight captions > Three-word chunks, uppercase, the spoken word in yellow and slightly larger. The Shorts default. - **Category:** Captions - **Format:** 1080x1920 - **Tags:** captions, subtitles, ass, highlight - **Worlds:** Explainer - **Preview:** https://media-lab-seven.vercel.app/previews/caption-word-highlight.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/caption-word-highlight - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/caption-word-highlight.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/caption-word-highlight.py ## When to use - Talking head and voiceover Shorts - Content watched muted ## Read first - [scripts/captions.py](https://media-lab-seven.vercel.app/skill/scripts/captions.py) - [formats/specs.md](https://media-lab-seven.vercel.app/skill/formats/specs.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py caption-word-highlight # writes public/previews/caption-word-highlight.jpg ``` ## Source ```python """Caption style: 3-word chunks, uppercase, white with the spoken word in yellow and slightly larger. The Shorts default.""" from pathlib import Path from _captions_common import burn, default_ass def render(out: Path) -> Path: return burn(default_ass(), out / "caption-word-highlight.jpg") ``` ===== docs/caption-boxed.md ===== # Boxed captions > Opaque box behind each chunk (ASS BorderStyle 3). The most readable choice on busy, bright footage. - **Category:** Captions - **Format:** 1080x1920 - **Tags:** captions, box, readability - **Worlds:** Faceless Story - **Preview:** https://media-lab-seven.vercel.app/previews/caption-boxed.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/caption-boxed - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/caption-boxed.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/caption-boxed.py ## When to use - Busy or bright backgrounds - Accessibility first ## Read first - [scripts/captions.py](https://media-lab-seven.vercel.app/skill/scripts/captions.py) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py caption-boxed # writes public/previews/caption-boxed.jpg ``` ## Source ```python """Caption style: opaque black box behind each chunk (ASS BorderStyle 3). Most readable on busy footage.""" from pathlib import Path from _captions_common import burn, default_ass def render(out: Path) -> Path: ass = default_ass(highlight="&H00FFFFFF").replace(",-1,0,0,0,100,100,0,0,1,6,2,2,", ",-1,0,0,0,100,100,0,0,3,14,0,2,") ass = ass.replace("&H64000000", "&H00000000") return burn(ass, out / "caption-boxed.jpg", photo="desk") ``` ===== docs/caption-quiet-lowercase.md ===== # Quiet lowercase captions > Lowercase serif, no highlight or pop, only a soft shadow. Captions that do not shout. - **Category:** Captions - **Format:** 1080x1920 - **Tags:** captions, serif, lowercase, calm - **Worlds:** Quiet Poetry - **Preview:** https://media-lab-seven.vercel.app/previews/caption-quiet-lowercase.jpg - **Gallery:** https://media-lab-seven.vercel.app/#/p/caption-quiet-lowercase - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/caption-quiet-lowercase.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/caption-quiet-lowercase.py ## When to use - Poetry, slow vlogs, ambient edits - When loud captions would fight the mood ## Read first - [scripts/captions.py](https://media-lab-seven.vercel.app/skill/scripts/captions.py) - [playbook/taste.md](https://media-lab-seven.vercel.app/skill/playbook/taste.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py caption-quiet-lowercase # writes public/previews/caption-quiet-lowercase.jpg ``` ## Source ```python """Caption style for quiet content: lowercase serif, no highlight or pop, soft shadow only. For poetry and slow vlogs.""" from pathlib import Path from _captions_common import burn, default_ass def render(out: Path) -> Path: ass = default_ass(font="Garamond", size=72, chunk=4, color="&H00F0F0F0", highlight="&H00F0F0F0", upper=False) ass = ass.replace(",-1,0,0,0,100,100,0,0,1,6,2,2,", ",0,0,0,0,100,100,0,0,1,0,3,2,").replace("\\fscx108\\fscy108", "") return burn(ass, out / "caption-quiet-lowercase.jpg", photo="rain") ``` ===== docs/short-word-captions.md ===== # Zero-asset voiceover Short > Script to edge-tts voice, gradient cards, word captions and a hook title. Runs with no footage at all. - **Category:** Shorts - **Format:** 1080x1920, 20-45 s - **Tags:** short, tts, edge-tts, faceless - **Worlds:** Tech Launch - **Preview:** https://media-lab-seven.vercel.app/previews/short-word-captions.mp4 - **Gallery:** https://media-lab-seven.vercel.app/#/p/short-word-captions - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/short-word-captions.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/short-word-captions.py ## When to use - Drafting a Short before footage exists - Explainers where the words carry it ## Read first - [recipes/end-to-end-short.md](https://media-lab-seven.vercel.app/skill/recipes/end-to-end-short.md) - [scripts/make_short.py](https://media-lab-seven.vercel.app/skill/scripts/make_short.py) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py short-word-captions # writes public/previews/short-word-captions.mp4 ``` ## Source ```python """Faceless Short from a script with zero assets: edge-tts voice, word-highlight captions, hook title. Runs scripts/make_short.py.""" import subprocess import sys import tempfile from pathlib import Path from lib import SCRIPTS, ffmpeg SCRIPT = """This app sorted 600 event photos in one night. Pebble Club shot a three day fest on five different phones. Half the dates were wrong, because messaging apps strip the camera time.""" # fictional club def render(out: Path) -> Path: with tempfile.TemporaryDirectory() as tmp: t = Path(tmp) (t / "script.txt").write_text(SCRIPT, encoding="utf-8") subprocess.run([sys.executable, str(SCRIPTS / "make_short.py"), str(t / "script.txt"), str(t / "job")], check=True) dst = out / "short-word-captions.mp4" ffmpeg("-i", str(t / "job" / "short.mp4"), "-t", "8", "-vf", "scale=360:640", "-c:v", "libx264", "-crf", "30", "-preset", "slow", "-c:a", "aac", "-b:a", "64k", "-movflags", "+faststart", str(dst)) return dst ``` ===== docs/short-kenburns-stills.md ===== # Real stills with Ken Burns > One CC0 photo per beat with a slow push, voice and captions on top. Real images make it watchable. - **Category:** Shorts - **Format:** 1080x1920, 20-45 s - **Tags:** short, ken burns, stills, photos - **Worlds:** Explainer, Warm Local, Faceless Story - **Preview:** https://media-lab-seven.vercel.app/previews/short-kenburns-stills.mp4 - **Gallery:** https://media-lab-seven.vercel.app/#/p/short-kenburns-stills - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/short-kenburns-stills.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/short-kenburns-stills.py ## When to use - Story and mood Shorts - Recaps built from photos ## Read first - [recipes/end-to-end-short.md](https://media-lab-seven.vercel.app/skill/recipes/end-to-end-short.md) - [recipes/stock-photos.md](https://media-lab-seven.vercel.app/skill/recipes/stock-photos.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py short-kenburns-stills # writes public/previews/short-kenburns-stills.mp4 ``` ## Source ```python """Short from real stills: one CC0 photo per beat with a slow Ken Burns push, voice and captions on top. Runs make_short.py --images.""" import subprocess import sys import tempfile from pathlib import Path from lib import PHOTOS, SCRIPTS, ffmpeg SCRIPT = """Rain on the window, and the tea goes cold. The street outside is still awake at midnight. And the leaves on the step say it is October.""" # fictional def render(out: Path) -> Path: imgs = [str(PHOTOS / f"{n}.jpg") for n in ("rain", "street", "leaves")] with tempfile.TemporaryDirectory() as tmp: t = Path(tmp) (t / "script.txt").write_text(SCRIPT, encoding="utf-8") subprocess.run([sys.executable, str(SCRIPTS / "make_short.py"), str(t / "script.txt"), str(t / "job"), "--voice", "en-GB-SoniaNeural", "--images", *imgs], check=True) dst = out / "short-kenburns-stills.mp4" ffmpeg("-i", str(t / "job" / "short.mp4"), "-t", "9", "-vf", "scale=360:640", "-c:v", "libx264", "-crf", "30", "-preset", "slow", "-c:a", "aac", "-b:a", "64k", "-movflags", "+faststart", str(dst)) return dst ``` ===== docs/punch-in-zoom.md ===== # Punch-in on emphasis > Jump to 110% for the key line, then back. Fixed pixel sizes so the crop always fits. - **Category:** Shorts - **Format:** 1080x1920 - **Tags:** zoom, edit, ffmpeg - **Worlds:** Tech Launch, Build Log - **Preview:** https://media-lab-seven.vercel.app/previews/punch-in-zoom.mp4 - **Gallery:** https://media-lab-seven.vercel.app/#/p/punch-in-zoom - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/punch-in-zoom.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/punch-in-zoom.py ## When to use - Talking head emphasis - Breaking up a static stretch ## Read first - [recipes/ffmpeg.md](https://media-lab-seven.vercel.app/skill/recipes/ffmpeg.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py punch-in-zoom # writes public/previews/punch-in-zoom.mp4 ``` ## Source ```python """Punch-in on emphasis: jump to 110% for two seconds, then back. Fixed pixel sizes so the crop always fits.""" import tempfile from pathlib import Path from lib import PHOTOS, ffmpeg ZOOM = "scale=w='if(between(t,1.5,3),1188,1080)':h='if(between(t,1.5,3),2112,1920)':eval=frame,crop=1080:1920,scale=360:640" def render(out: Path) -> Path: with tempfile.TemporaryDirectory() as tmp: v = Path(tmp) / "v.mp4" ffmpeg("-loop", "1", "-i", str(PHOTOS / "desk.jpg"), "-t", "4.5", "-vf", "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,fps=30", "-pix_fmt", "yuv420p", str(v)) dst = out / "punch-in-zoom.mp4" ffmpeg("-i", str(v), "-vf", ZOOM, "-c:v", "libx264", "-crf", "30", "-an", "-movflags", "+faststart", str(dst)) return dst ``` ===== docs/reframe-dark-panels.md ===== # 16:9 to 9:16 with dark panels > Landscape footage centred on near-black panels. Reads as intentional rather than edited. - **Category:** Reframes - **Format:** 1920x1080 in, 1080x1920 out - **Tags:** reframe, vertical, ffmpeg - **Worlds:** Faceless Story - **Preview:** https://media-lab-seven.vercel.app/previews/reframe-dark-panels.mp4 - **Gallery:** https://media-lab-seven.vercel.app/#/p/reframe-dark-panels - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/reframe-dark-panels.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/reframe-dark-panels.py ## When to use - Repurposing landscape video for Shorts - When the whole frame matters ## Read first - [recipes/ffmpeg.md](https://media-lab-seven.vercel.app/skill/recipes/ffmpeg.md) - [formats/specs.md](https://media-lab-seven.vercel.app/skill/formats/specs.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py reframe-dark-panels # writes public/previews/reframe-dark-panels.mp4 ``` ## Source ```python """Landscape footage into 9:16 with dark panels (reads less 'edited' than blur). Footage here is a slow pan over a CC0 photo.""" import tempfile from pathlib import Path from lib import PHOTOS, ffmpeg PAN = "scale=2400:-2,crop=1920:1080:x='(iw-1920)*t/5':y='(ih-1080)/2'" def landscape_clip(tmp: Path) -> Path: src = tmp / "landscape.mp4" ffmpeg("-loop", "1", "-i", str(PHOTOS / "street.jpg"), "-t", "5", "-vf", PAN + ",fps=30", "-pix_fmt", "yuv420p", str(src)) return src def render(out: Path) -> Path: with tempfile.TemporaryDirectory() as tmp: src = landscape_clip(Path(tmp)) dst = out / "reframe-dark-panels.mp4" ffmpeg("-i", str(src), "-vf", "scale=1080:-2,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=0x0d0d0f,scale=360:640", "-c:v", "libx264", "-crf", "30", "-an", "-movflags", "+faststart", str(dst)) return dst ``` ===== docs/reframe-blur-fill.md ===== # 16:9 to 9:16 with blur fill > A blurred, darkened copy fills the background behind the centred footage. - **Category:** Reframes - **Format:** 1920x1080 in, 1080x1920 out - **Tags:** reframe, blur, vertical, ffmpeg - **Preview:** https://media-lab-seven.vercel.app/previews/reframe-blur-fill.mp4 - **Gallery:** https://media-lab-seven.vercel.app/#/p/reframe-blur-fill - **Renderer:** https://media-lab-seven.vercel.app/raw/patterns/src/reframe-blur-fill.py (uses https://media-lab-seven.vercel.app/raw/patterns/lib.py) - **GitHub:** https://github.com/HarshSalunkhe2005/Media-Lab/blob/main/patterns/src/reframe-blur-fill.py ## When to use - When dark panels look too empty - Colourful footage ## Read first - [recipes/ffmpeg.md](https://media-lab-seven.vercel.app/skill/recipes/ffmpeg.md) - [Taste and the AI-look smell test](https://media-lab-seven.vercel.app/skill/playbook/taste.md) ## Render it ```bash python patterns/render.py reframe-blur-fill # writes public/previews/reframe-blur-fill.mp4 ``` ## Source ```python """Landscape footage into 9:16 with a blurred, darkened copy behind it. Use when the panels look too empty.""" import importlib.util import tempfile from pathlib import Path from lib import ffmpeg _spec = importlib.util.spec_from_file_location("panels", Path(__file__).with_name("reframe-dark-panels.py")) panels = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(panels) BLUR = ("[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,boxblur=24:2,eq=brightness=-0.08[bg];" "[0:v]scale=1080:-2[fg];[bg][fg]overlay=(W-w)/2:(H-h)/2,scale=360:640") def render(out: Path) -> Path: with tempfile.TemporaryDirectory() as tmp: src = panels.landscape_clip(Path(tmp)) dst = out / "reframe-blur-fill.mp4" ffmpeg("-i", str(src), "-filter_complex", BLUR, "-c:v", "libx264", "-crf", "30", "-an", "-movflags", "+faststart", str(dst)) return dst ```