DEV Community

Jason Wei
Jason Wei

Posted on Edited on

Making 15-Second AI Rap Videos Reliable: Structured Prompts, a Credit Ledger, and Safe Refunds

I recently launched AI Rap Generators, a small tool that turns two adult portraits into a 15-second vertical rap video with an original verse and beat. You pick a "stage" (roast battle, birthday, pet duet, rooftop), optionally add a name or inside joke, and a few minutes later you get a 9:16 MP4.

This post is a write-up of the three things that mattered most while building it:

  1. Why I made the product narrow on purpose
  2. How I structure prompts so a video model behaves
  3. How I think about credits and failed renders when every generation costs real money

The code in this post is a simplified sketch of the patterns, not a copy of production code.

1. Narrow beats general (for a solo builder)

General-purpose text-to-video tools are impressive, but they hand the user a long list of decisions: the prompt, the camera, the music, how to keep a face consistent across shots. Open-ended input also means an unbounded number of ways to fail.

So I locked the format to two people, one stage, one rap:

  • The user only does two things: upload portraits and pick a stage.
  • I can turn almost every other decision into a per-scene template and tune for reliability.
  • The output (15 seconds, 9:16) already matches how people share on TikTok, Reels, and Shorts.

The trade-off is obvious, and I put it on the page instead of hiding it: videos are 15 seconds, there is no custom song upload yet, and it is built for two adult performers.

2. The model: Seedance 2.0

Videos are generated with Seedance 2.0, ByteDance's multimodal audio-video model. Two properties made it a good fit for this product:

  • It generates video and audio together. For a rap clip, the beat, the vocal, and the performance come out of one pass, so the mouth movement is produced alongside the sound instead of being glued on afterwards.
  • It accepts reference images. The user's portraits act as identity references (face, outfit, hair), and the stage supplies everything else.

According to the provider docs I used, clip length goes up to 15 seconds, which is exactly the length I wanted, and a single request can combine a text prompt with several reference images. Check the docs of whichever provider you call it through, since limits and parameters can differ.

3. Prompts as structured templates

My first prompts were a paragraph of prose per scene. They produced inconsistent results, and when something went wrong I could not tell which sentence caused it.

The fix was to split every scene prompt into five labelled blocks:

[Scene]        identity + environment + camera setup
[Music]        genre, tempo, instruments, vocal requirements
[Timeline]     a second-by-second action script
[Lyrics]       the exact lines to perform
[Constraints]  hard rules and a list of things that must NOT happen
Enter fullscreen mode Exit fullscreen mode

A simplified example for a rooftop stage:

[Scene]
Use reference image 1 for the single rapper's identity, outfit, hair,
and proportions. On the rooftop terrace, keep the performer centered in
a locked-off knee-up vertical 9:16 shot. Preserve the city skyline,
string lights, palms, lounge furniture, and reflective stone floor;
no other people enter.

[Music]
Confident melodic rap, 110 BPM: hard kick, snare, rolling hats, deep sub
bass, piano-and-bell hook, airy pads. One clear vocal rides a constant
radio-loud beat.

[Timeline]
0-3s: bounce on beat and point at camera. 3-6s: broad arm sweeps.
6-9s: lean in with a smile and head bob. 9-12s: step side to side,
hands wide. 12-15s: keep the energy high and freeze on the final hit.

[Lyrics, in English, perform exactly these]
1. ...
(six lines)

[Constraints]
Accurate lip-sync, one rapper only, no subtitles or text, no cuts, no
camera move, no extra people. Keep the performer in the lower three
quarters, knee-up, with hands below the top quarter.
Enter fullscreen mode Exit fullscreen mode

Why this structure works for me:

Scene: lock identity first, then framing. Saying which reference image controls identity, and fixing the shot as locked-off and knee-up, removes a lot of variation. Waist-up framing is also kinder to faces, hands, and lip sync than full-body shots.

Music: translate "vibe" into parameters. "A cool rap beat" does very little. A fixed tempo, a named instrument list, and "one clear vocal" give the model something concrete, and they let me plan the lyrics against a known rhythm.

Timeline: one action per segment. Splitting 15 seconds into five 3-second beats, each with one main movement, gives the clip visual variety and a clear ending ("freeze on the final hit") so it feels finished instead of cut off.

Lyrics: short on purpose. Six lines of roughly 7 to 10 syllables fit in 15 seconds. Longer lines are where lip sync drifts. Telling the model to perform the lines exactly also reduces rewrites.

Constraints: one rule per failure mode. This is the block that grew the most. Typical image-to-video problems and the rule I added for each:

Failure mode Constraint
Lip sync drifting Accurate lip-sync, shorter lines
Extra people appearing one rapper only, no extra people
Garbled text on signs or captions no subtitles or text
Face changing after a cut or camera move no cuts, no camera move
Hands deformed or cropped at the top of frame hands below the top quarter
Framing hidden by app UI overlays lower three quarters, knee-up

