Fatih İlhan 示範直接修改 CapCut 專案檔,自動製作 Judge Me 宣傳短片
I Automated a TikTok Campaign for My App by Editing CapCut's Project Files Directly
Fatih İlhan 直接修改 CapCut 桌面版專案 JSON,配合 edge-tts 產生語音和逐字時間戳,將 Judge Me 宣傳短片製作流程自動化。他使用的 Windows 版 CapCut 9.6 專案檔未加密,並以模板批量建立 10 個專案;完成設定後,每段影片約需 5 分鐘人工檢查、匯出及排程,工具成本為 $0。
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.
The constraints
- Solo dev with a full-time day job. Video editing time is near zero.
- No budget for AI video SaaS subscriptions.
- English content aimed at a US audience, posted from Turkey (so everything gets scheduled for 03:00 my time).
- Every video has the same structure, which is the key insight: if the structure is fixed, the editing is a data problem.
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 |
Step 1: Script → voice + word timestamps
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.
Gotcha: word boundaries have no punctuation
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.
Step 2: CapCut projects are just JSON
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:
- Build one template project by hand with placeholder text layers:
HOOK_TEXT,SUB_1,VOTE_TEXT, plus the background, audio, and vote-screen image. - Let CapCut open and re-save it once, so the file is in its own canonical format.
- Keep that folder as a read-only master.
- For each video, a Python script copies the template's JSON and rewrites it:
- swap the audio and background paths
- set every duration to the MP3 length (in microseconds)
- put the first sentence into
HOOK_TEXT -
clone the
SUB_1layer once per caption chunk, each with a fresh UUID and the chunk's timing - set the vote text and move it to the last 4 seconds
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.
Batch mode
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.
Rules I learned the hard way
-
CapCut must be fully closed when you write. If it's open, it autosaves over your changes. A
.lockedfile in the project folder tells you it's open. - Back up before every write. My script copies the target project to a backup folder first, every time.
- Text wrapping isn't guaranteed. My first hook ran off both edges of the screen — CapCut ignored the max line width I set. I now wrap the hook myself into balanced lines of ≤20 characters before writing it.
- Pin the CapCut version and turn off auto-update. This is an undocumented format. An update can break everything.
What didn't work: letting an AI agent click through CapCut
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.
Distribution gotcha: no link in bio
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 setup, in numbers
- Cost: $0 (edge-tts, CapCut free tier, Pexels backgrounds)
- My time per video after setup: ~5 minutes (open project, check, export, schedule)
- Stock ready before day 1: 10 videos
- Posting: one per day at 03:00 Turkey time (evening in the US), scheduled
What's next
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.
來源:DEV Community · dev.to