I shipped Judge Me, a small Android app where people read "Am I the jerk?"-style dilemmas and vote: you were right, you were wrong, both of you messed up, or it's complicated. The app works. Nobody knows it exists.
So I'm running a 14-day experiment: short vertical videos on TikTok and Instagram Reels, one per day, each telling one dilemma from the app and ending with "go vote." The goal is simple and measurable: 500+ Play Store installs in 14 days, or one video over 50k views. If neither happens, I stop and try something else.
This post is about the boring-but-interesting part: how I made producing a video take ~5 minutes of my time instead of an hour, with no paid tools.
Each video looks like this:
| Time | What's on screen |
|---|---|
| 0–2 s | Big hook text (first sentence of the story) |
| 2 s → end−4 s | Background clip + AI voiceover + 2–3 word captions |
| Last 4 s | "Right or wrong? YOU decide." + screenshot of the app's vote screen |
Scripts are 100–130 words, first person, short sentences, ending with "So… was I wrong?". I use edge-tts, which is free and — crucially — streams word boundary events with timestamps alongside the audio.
comm = edge_tts.Communicate(text, "en-US-GuyNeural", rate="+12%", boundary="WordBoundary")
async for chunk in comm.stream():
if chunk["type"] == "audio":
f.write(chunk["data"])
elif chunk["type"] == "WordBoundary":
start = chunk["offset"] // 10_000 # 100ns ticks → ms
words.append({"text": chunk["text"], "start_ms": start,
"end_ms": start + chunk["duration"] // 10_000})
That gives me an MP3 plus a JSON list of every word and when it's spoken. Captions come for free.
My first captions looked like star She launched and reviews So I — chunks bleeding across sentences. The boundary events return bare words, so my "break on punctuation" rule never fired.
Fix: re-align the timed words against the original script tokens (which do have punctuation), then chunk in two passes — split on clause boundaries first, then divide each clause into balanced groups so you never get a lonely one-word chunk:
def chunk_words(words, max_words=3, max_chars=20):
clauses, cur = [], []
for w in words:
cur.append(w)
if w["text"][-1:] in ".!?,;:…":
clauses.append(cur); cur = []
if cur: clauses.append(cur)
chunks = []
for cl in clauses:
k = -(-len(cl) // max_words) # ceil
while True: # balance sizes, respect char limit
size, extra = divmod(len(cl), k)
parts, i = [], 0
for j in range(k):
step = size + (j < extra)
parts.append(cl[i:i+step]); i += step
if all(len(" ".join(x["text"] for x in p)) <= max_chars for p in parts) or k >= len(cl):
break
k += 1
chunks.extend(parts)
return chunks
Before: They showed up | late.
After: They showed | up late.
Small thing, but captions are most of what people actually read on mute.
This is the part that made the whole pipeline possible. CapCut desktop stores each project as a folder containing draft_content.json — tracks, segments, materials, text styles, all in plain JSON. (Some versions encrypt it; mine, 9.6 on Windows, didn't. Check yours before building on this.)
The structure, simplified:
tracks[] → video / audio / text tracks, each with segments[]
segments[] → material_id + target_timerange {start, duration} in µs + clip transform
materials.texts[] → text content (as an embedded JSON string!) + style
materials.videos[] → file paths for clips and images
materials.audios[] → file paths for audio
My approach:
HOOK_TEXT, SUB_1, VOTE_TEXT, plus the background, audio, and vote-screen image.
The one non-obvious detail: the text content lives in a JSON string inside the JSON, with a character range for styling. Change the text without updating the range and the styling breaks.
def set_text(mat, text):
c = json.loads(mat["content"])
c["text"] = text
for st in c["styles"]:
st["range"] = [0, len(text)]
mat["content"] = json.dumps(c, ensure_ascii=False)
Then I open CapCut, glance at the timeline, and hit Export. That's my whole manual step.
Once one video worked, I generated 10 separate CapCut projects in one go — copy the template folder, give it a new draft_id, register it in CapCut's root_meta_info.json, render the content. CapCut picked them all up in its project list. Ten videos, ten exports, done in one sitting.
.locked file in the project folder tells you it's open.
My first attempt was to have an AI agent with desktop control build the template inside CapCut by clicking. It was slower and flakier than I expected: CapCut's media panel didn't respond to synthetic mouse events at all (couldn't drag a text layer onto the timeline), and the agent's own window kept stealing focus. After a few rounds I switched to "just edit the files," which took less time than the clicking attempts had.
Lesson: if an app has a readable file format, editing the file beats automating the UI almost every time.
Plan said "link in bio." TikTok said no — clickable bio links need 1,000 followers on a personal account, and the business account switch wasn't available to me without verification. Plain-text URLs in the bio aren't clickable.
So the end card now says "Vote free: search 'Judge Me AITA' on Google Play" — I checked that exact search ranks the app first. The downside: I lose UTM attribution on TikTok and have to infer it from Play Console's search-traffic numbers. Instagram still gets a proper tracked link.
The experiment starts this week. I'll post the results — views, installs, what hooks worked, and whether 03:00 scheduling actually mattered — when the 14 days are up, including if it flops.
If you're an indie dev sitting on an app nobody knows about, the takeaway isn't "use CapCut." It's: find the fixed structure in your marketing content, and turn the repetitive part into data.
Judge Me is on Google Play. I build in public as @fatihbuilds.