November 5, 2026. I build hittheroaddude.app, a small paid studio that remakes the Hit the Road Dude pickup-window clip from one photo. The script below was tested on synthetic clips made with ffmpeg's testsrc2 and flite. No generated clips are shown or claimed here.
A question I keep seeing: "Can I put my own voice on the 'State?' line?" People make a remake of the State Property clip, like the picture, and want one line to sound like them. Or they want their friend to say "hit the road, dude" in their own voice.
You can't do that on my site. The voice is part of the saved preset. The FAQ says "The voice is newly generated for your clip. This studio does not put the original recording in the file and does not lip-sync you to it" (FAQ). Once you have the MP4, though, it's your file, and replacing a single line is a small, well-defined audio edit. In audio engineering it's called a punch-in: you keep everything before and after a window and record over the window.
This post is the punch-in as a ~70-line Python script around one ffmpeg filter graph, plus a check that proves it replaced what you think it replaced.
What has to stay true
The Hit the Road Dude joke is timing. The four beats run in order: the claim about state property, a puzzled "State?", the correction to private property, then the send-off (how the exchange runs). A punch-in must not move any of them. That gives four rules:
-
The video stream is not touched.
-c:v copy. No re-encode, no shifted frames. - The output is exactly as long as the input. If your recording is shorter than the window, pad it with silence. If it's longer, cut it. Never let it push the rest of the clip later.
- No clicks at the seams. Cutting audio mid-waveform makes a pop. A 10–20 ms fade on each side of each seam fixes it.
-
Same sample rate and channel layout as the original, so
concatdoesn't complain and players don't resample oddly.
Find the window
You need a start and an end time in seconds. Open the clip in any editor that shows a waveform (Audacity, Reaper, even a phone editor), find "State?", and note where the word starts and where the next line begins. Give yourself a little room on both sides. In my synthetic test clip, "State?" starts at 4.5 s, so I used the window 4.4–6.0 s.
Record your line as a WAV. Any length is fine, but try to say it in about the same time as the original word, because the script won't stretch you to fit. It only pads or cuts.
The script
#!/usr/bin/env python3
"""Replace one line of a clip's audio with your own recording (a "punch-in").
The video stream is copied untouched. The audio before START and after END is
kept; the window START..END is replaced by your recording, which is trimmed or
padded with silence to fit exactly, with short fades at both seams so there are
no clicks. The output has the same duration as the input.
usage: punch_in.py CLIP.mp4 MINE.wav START END OUT.mp4 [--fade-ms 15] [--gain-db 0]
"""
import argparse, json, subprocess, sys
def probe(path):
out = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
"format=duration:stream=codec_type,sample_rate,channels",
"-of", "json", path], capture_output=True, text=True, check=True).stdout
j = json.loads(out)
a = next((s for s in j["streams"] if s["codec_type"] == "audio"), None)
return float(j["format"]["duration"]), a
def main():
ap = argparse.ArgumentParser()
ap.add_argument("clip"); ap.add_argument("mine")
ap.add_argument("start", type=float); ap.add_argument("end", type=float)
ap.add_argument("out")
ap.add_argument("--fade-ms", type=float, default=15.0)
ap.add_argument("--gain-db", type=float, default=0.0)
a = ap.parse_args()
dur, astream = probe(a.clip)
if astream is None:
sys.exit("clip has no audio stream")
if not (0 <= a.start < a.end <= dur):
sys.exit(f"window {a.start}-{a.end} is outside the clip (0-{dur:.3f})")
sr = int(astream["sample_rate"]); ch = int(astream["channels"])
mine_dur, _ = probe(a.mine)
win = a.end - a.start
if mine_dur > win:
print(f"note: recording is {mine_dur:.3f}s, window is {win:.3f}s; the tail is cut", file=sys.stderr)
f = a.fade_ms / 1000.0
fmt = f"aresample={sr},aformat=sample_fmts=fltp:channel_layouts={'mono' if ch == 1 else 'stereo'}"
graph = ";".join([
f"[0:a]{fmt},asplit=2[o1][o2]",
# head: 0..start, fade out at the seam
f"[o1]atrim=0:{a.start},asetpts=N/SR/TB,afade=t=out:st={max(a.start - f, 0)}:d={f}[head]",
# tail: end..dur, fade in at the seam
f"[o2]atrim={a.end}:{dur},asetpts=N/SR/TB,afade=t=in:st=0:d={f}[tail]",
# your line: fit exactly into the window
f"[1:a]{fmt},volume={a.gain_db}dB,apad,atrim=0:{win},asetpts=N/SR/TB,"
f"afade=t=in:st=0:d={f},afade=t=out:st={win - f}:d={f}[mid]",
"[head][mid][tail]concat=n=3:v=0:a=1[aout]",
])
cmd = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", a.clip, "-i", a.mine,
"-filter_complex", graph, "-map", "0:v", "-map", "[aout]",
"-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-t", f"{dur}",
"-movflags", "+faststart", a.out]
subprocess.run(cmd, check=True)
new_dur, _ = probe(a.out)
print(f"wrote {a.out}: {new_dur:.3f}s (input {dur:.3f}s), replaced {a.start:.2f}-{a.end:.2f}s")
if __name__ == "__main__":
main()
The filter graph is the whole idea, so here it is in words:
-
asplitmakes two copies of the original audio. - The first copy becomes the head:
atrim=0:START, thenafade=t=outover the last 15 ms. - The second copy becomes the tail:
atrim=END:DURATION, thenafade=t=inover the first 15 ms. - Your recording becomes the middle: resampled to match,
apad(infinite silence after it), thenatrim=0:WINDOWso it is exactly the window length, with fades at both ends. -
concat=n=3:v=0:a=1glues head, middle and tail back together.
asetpts=N/SR/TB after each trim resets timestamps so each piece starts at zero. Don't skip it. atrim keeps the original timestamps, so the tail starts at 6.0 s and the fade times (written relative to zero) land in the wrong place. When I removed the three asetpts calls as a test, ffmpeg still reported success, but the decoded audio track was only 9.0 s long. That's why verify.py also compares lengths.
Run it:
python3 punch_in.py clip.mp4 mine.wav 4.4 6.0 out.mp4
# wrote out.mp4: 15.000s (input 15.000s), replaced 4.40-6.00s
If your recording is longer than the window, you get a note on stderr (recording is 2.150s, window is 1.600s; the tail is cut) and the output is still 15.000 s.
Prove it did the right thing
Listening is the real test, but a number helps when you're doing this to several clips. verify.py decodes the original, your recording and the output to 16 kHz mono floats and computes a normalized correlation in three places, skipping 50 ms around each seam (that's where the fades are):
#!/usr/bin/env python3
"""Check a punch-in: inside the window the output should match MINE, outside it the original."""
import subprocess, sys
import numpy as np
def pcm(path, sr=16000):
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", path, "-ac", "1", "-ar", str(sr),
"-f", "f32le", "-"], capture_output=True, check=True).stdout
return np.frombuffer(raw, dtype=np.float32)
def corr(x, y):
n = min(len(x), len(y)); x, y = x[:n], y[:n]
d = np.linalg.norm(x) * np.linalg.norm(y)
return float(np.dot(x, y) / d) if d else 0.0
orig, mine, out = sys.argv[1], sys.argv[2], sys.argv[3]
start, end = float(sys.argv[4]), float(sys.argv[5]); sr = 16000
o, m, r = pcm(orig), pcm(mine), pcm(out)
s, e, g = int(start * sr), int(end * sr), int(0.05 * sr) # skip 50 ms around seams
print(f"lengths orig={len(o)/sr:.3f}s out={len(r)/sr:.3f}s"
+ ("" if abs(len(o) - len(r)) <= sr // 100 else " <-- LENGTH MISMATCH"))
print(f"window corr(out, mine) = {corr(r[s+g:e-g], m[g:]):.3f}")
print(f"window corr(out, orig) = {corr(r[s+g:e-g], o[s+g:e-g]):.3f}")
print(f"outside corr(out, orig) = {corr(np.r_[r[:s-g], r[e+g:]], np.r_[o[:s-g], o[e+g:]]):.3f}")
On my test clip:
$ python3 verify.py clip.mp4 mine.wav out.mp4 4.4 6.0
lengths orig=15.019s out=15.019s
window corr(out, mine) = 1.000
window corr(out, orig) = 0.016
outside corr(out, orig) = 1.000
That's what you want: inside the window the output is your recording and not the original, and outside it the original is intact. (The decoded length reads 15.019 s for both because of AAC encoder padding. The container duration is 15.000 s for both.) ffprobe confirms the video stream is still the original H.264, copied.
Making it sound like it belongs
The script gets the edit right. Making it sound right is a different job:
-
Level. If your line is noticeably louder or quieter than the generated voice, use
--gain-db(for example--gain-db -3) and listen again. Don't normalize the whole clip for this; you'd change the lines you kept. - Room. The generated voice has some kind of "room" around it. A dry phone recording in a closet sounds pasted on. Recording in a normal room, a little away from the mic, usually blends better than a perfectly dead recording.
- Breaths and tails. Leave a little silence at the end of your recording. The 15 ms fade-out cuts whatever is still sounding at the end of the window.
- Lip-sync. Your voice won't match the mouth movement exactly. For a one-word line like "State?", it usually doesn't matter much. For a full sentence it will show. That's a reason to replace short lines, not long ones.
Things not to do with this
- Don't punch in someone else's voice without asking them. The same rule as the photo applies: "Upload only a photo you have the right to use: yourself, an adult who agreed, or your pet" (acceptable use). A voice is at least as personal as a face.
- Don't drop the original TikTok audio back in and post it as "the real thing". The check-before-posting guide asks you to share a remake as a generated comedy remake, not as the original recording.
- Don't use this to cover a bad render. If a paid clip failed before delivery, that's a refund question, not an editing one.
Where the clip comes from
If you're making the clip on my studio: you upload one clear photo of one adult or one pet, pick 480p ($9.99, 6 credits from a pack) or 720p ($12.99, 14 credits), and every clip is 15 seconds, vertical 9:16, with sound (pricing). The punch-in works the same on either resolution, because it never touches the picture.
Recap
- Find the window in a waveform editor. Leave a little room either side.
- Record your line, about as long as the original.
-
punch_in.py clip.mp4 mine.wav START END out.mp4: video copied, audio replaced only in the window, same length, faded seams. -
verify.py: about 1.0 inside the window against your recording, about 1.0 outside it against the original. - Adjust
--gain-dbby ear. Short lines blend better than long ones.
Top comments (0)