This repo documents a seemingly simple idea that turned out to be surprisingly hard.
Take a well-known programming meme and turn it into a LinkedIn cover (1584×396), with me replacing the original character while keeping everything else the same.
This is not a polished library.
It is a worked-through engineering experience: the plan, the failures, what helped, what did not, and why.
I started with a meme from Agent X Comics, a familiar scene of a developer surrounded by monitors and cables, saying something painfully relatable.
As someone who codes in C++, I resonated with it immediately.
The goal was to turn this into a LinkedIn cover image:
- same joke
- same speech bubble
- same environment
- but me instead of the original guy
Target size: 1584 × 396 px (LinkedIn cover requirement)
I had previously tried doing constrained image edits in GUI-based image generation tools (Tintina comic book) and repeatedly hit issues that made careful iteration difficult.
The concrete problems were:
-
Requests stopped having any effect
Small changes often produced outputs that were effectively identical to previous ones. It felt like the generation had converged and could not escape its local solution unless the session was restarted. -
Directional language was unreliable
Instructions like “left” and “right” frequently broke down, especially when trying to freeze most of the image and modify only a small region. -
Hidden parameters and defaults
There was no visibility into internal settings such as guidance strength, resizing, conditioning order, or retry logic. This made it impossible to reason about why a change did or did not happen.
Because of this, I wanted a workflow with:
- explicit stages
- inspectable intermediate artifacts
- reproducibility
- prompts treated as artifacts rather than ephemeral UI state
Trying to do everything in one generation was unreliable (see below picture), so I split the task into two explicit stages.
Goal: replace only the person in the meme, while keeping everything else pixel-faithful:
- framing and perspective unchanged
- desk, monitors, cables unchanged
- cartoon line thickness and shading unchanged
- speech bubble unchanged (text, font, shape, placement)
This sounds simple. I wrote a few lines of code in make_cover.py to call the API and give me the output image.
I ran into a wide range of failure modes. The most common one was that the reference photo was reduced to a few abstract keywords such as “woman” or “brown hair”, rather than being treated as a concrete identity constraint.
Results included:
- little to no resemblance to the reference photo
- facial features drifting wildly
- body proportions changing
- style simplification
At this point, I tried the same task once using the UI and even though the result was not perfect, the difference was immediately obvious.
It appears that the UI benefits from several things that are not directly exposed through the API.
-
Stronger edit anchoring
- The original image is locked more aggressively
- Internal masks and attention bias favor editing over regeneration
- Geometry drift is penalized harder
-
Multi-pass correction and implicit rejection sampling
- Internally retries generations
- Rejects drafts with excessive drift
- Selects the least-damaging candidate
-
Human-tuned post-processing
- Implicit face-region correspondence
- Identity projected onto an existing head volume
- In the API, this must be approximated via wording and it is fragile
In contrast, the API often has to guess.
Guessing leads to body inflation, redraws, and style drift.
Despite starting this as an API-first experiment, the cleanest and most usable Stage 1 result came from the UI.
For this very constrained task, the UI performed better.
However, extracting a reliable result from the UI still required a hybrid pipeline.
With a good Stage 1 base, the next problem was sizing.
Shrinking or naive cropping destroys legibility, so the plan was:
- Build a larger canvas
- Place the meme in a center band
- Fully protect that band using a mask
- Allow edits only in the left and right gutters within the band
- Outpaint background only in those gutters
- Resize and crop exactly to 1584 × 396
Common problems:
- edits leaked outside the masked region
- facial color shifts (I got blushed)
- text corruption (Finally lost an l)
Stage 2 using the API consistently hit a known failure mode.
The model ignored the intended semantics of the mask and instead zoomed or recomposed the entire canvas.
From reading the code and inspecting outputs, this seems to happen when:
- the model internally rescales or reframes the image
- the mask is treated as a soft constraint
- the generation is conditioned on the bounding region rather than strict pixel locality
In practice, this turns outpainting into zooming.
The final usable result again came from applying the prompt through the UI, then using code to crop precisely.
Stage 1
- Use UI plus prompt1 to replace the character in the meme
Stage 2
- Use code to:
- construct the canvas
- build an exact mask
- enforce geometry guarantees
- Use UI plus prompt2 for controlled outpainting
- Use code to crop to the LinkedIn cover size
Code should enforce structure.
Models should only be allowed to add texture.
This project sits at the intersection of image inpainting, meme generation, and prompt-controlled editing.
Face swap and meme style tools
- Canva Face Swap
https://www.canva.com/features/face-swap/ - Reface
https://reface.ai/ - Pixlr Face Swap
https://pixlr.com/features/face-swap/
AI LinkedIn cover generators
- Taplio AI LinkedIn Cover Generator
https://taplio.com/tools/linkedin-cover - Simplified LinkedIn Banner Generator
https://simplified.com/tools/linkedin-banner - Designs.ai LinkedIn Banner Maker
https://designs.ai/design-types/linkedin-banner
These tools generate new compositions and branding graphics.
They do not aim to preserve an existing scene pixel-for-pixel.
The failures encountered here align with known reports:
- AUTOMATIC1111 Stable Diffusion issues related to inpainting masks
https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues - Discussions on mask leakage and bounding-box conditioning
https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions - OpenAI image editing documentation for intended behavior
https://platform.openai.com/docs/guides/images
These issues are inherent to how diffusion-based models condition on context.
This project used the following OpenAI models at different stages:
-
gpt-5.2
Used via the ChatGPT UI for iterative image editing and prompt-driven refinement. -
gpt-image-1
Used via the API for scripted image edits, masked outpainting attempts, and reproducible experiments.
Both UI and API paths are documented because they exhibit meaningfully different behavior under tight constraints.
- Enforce all geometry and layout constraints entirely in code
- Use models only for texture and color refinement
- Avoid semantic masks whenever possible in favor of explicit pixel guarantees
- Treat UI and API as different products with different affordances rather than interchangeable interfaces
Put your files in the assets folder:
assets/
├── meme.png # the original meme
├── me.jpeg # your reference photoOutputs are written to:
- Intermediate files in
outputs/ - Final cover in
assets/cover.png
Don't forget to set your API key.
pip install pillow openai python-dotenv
export OPENAI_API_KEY="your_key_here"This attempts character replacement using the API:
python make_cover.py stage1Output:
outputs/stage1_replaced.png
If you prefer the UI for Stage 1, use the prompt in gpt_prompts/stage1_ui.md and generate your best result in the UI and save it to the outputs folder.
This step is deterministic. It creates the canvas and mask you can use either with the API or the UI.
python make_cover.py stage2_prepareYou will get:
outputs/stage2_canvas.png
outputs/stage2_mask.png
Optional API call:
python make_cover.py stage2_editOutput:
outputs/stage2_outpainted.png
If you prefer the UI for Stage 2, use:
outputs/stage2_canvas.pngas the imageoutputs/stage2_mask.pngas the mask- prompt in
gpt_prompts/stage2_ui.md
Then save your UI result as:
outputs/stage2_outpainted.png
This step uses no AI and is fully reproducible.
python make_cover.py finalizeFinal output:
assets/cover.png
I initially intended to share a small script others could reuse to:
- swap themselves into the same meme
- generate similar LinkedIn covers
- learn lessons to finish my Father’s Day comic book of Tintina
What I ended up sharing instead is:
- a concrete failure trail
- a set of constraints that are harder than they look
- a practical example of how UI and API behaviors diverge
If you have found:
- a more reliable masking strategy
- a way to prevent zoom or recomposition
- or a cleaner staged approach
I would genuinely like to hear it.
This started as just an image.
It turned into a lesson about:
- constraint enforcement
- tooling opacity
- and the difference between controlling a model and negotiating with one















