<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thomas Chan</title>
    <description>The latest articles on DEV Community by Thomas Chan (@thomaschan).</description>
    <link>https://dev.to/thomaschan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4218205%2F9b6a3bce-9951-46a7-aff6-d8e7f24a5b7b.png</url>
      <title>DEV Community: Thomas Chan</title>
      <link>https://dev.to/thomaschan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thomaschan"/>
    <language>en</language>
    <item>
      <title>A Meme Is a Spec: Breaking a 14-Second 9:16 Video Into Beats, Timecodes, and a Posting Checklist</title>
      <dc:creator>Thomas Chan</dc:creator>
      <pubDate>Sat, 31 Oct 2026 03:46:50 +0000</pubDate>
      <link>https://dev.to/thomaschan/a-meme-is-a-spec-breaking-a-14-second-916-video-into-beats-timecodes-and-a-posting-checklist-4o7m</link>
      <guid>https://dev.to/thomaschan/a-meme-is-a-spec-breaking-a-14-second-916-video-into-beats-timecodes-and-a-posting-checklist-4o7m</guid>
      <description>&lt;p&gt;When I started building a small generator for a TikTok meme format, I thought the hard part would be the model. It wasn't. The hard part was working out &lt;em&gt;what the meme actually is&lt;/em&gt;, precisely enough that a pipeline could reproduce it every time.&lt;/p&gt;

&lt;p&gt;Meme formats look like vibes, but they behave like specs. Drop a beat or shift a timecode by a second and the joke stops working. This post is the spec I ended up with for one format, the &lt;strong&gt;window peeper&lt;/strong&gt; trend, plus the ffmpeg checks I run before anything gets posted. Even if you never touch this meme, the method (treat a format like an interface contract) carries over to any short-form video work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The format
&lt;/h2&gt;

&lt;p&gt;Night exterior. A camera slowly pushes toward the only lit window in a dark building. Inside, someone is eating a burger. They notice the camera, freeze mid-chew, stare into the lens, then pull the curtain closed. The first matching clips appeared on TikTok on 20 October 2026, usually paired with an original sound credited to @therealrodentspawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Write the beats as a timeline
&lt;/h2&gt;

&lt;p&gt;Here's the beat sheet for a 14-second version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Timecode&lt;/th&gt;
&lt;th&gt;Beat&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;00:00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Block&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wide establishing shot, one window lit, everything else dark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;00:01&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Push-in&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slow, continuous camera move toward the window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;00:04&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Bite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subject eating, unaware, readable at phone size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;00:07&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Stare&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subject looks up into the lens, keeps chewing, holds it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;00:13&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Curtain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subject closes the curtain; video ends&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Writing it this way showed me three things that weren't obvious from watching clips:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The stare is the payload.&lt;/strong&gt; It gets roughly six seconds, nearly half the runtime. Shortened versions (the popular CapCut template is about 8 seconds) mostly cut it here, and you can feel it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The curtain is a hard end.&lt;/strong&gt; Nothing comes after it, no outro and no text card. Adding anything after 00:13 kills the punchline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first second carries the context.&lt;/strong&gt; Without the wide shot, a viewer doesn't know they're the one watching, and the stare loses its target.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building templates, prompts or a generation pipeline, this table is your acceptance test. A render passes only if every beat lands roughly on time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Define the input contract
&lt;/h2&gt;

&lt;p&gt;My generator takes one photo, and the photo decides most outcomes. Here's the input contract I settled on, written as validation rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;faces        == 1            # extra faces leak into the scene
eyes_visible == 2            # the payload is a stare
framing      &amp;gt;= chest_up     # the outfit is part of the joke
lighting     == front, even  # side light causes face drift
source       == original     # not a screenshot of a forward
min_side_px  &amp;gt;= 1000
expression   == neutral      # a smiling stare is a different joke
subject      == consenting adult
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last rule isn't a technical constraint, but it belongs in the contract anyway. The format is a joke about being watched. Making a realistic video of someone who didn't agree turns it into the real thing. In my product, uploads are reviewed and renders that break this rule are removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Define the output contract
&lt;/h2&gt;

&lt;p&gt;Short-form platforms are forgiving about input, but the feed rewards a specific shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aspect ratio:&lt;/strong&gt; 9:16 vertical. Anything else gets letterboxed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; about 14 s for this format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container/codec:&lt;/strong&gt; MP4 with H.264 is the safe default for uploads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No baked-in trend audio.&lt;/strong&gt; The sound is an in-app original sound. Ship the clip with ambience only and let the creator attach the trend audio inside TikTok or Reels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No watermark&lt;/strong&gt; if you want people to post it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Verify with ffprobe before posting
&lt;/h2&gt;

