Case studies/Media, tools & data
Video automation · Instagram Reels to narrated Shorts

22sGems on YouTube

2026 - now

Every specimen at 22sGems appears first as an Instagram Reel with a licensed music track, and this pipeline republishes each one as a YouTube Short that speaks: the music is stripped and replaced by a narration in a clone of the owner's own voice, made from his recordings with his consent and disclosed to YouTube as synthetic on every upload.

Overview

22sGems is Jon Singer's fine-mineral brand, and every specimen appears first as an Instagram Reel: a hand turning a crystal in the light, a licensed music track underneath, and a caption written like a voice-over. This project takes each of those Reels to YouTube as a Short that speaks. The music is gone, and in its place is a narration in a clone of the owner's own voice, reading his own caption. It runs unattended, and the channel now holds more than 400 videos.

The music has to go because Instagram's licence does not travel. The Instagram Graph API withholds the video file for Reels with copyright-flagged audio - on this account that was every Reel measured, 53 of 53 - so the download falls back to the public reel page, and a Reel uploaded to YouTube with its Instagram track would be claimed by Content ID. The audio is therefore stripped at transcode, and the narration is a replacement track, never a mix over the original.

The voice is an ElevenLabs Instant Voice Clone of the owner, made from recordings he supplied and with his consent. A stock voice was the first plan; his own won the comparison, because a dealer's voice is the one thing on these videos a competitor cannot copy. He sent a 56-second phone recording and then three cleaner takes, and the production voice is built from those three, 2 minutes 57 seconds of speech. The first take was left out on purpose: its noise floor sat 12 to 15 dB higher, and a clone learns the room along with the voice. It is a clone and not a recording, and it is labelled as one: every narrated upload carries YouTube's altered or synthetic content disclosure.

There is no language model in the narration path. The owner's captions are already scripts - a hook, two or three beats of prose, then a block of specifications - so the code keeps the hook and the prose, adds a spoken identity line and a call to action that depends on whether the piece is for sale, and trims the result to a character budget derived from the real length of the video. Then it makes the text speakable: dashes become pauses, hashtags and emoji go, abbreviations are spelled out, shouted headlines are title-cased, a leading weight becomes its own sentence, and a small pronunciation dictionary fixes the words the engine gets wrong, each entry chosen by ear from numbered A/B clips.

The rest is audio engineering. The ElevenLabs v3 model ignores the speed setting, so pace is set afterwards: ffmpeg's atempo filter, with a ratio computed for every render from its measured words per minute toward a target of 115 and clamped between 0.8 and 1.35. Loudness is normalised in two passes to -16 LUFS, the voice gets 0.6 seconds of silence before the first word and 1.6 after the last, and the picture is never cut to fit the voice: if the narration runs longer the last frame holds, and if the footage runs longer it plays on in silence. A quality gate rejects a render whose pace misses the target, whose stretch ratio hit its clamp or whose read looks truncated, and a rejected file is renamed so nothing downstream can pick it up.

Publishing goes through the YouTube Data API as plain REST calls: OAuth, a resumable upload, a title built as mineral, locality and weight, a description with a Buy link to the Shopify product and a link to the specimen's gallery page, and subtitles as a real caption track. Afterwards the video id is written back to the Shopify product and to a map in R2 that the gallery reads at runtime. Shorts that went up silent before the voice existed were replaced one by one, most-viewed first, because YouTube offers no way to change the audio of a published video. The whole thing runs in a Cloudflare Container started by a Worker on cron triggers: two full passes a day and an hourly pass that only works the YouTube queue.

What was built

Stack

Pipeline

Python, standard library only
Watcher, script builder, narration, uploader and replacement tools; HTTP through urllib, no SDKs for ElevenLabs or Google.
ffmpeg
Transcode without audio, atempo pace correction, two-pass loudnorm, delay, pad and fade, H.264 and AAC encode with faststart.
yt-dlp
Fallback download from the public reel page when the Graph API withholds the media URL.
whisper.cpp
Used once, to backfill subtitles for renders made before alignment was stored: its word timings matched against the real script.

AI voice

ElevenLabs text to speech
The eleven_v3 model through the with-timestamps endpoint, MP3 at 44.1 kHz and 192 kbps; stability 0.5, similarity 0.8, style 0.
ElevenLabs Instant Voice Clone
The owner's voice from three clean phone takes, 2 minutes 57 seconds in total, prepared as mono 44.1 kHz with a high-pass and loudness normalisation and nothing else.

Platforms & APIs

Instagram Graph API v23
Instagram API with Instagram Login on the project's own Meta app, a long-lived token refreshed on every run.
YouTube Data API v3
videos.insert with resumable upload, videos.update for status and metadata, captions.insert for subtitles, videos.list for statistics and file details.
Shopify Admin GraphQL
Writes the product's YouTube URL metafield, which drives the Watch on YouTube link and the product's VideoObject markup.

Infrastructure & DevOps

Cloudflare Containers + Durable Objects
One container instance behind a Worker, on a Debian Node 22 image with Python, ffmpeg, git, yt-dlp and the AWS CLI.
Cloudflare Workers cron triggers
Two full passes a day plus an hourly YouTube-only pass, a status page, a manual sync and a log tail.
Cloudflare R2
Authoritative state file, tokens, silent masters, narrated renders and the video map the gallery reads.
GitHub
The watcher commits each ingest to the repository, and the gallery deploys to Cloudflare Pages from it.

Frontend

Astro on Cloudflare Pages
The specimen gallery that embeds each piece's current YouTube video from the runtime map.
Shopify theme (Liquid)
Product pages with the narrated video and the YouTube link, and a click-to-play section for collection tours.

Engineering highlights

YouTube has three separate walls, and the documentation describes none of them the way they behave: a per-channel upload allowance that phone verification lifted, the 10,000-unit daily API pool that videos.insert turned out not to be charged against, and a separate uploads-per-day metric that answers with a 429. All three reset at 07:00 UTC. The watcher now stops quietly on any of them and records Google's own error reason verbatim next to the day's count, because limits inferred from documentation had twice been wrong.

Two API behaviours decide whether a disclosure survives. videos.insert silently drops the synthetic-media flag, so every upload is followed by a videos.update that sets it; and videos.update replaces the whole status object, so making an old video private without echoing back the made-for-kids and synthetic-media declarations would silently undo them.

Pace is corrected with atempo and never with a resample. Measured on this voice at 1.24x, atempo moved the same passage from 132.2 Hz to 129.0 Hz, while a sample-rate change would have carried it to 164 Hz, a different and much younger man. The ratio is computed per render, because one fixed ratio does not travel between specimens.

Subtitles come from where each character actually landed in the audio. Spreading the script evenly over the duration was measured first and put the first cue 3.4 seconds late on a 62-second read; the alignment that ElevenLabs returns at no extra cost, rescaled by the tempo ratio and the lead-in, puts it where the word is.

State is written before it is verified. The upload is recorded and pushed to R2 immediately after it succeeds, before any call that can fail, so a container killed mid-run cannot publish the same piece twice; and a replaced Short is made private rather than deleted, which is reversible and does not look like mass deletion to the platform.

A filter chain that ffmpeg 9 on a laptop negotiated silently was refused by ffmpeg 5.1 in the container image. Two rules came out of it: every ffmpeg failure is raised with its stderr rather than its command line, and anything that was paid for is cached before the step that can fail.