Building Case Analyst · Part 7 of 8
The AI asset pipeline of a solo indie game
TL;DR — Case Analyst ships 11 mission videos, dozens of character portraits, voice clips and a full trailer set — all produced by one part-time dev. Voices came from ElevenLabs, stills and character design from MidJourney + ChatGPT, video from Seedance/Kling/Luma Dream Machine, code help from Claude Code, and the brand-consistent marketing cut-downs from a couple of ffmpeg + ASS/libass shell scripts. Here's the honest pipeline, including the parts that broke.
There's a scene in the game where a video of agent Nova Scarlet plays on your terminal — except the video is a deepfake, and your job is to prove it. Building that scene meant generating a believable talking-head clip, a matching voice, a still portrait for the case file, and the on-screen text that frames it. Four different tools, four different failure modes, one person at a kitchen table.
This part of the series isn't about Dart. It's about how a solo dev fills a game with assets that would normally need a small studio — and where the seams actually are. I'll be specific about tools and costs, because that's the post I wish I'd found.
Voices: ElevenLabs
Every spoken line in the game is synthetic. Inside the repo, the mission folders carry the evidence: ghost_protocol/audio/viper7_first_voice.mp3, panopticon/audio/coordinator_voice.mp3, and — my favourite trick — panopticon/audio/merged_voice.mp3, the "cleaned" forensic version of the Coordinator's voice that the player's analysis confirms as Director Morrison (a 99.7% match). The asset isn't just a sound file; it's the payoff of a deduction.
ElevenLabs was the workhorse. The workflow was unglamorous: write the line in the script, pick a consistent voice per character, render, listen, and re-render when the prosody fought the line. The hard part was never the first take — it was consistency across a 21-mission arc. A character who appears in mission 2 and again in mission 19 has to sound like the same person, so I locked one voice ID per character early and never touched it. In the generation scripts that's literal: a small VOICES map pins each character to a fixed ElevenLabs voice ID, with a comment next to VIPER-7 noting it's the same voice used back in Ghost Protocol "for consistency."
Two practical notes. First, short interjections ("…run.") are where TTS breaks character most — it wants to smooth them, so I'd pad with punctuation and trim the silence afterward. Second, the files are plain MP3 dropped into assets/missions/<mission>/audio/ and played through audioplayers; nothing about the engine cares that they're AI-made. That decoupling is the whole point — the game treats an ElevenLabs clip exactly like a clip a voice actor would have handed me.
Stills & character design: MidJourney + ChatGPT
The cast — Nova Scarlet, the rogue AI PANOPTICON, the vanished agent known as The Ghost (Viper-7) — was designed in MidJourney, with ChatGPT used upstream to turn loose narrative notes into tight, repeatable prompts. ChatGPT was the art director I couldn't afford: I'd paste a character's backstory and ask for a prompt that nailed wardrobe, lighting and a cyberpunk-noir palette, then iterate the seed in MidJourney until the face was consistent enough to reuse across a "case file."
What worked: getting from "I need a 50-year-old ex-handler with a guilty conscience" to a usable portrait in an evening, for the cost of a subscription. What needed manual fixing: hands, text on props, and the fact that MidJourney does not remember a character — keeping a face stable across angles is a battle of seeds, image prompts and patience. For a detective game where the player stares at the same faces for hours, that consistency mattered more than raw quality.
Video: Seedance, Kling & Luma Dream Machine
The game has 11 mission videos — surveillance footage, an arrest (v7_arrest.mp4), a briefcase handoff, the PANOPTICON boot sequence, the deepfake (deepfake_nova.mp4). I used Seedance and Kling for image-to-video and text-to-video shots — feeding a MidJourney still in and getting a few seconds of motion out — and Luma Dream Machine (Luma Labs) for parts of the talking-head / lip-sync work. The exact division of labour shifted as each tool's quality moved month to month; what stayed constant was the shape of the pipeline, not the vendor.
The honest part: generated video is short, and it drifts. You get a clean 5–10 seconds, then faces wobble and physics give up. So I designed the scenes around the limitation — grainy CCTV, tight crops, hard cuts, "corrupted feed" framing. In a game about surveillance and fakes, low-fidelity AI footage isn't a compromise, it's the aesthetic. The deepfake mission is the cleanest example: the asset being slightly off is literally the puzzle.
Code: Claude Code
The whole game is hand-written Flutter (see Part 1), but I leaned on Claude Code for the parts a solo dev has no second pair of eyes for: reviewing the Riverpod provider graph, hunting state-leak bugs across the mission engine, and optimizing hot widgets. It was also how I built the marketing toolchain below — I described the brand look and let it draft the ffmpeg filtergraphs, then I fixed what didn't survive contact with reality.
The mental model that worked: AI writes the boilerplate and the ffmpeg incantations, I own the architecture and the taste. It never touched a design decision. It saved me from re-reading the libass docs at midnight.
The trailer pipeline: ffmpeg + ASS/libass
Marketing cut-downs needed to look on-brand: the same green-on-black terminal type, the same Share Tech Mono font as the game, every time, across nine aspect ratios. I did not want to open a video editor for each post. So the trailers are generated by shell scripts — produce_full_video.sh, produce_batch.sh, produce_images.sh — that drive ffmpeg and burn text via an ASS subtitle file rendered by libass.
Why ASS instead of ffmpeg's drawtext? Because drawtext and I had a falling-out over the % character — my "-15% LAUNCH DISCOUNT" end-card kept breaking, since % is a format specifier in drawtext's expansion. ASS treats text as text. It also gives real typographic control (letter-spacing, outlines, positioning) and lets me ship the brand font with fontsdir. Here's the actual style + event block the video engine writes per clip:
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, ...
Style: Hook,Share Tech Mono,54,&H00FFFFFF,&H00000000,...
Style: Head,Share Tech Mono,96,&H0014FF39,&H00000000,... # FDI green
Style: Sub,Share Tech Mono,58,&H00FFFFFF,...
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.00,0:00:03.50,Hook,,0,0,0,,{\pos(540,180)}$hook
Dialogue: 0,$ts,9:59:59.00,Head,,0,0,0,,{\pos(540,800)}CASE ANALYST
Dialogue: 0,$ts,9:59:59.00,Sub,,0,0,0,,{\pos(540,940)}$cta
That ASS file then gets composited over the game footage. The vertical (9:16) clips use a blurred-fill background so a 16:9 source never shows letterbox bars — split the stream, scale one copy up and Gaussian-blur it for the backdrop, scale the other to fit, overlay centered:
ffmpeg -i "$src" -filter_complex \
"[0:v]split=2[a][b];\
[a]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=26[bg];\
[b]scale=1080:1920:force_original_aspect_ratio=decrease[fg];\
[bg][fg]overlay=(W-w)/2:(H-h)/2[v]" \
-map "[v]" -map 0:a -c:v libx264 -crf 20 -pix_fmt yuv420p -c:a copy "$base"
Then a second pass darkens an intro strip for the hook, fades to a black end-card, and burns the subtitles. One subtlety I paid for in time: in this static ffmpeg build, subtitles= segfaults inside -filter_complex but works fine in -vf — so the blurred-fill and the text burn are deliberately split into two passes. The script even carries the comment to remind future-me.
The still images (Instagram carousels, character cards, countdown cards) run the same machinery with -frames:v 1 — one ffmpeg invocation per slide, text via ASS, the same font and palette. The "classified personal file" cards you saw above are produced exactly this way, straight from the MidJourney portrait.
The one-character gotcha worth remembering
Every script starts with export LC_ALL=C. On my Italian-locale Mac, the decimal separator is a comma, and awk happily wrote subtitle timestamps like 0:00:30,50 — which ASS reads as garbage, so the end-card text would silently vanish. Forcing the C locale makes the whole toolchain use a dot. A comma cost me an afternoon. It's right there at the top of every file now, with a comment, so it never happens again.
What this actually cost
Concretely: a handful of monthly subscriptions (ElevenLabs, MidJourney, the video tools, an LLM) running in parallel for the production months, plus a free static ffmpeg binary and a free brand font. Call it the price of a few dinners out per month while shipping — versus the impossibility of commissioning voice actors, a concept artist, a motion designer and a video editor on a part-time indie budget.
The trade I keep coming back to: AI didn't make the assets better than a studio would. It made a whole category of assets exist at all for a project that otherwise couldn't have afforded them. And the trick that made it cohere wasn't any single model — it was building dumb, repeatable scripts so that the brand stayed identical across 11 videos, 20-odd cards and nine aspect ratios, no matter how many tools I swapped underneath.
Takeaway: treat AI tools as interchangeable suppliers and invest your engineering effort in the pipeline around them — version-controlled scripts, one locked voice per character, one font, one filtergraph. The models will keep changing. Your produce_*.sh shouldn't have to.
The series
- Building a detective game in pure Flutter (no game engine)
- An in-game terminal with a real command parser
- A forensic image analyzer: enhance, thermal & deepfake detection
- Field Link: a data-driven branching dialogue engine
- A data-driven mission & narrative engine (21 missions)
- One codebase to iOS, Android, macOS, Windows & Steam
- The AI asset pipeline of a solo indie game
- Shipping a Flutter game solo: what worked, what I'd change
Case Analyst is a cyberpunk detective game where you investigate by typing real commands in a terminal — out on mobile, coming to Steam on October 29. Wishlist it on Steam, and follow for the next part.
Where to find Case Analyst
- Official site: case-analyst.com
- Steam (launches October 29 — wishlist now): store.steampowered.com
- iOS: App Store · Android: Google Play
- Follow: YouTube · Instagram · TikTok · Discord