&lt;p&gt;I don't trust a render until it passes a scripted check. &lt;code&gt;ffprobe&lt;/code&gt; gives you everything you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; error &lt;span class="nt"&gt;-select_streams&lt;/span&gt; v:0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;width,height,codec_name,r_frame_rate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;duration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-of&lt;/span&gt; &lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;noprint_wrappers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 render.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then assert on the output: &lt;code&gt;height / width&lt;/code&gt; should be about 1.78 (16/9), duration should be close to 14, and the codec should be &lt;code&gt;h264&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two more one-liners I use all the time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Grab the stare frame (00:07) as a thumbnail / cover candidate&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-ss&lt;/span&gt; 00:00:07 &lt;span class="nt"&gt;-i&lt;/span&gt; render.mp4 &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 stare.jpg

&lt;span class="c"&gt;# Contact sheet: one frame per beat, to eyeball timing in a single image&lt;/span&gt;
ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; render.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"select='eq(n&lt;/span&gt;&lt;span class="se"&gt;\,&lt;/span&gt;&lt;span class="s2"&gt;0)+eq(n&lt;/span&gt;&lt;span class="se"&gt;\,&lt;/span&gt;&lt;span class="s2"&gt;30)+eq(n&lt;/span&gt;&lt;span class="se"&gt;\,&lt;/span&gt;&lt;span class="s2"&gt;120)+eq(n&lt;/span&gt;&lt;span class="se"&gt;\,&lt;/span&gt;&lt;span class="s2"&gt;210)+eq(n&lt;/span&gt;&lt;span class="se"&gt;\,&lt;/span&gt;&lt;span class="s2"&gt;390)',scale=270:-1,tile=5x1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 &lt;span class="nt"&gt;-vsync&lt;/span&gt; vfr beats.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The frame numbers in the contact-sheet filter assume 30 fps. Multiply each timecode by your actual frame rate.)&lt;/p&gt;

&lt;p&gt;The contact sheet is the most useful habit I've picked up. Five thumbnails side by side tell you at a glance whether the stare happens at the stare beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Treat posting as part of the pipeline
&lt;/h2&gt;

&lt;p&gt;A perfect render can still flop because of how it's posted. My posting checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Trend sound attached in-app (or deliberate near-silence)&lt;/li&gt;
&lt;li&gt;[ ] Caption plays along with the joke instead of explaining it ("who is filming my window at 2am")&lt;/li&gt;
&lt;li&gt;[ ] 3â€“5 hashtags, not 30 (#windowpeeper #windowpeeperai #windowpeeperfilter plus one or two broad tags)&lt;/li&gt;
&lt;li&gt;[ ] Platform AI label turned on: TikTok "AI-generated content", Instagram "AI info", YouTube "Altered or synthetic content"&lt;/li&gt;
&lt;li&gt;[ ] Posted in the evening, since a night-window clip fits a night-time feed (roughly 7pmâ€“1am local)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep longer &lt;a href="https://aiwindowpeeper.app/blog/window-peeper-sound-and-hashtags" rel="noopener noreferrer"&gt;sound and hashtag notes&lt;/a&gt; on my project blog if you want the reasoning behind each item.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell another dev building for a trend
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write the spec before the pipeline.&lt;/strong&gt; Watching ten clips and writing a beat table took an hour and saved days of tuning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate inputs aggressively.&lt;/strong&gt; Most "the AI is broken" reports were really input-contract violations: sunglasses, group photos, side lighting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund failures automatically.&lt;/strong&gt; If a render fails, give the credits back without making anyone ask. It's cheap and it builds trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan for the trend to end.&lt;/strong&gt; A single-format product has a shelf life, so keep the scope and the costs small.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project this came from is &lt;a href="https://aiwindowpeeper.app/" rel="noopener noreferrer"&gt;Window Peeper AI&lt;/a&gt;: one photo in, a 14-second 9:16 MP4 out, with no watermark, using pay-per-video credits and no subscription. But the spec-first approach is the part worth stealing.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: I'm the developer of Window Peeper AI, an independent project not affiliated with TikTok or CapCut. This article was written with AI assistance and reviewed and edited by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>video</category>
      <category>ffmpeg</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