The takeaway: a longer prompt is not a better prompt. Saying clearly what should happen and what must not happen is what moved quality, and having separate blocks means I can change one thing at a time and see what it did.

4. Treat generations like payments

Every generation costs real money, and some will fail. If a user pays and gets a broken video, trust is gone. So I treat credits like a small payments system. Three rules:

  1. Hold credits before doing the work. Never call the model first and charge afterwards.
  2. Record every change in an append-only ledger with an idempotency key.
  3. Refund on terminal failure, at most once.

A simplified schema:

CREATE TABLE credit_ledger (
  id         BIGSERIAL PRIMARY KEY,
  user_id    BIGINT NOT NULL,
  delta      INT    NOT NULL,                -- negative = spend, positive = grant/refund
  kind       TEXT   NOT NULL CHECK (kind IN ('purchase','hold','refund')),
  job_id     BIGINT,
  idem_key   TEXT   NOT NULL UNIQUE,         -- one business action, one row
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE credit_balance (
  user_id BIGINT PRIMARY KEY,
  balance INT NOT NULL CHECK (balance >= 0)  -- last line of defense
);
Enter fullscreen mode Exit fullscreen mode

Creating a job and holding credits happen in one transaction. The conditional update makes the balance check and the deduction a single atomic statement:

const dec = await tx.query(
  `UPDATE credit_balance SET balance = balance - $2
   WHERE user_id = $1 AND balance >= $2
   RETURNING balance`,
  [userId, cost]
);
if (dec.rowCount === 0) throw new Error("INSUFFICIENT_CREDITS");

const job = await tx.query(
  `INSERT INTO jobs (user_id, scene_key, status, cost_credits)
   VALUES ($1, $2, 'created', $3) RETURNING id`,
  [userId, sceneKey, cost]
);

await tx.query(
  `INSERT INTO credit_ledger (user_id, delta, kind, job_id, idem_key)
   VALUES ($1, $2, 'hold', $3, $4)`,
  [userId, -cost, job.rows[0].id, `hold:job:${job.rows[0].id}`]
);
Enter fullscreen mode Exit fullscreen mode

The refund path has two guards. First, the job status moves to failed only from a non-terminal state (a compare-and-set). Second, the ledger insert uses ON CONFLICT DO NOTHING, and the balance is only increased if a row was actually inserted:

const upd = await tx.query(
  `UPDATE jobs SET status = 'failed', error_code = $2, finished_at = now()
   WHERE id = $1 AND status NOT IN ('succeeded','failed')
   RETURNING user_id, cost_credits`,
  [jobId, errorCode]
);
if (upd.rowCount === 0) return; // already in a terminal state

const ins = await tx.query(
  `INSERT INTO credit_ledger (user_id, delta, kind, job_id, idem_key)
   VALUES ($1, $2, 'refund', $3, $4)
   ON CONFLICT (idem_key) DO NOTHING RETURNING id`,
  [upd.rows[0].user_id, upd.rows[0].cost_credits, jobId, `refund:job:${jobId}`]
);

if (ins.rowCount === 1) {
  await tx.query(
    `UPDATE credit_balance SET balance = balance + $2 WHERE user_id = $1`,
    [upd.rows[0].user_id, upd.rows[0].cost_credits]
  );
}
Enter fullscreen mode Exit fullscreen mode

Duplicate callbacks, worker restarts, and manual retries can all hit this code path, and the user still gets exactly one refund. A nightly query comparing each balance against the sum of its ledger rows catches anything that slips through.

Retries are not new jobs

Failures fall into two groups. Timeouts, upstream 5xx errors, and rate limits are worth retrying with backoff. Rejected inputs and moderation failures are not. Retries reuse the original hold, so a user is never charged twice for one video.

5. Consent and privacy are product features

The tool works with real faces, so the rules are part of the product, not a footnote:

  • Everyone shown must be you or an adult who agreed to take part.
  • Images of minors and public figures are not accepted.
  • Sexual, hateful, deceptive, or harassing content is not allowed.
  • Outputs are labeled as AI-generated.
  • Uploaded photos are deleted after seven days.

I would rather lose some "make a video of a celebrity" traffic than build something that is awkward to defend. Retention promises like "deleted after seven days" are also easy to forget operationally, so I treat them as a system mechanism (storage lifecycle rules plus a database flag), not as a manual chore.

What it can't do yet

  • Videos are 15 seconds.
  • No custom song upload.
  • Two adult performers only.
  • It is AI, so likeness to the photos varies.

Credits are 10 per video, packs start at $6.99, there is no subscription, and failed renders are refunded automatically.

What I'd like feedback on

  • Which stage or scene would you want next?
  • If you have built on a video model: what other failure modes should I be writing constraints for?
  • Does the credit pricing feel fair?

You can browse every stage for free at airapgenerators.com. If you try it, I would love to hear where it breaks.

Top comments (0)