How Do I Replace One Line in a Hit the Road Dude AI Clip With My Own Voice? A Tested ffmpeg Punch-In Recipe A developer who runs hittheroaddude.app, a paid studio that remakes the Hit the Road Dude pickup-window clip from a photo, published a tested ffmpeg punch-in recipe for replacing a single spoken line in a generated clip with the user's own voice. The roughly 70-line Python script wraps one ffmpeg filter graph that copies the video stream untouched, swaps only the audio window between a start and end time, trims or pads the new recording to fit with short fades at the seams, and verifies the replacement. The developer notes the site itself does not allow voice swaps, since the voice is part of the saved preset, and that the script was tested on synthetic clips made with ffmpeg's testsrc2 and flite. November 5, 2026. I build hittheroaddude.app https://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 https://hittheroaddude.app/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. 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 https://hittheroaddude.app/blog/window-exchange/ . A punch-in must not move any of them. That gives four rules: -c:v copy . No re-encode, no shifted frames. concat doesn't complain and players don't resample oddly. 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. bash /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: asplit makes two copies of the original audio. atrim=0:START , then afade=t=out over the last 15 ms. atrim=END:DURATION , then afade=t=in over the first 15 ms. apad infinite silence after it , then atrim=0:WINDOW so it is exactly the window length, with fades at both ends. concat=n=3:v=0:a=1 glues 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. 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 : bash /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: bash $ 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. The script gets the edit right. Making it sound right is a different job: --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. 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 https://hittheroaddude.app/pricing/ . The punch-in works the same on either resolution, because it never touches the picture. 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.