# How I made Palmer Luckey and Paul Graham perform Hotel Lobby

> Source: <https://www.skeptrune.com/posts/seedance-video-character-swap/>
> Published: 2026-09-22 06:00:00+00:00

# How I made Palmer Luckey and Paul Graham perform Hotel Lobby

It feels like AI video generation is finally solved and I wanted to be a part of it. After seeing an [AI-generated video of Sam Altman and Dario Amodei](https://x.com/BasedDaedalus/status/2101682062105133438?s=20) go extremely viral on X today, it felt like it was finally time to learn how to do this myself.

I made a version with [Jensen Huang and Lisa Su](https://x.com/tejasybhakta/status/2102261155293270100?s=20) that got over 100,000 impressions with less than 30 minutes of total effort. However, it still does feel like somewhat of a dark art to the uninitiated, hence why I’m writing this blog. Feel free to just copy and paste it into an agent if you want to generate similar kinds of videos.

## [1. Prepare the performance first](#1-prepare-the-performance-first)

I started with [Quavo and Takeoff’s COLORS performance](https://www.youtube.com/watch?v=x9yop0nYR9g), then matched the section and framing of [this character replacement edit on X](https://x.com/BasedDaedalus/status/2101682062105133438). Starting with an existing performance gives the model gestures, expressions, interactions and camera movement to follow.

I downloaded the original with [yt-dlp](https://github.com/yt-dlp/yt-dlp), without its attached YouTube playlist. You will also need FFmpeg and jq for the commands in this post.

```
yt-dlp --no-playlist \
  -f 'bv[width=3840][height=2160]+ba' \
  --merge-output-format mp4 \
  -o 'source.%(ext)s' \
  'https://www.youtube.com/watch?v=x9yop0nYR9g'
```

For this clip, the section begins at 14.60575 seconds and lasts about 29.09 seconds. I cropped the 3840 × 2160 source to 2880 × 2160, offset 606 pixels from the left. That gives a 4:3 frame with the performers and microphone positioned correctly.

```
ffmpeg -ss 14.60575 -i source.mp4 \
  -t 29.094603 \
  -vf 'crop=2880:2160:606:0,fps=24:start_time=0,trim=end_frame=698,setsar=1' \
  -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
  -c:a aac -b:a 192k \
  -movflags +faststart source_trimmed.mp4
```

These crop coordinates are specific to that 4K source. For another video, choose your own section and crop before submitting it. Keep the audio in the trimmed file, since you will use it again after generation.

## [2. Give each person face and outfit references](#2-give-each-person-face-and-outfit-references)

A good portrait tells the model what someone’s face looks like. It does not tell it what shoes to put on them. I collected full body photos with visible footwear alongside facial references, then told the model how to use each photo.

These are the six actual images I submitted, in request order. Click any image to inspect it at full size.

### [Palmer: images 1, 2 and 3](#palmer-images-1-2-and-3)

The first photo is the primary outfit reference: a red patterned Hawaiian shirt, khaki cargo shorts and dark thong flip flops. His full body and both feet are visible. The second is another full body view, useful for his build and footwear, but it shows a different shirt and black shorts. I explicitly told the model to ignore those clothing differences and follow Image 1.

The third photo supplies recent facial detail, including his wavy hair and pointed goatee. Its watch and clip microphone should not carry into the video.

The second photo was originally 8,047 pixels wide. The API rejected it because reference dimensions must be between 300 and 6,000 pixels. I resized the entire photo to 6,000 pixels wide and kept both feet in frame. The rejected attempt used no credits.

### [Paul: images 4, 5 and 6](#paul-images-4-5-and-6)

Paul is on the left in Image 4, but he belongs on the right in the video. That distinction needs to be in the prompt. Image 5 is older, so I used it for facial detail while asking the model to keep his gray hair and appearance from Image 4.

Image 6 is the important wardrobe photo. Paul is standing at the far right, wearing a pale polo, gray shorts and brown sandals. I also asked for visible cargo pockets, even though that photo does not clearly show them. That makes the pockets a detail to check in the output, rather than something the reference already establishes.

## [3. Set up a video edit](#3-set-up-a-video-edit)

I used the standard Seedance 2.5 model, `doubao-seedance-2.5-face`, with `1080p` output and `omni_reference_task_type` set to `edit`.

The [reAPI documentation](https://reapi.ai/docs/seedance-2-5) requires `duration: -1` and `size: "adaptive"` for this editing mode. Automatic duration also requires enough account credit for the maximum reservation, even if the source is shorter. I kept the default moderation settings and set `generate_audio: false`, since I wanted to restore the original music afterward.

Create an [API key](https://reapi.ai/settings/apikeys) and put it in your local `REAPI_API_KEY` environment variable. Upload `source_trimmed.mp4` and the six photos to storage that gives you directly accessible HTTPS file URLs. A link to a photo viewer page is not the same as a link to the image bytes. Temporary signed download URLs work as long as they remain valid for the job.

Save the six image URLs as a JSON array in `image_urls.json`, in the order shown above, and set `SOURCE_URL` to your uploaded trimmed MP4. Keep the key and private URLs out of screenshots and published examples.

## [4. Map the people, then describe what stays the same](#4-map-the-people-then-describe-what-stays-the-same)

Here is a shorter version of my prompt. The image numbers refer to the order in `image_urls.json`.

```
Edit the entire source video @Video1.

Replace the viewer's LEFT performer with Palmer Luckey.
Use @Image1 as his primary full body and outfit reference:
red patterned Hawaiian shirt, khaki cargo shorts and
dark thong flip flops. Keep his bare legs and toes visible.
Use @Image2 for build and footwear only; ignore its
different shirt and black shorts. Use @Image3 for recent
facial detail. Keep his wavy hair and pointed goatee.
No eyeglasses, socks or closed shoes.

Replace the viewer's RIGHT performer with Paul Graham.
Paul is the man on the LEFT in @Image4, the closeup
subject in @Image5, and the standing man at the far
RIGHT in @Image6. Ignore every other person.
Use his gray haired appearance from @Image4 throughout.
Dress him in the pale polo, gray shorts and brown sandals
shown in @Image6. Add visible cargo pockets to the shorts.

The photos provide identity and wardrobe only.
The video provides motion, expressions, gestures, timing,
interactions, camera cuts, framing, lighting and background.
Preserve the original performance as faithfully as possible.
Do not swap positions or invent new movement or scene elements.
Keep the orange studio and hanging microphone.
Remove all rings, watches, bracelets, necklaces and earrings.
```

[Download the complete prompt used for this run.](https://www.skeptrune.com/_astro/submitted-prompt.D8wKirvL.txt) Save whichever version you use as `prompt.txt`.

The position in a reference photograph and the position in the output are separate instructions. This matters when a reference has two people in it. Also name the specific clothes, rather than assuming the model will know which photo should determine the outfit.

Build the request with jq so multiline prompt text is escaped correctly:

```
jq -n \
  --rawfile prompt prompt.txt \
  --arg source "$SOURCE_URL" \
  --slurpfile images image_urls.json \
  '{
    model: "doubao-seedance-2.5-face",
    prompt: $prompt,
    image_urls: $images[0],
    video_urls: [$source],
    omni_reference_task_type: "edit",
    duration: -1,
    size: "adaptive",
    resolution: "1080p",
    bitrate_mode: "default",
    generate_audio: false,
    output_format: "mp4",
    watermark: false
  }' > request.json

curl --fail-with-body --silent --show-error \
  https://reapi.ai/api/v1/videos/generations \
  -H "Authorization: Bearer $REAPI_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @request.json \
  -o submission.json || exit 1

TASK_ID="$(jq -er '.id' submission.json)" || exit 1
```

## [5. Download the result and check the details](#5-download-the-result-and-check-the-details)

The submission returns a task ID. Keep polling that task until it completes. If a submission has an uncertain network result, check whether it created a task before submitting again, since another POST can create another paid render.

```
while true; do
  curl --fail-with-body --silent --show-error \
    "https://reapi.ai/api/v1/tasks/$TASK_ID" \
    -H "Authorization: Bearer $REAPI_API_KEY" \
    -o task.json || exit 1

  status="$(jq -er '.status' task.json)" || exit 1
  case "$status" in
    completed) break ;;
    failed) jq '.error' task.json; exit 1 ;;
    processing) sleep 5 ;;
    *) printf 'Unexpected task status\n' >&2; exit 1 ;;
  esac
done

curl --fail --location --silent --show-error \
  "$(jq -er '.output.video_urls[0]' task.json)" \
  -o generated.mp4
```

Compare the source and generated clip at the same timestamp. Check the positions, scene, gestures and camera cuts, then inspect the faces and clothing through the turns.

Watch every cut, the turns and the moments when hands pass near faces. Check that neither original performer comes back, the replacements keep their positions, and the outfits stay consistent. A successful API response only means that a video was produced.

## [6. Restore the audio and export for X](#6-restore-the-audio-and-export-for-x)

The model returned a 1664 × 1248 video for the `1080p` request. I exported a 1440 × 1080 H264 MP4 for X, keeping the 4:3 frame and copying the audio from the trimmed source.

```
ffmpeg -i generated.mp4 -i source_trimmed.mp4 \
  -map 0:v:0 -map 1:a:0 \
  -vf 'scale=1440:1080:flags=lanczos,setsar=1' \
  -c:v libx264 -preset slow -crf 16 \
  -profile:v high -level:v 4.1 -pix_fmt yuv420p \
  -maxrate 20M -bufsize 40M -g 48 \
  -c:a copy -movflags +faststart \
  final_1080p.mp4
```

The two `-map` arguments select video from the generated file and audio from the original trimmed file. `-c:a copy` preserves that AAC audio without encoding it again. Compare the stream durations and check the end of the clip for any mismatch.

Check the file before uploading:

```
ffprobe -v error \
  -show_entries stream=codec_name,width,height,r_frame_rate,duration \
  -show_entries format=duration \
  -of json final_1080p.mp4

ffmpeg -v error -i final_1080p.mp4 -f null -
```

The export settings target H264 at 1440 × 1080 with AAC audio. Check the resulting frame rate, dimensions and bitrate against [X’s documented upload limits](https://help.x.com/en/using-x/x-videos). The second command decodes the file to catch errors that metadata alone would miss.

The reference photos, prompt and source framing are the parts to reuse when making your own version. If one detail is essential, like Palmer’s flip flops or Paul’s sandals, make it visible in the references and then check that it actually appears in the video.
