Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LinkedIn Meme Cover a small project that refused to be small

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.

The starting point

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)

Why I didn’t want to do this in a GUI originally

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

The plan split the problem into two stages

Trying to do everything in one generation was unreliable (see below picture), so I split the task into two explicit stages.

Stage 1 replace the character only

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.

Stage 1 failures using the API (many of them)

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.

The twist the best Stage 1 result came from the UI

It appears that the UI benefits from several things that are not directly exposed through the API.

What the UI seems to do differently

  • 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.

Stage 2 fitting LinkedIn dimensions via masked outpainting

With a good Stage 1 base, the next problem was sizing.

Shrinking or naive cropping destroys legibility, so the plan was:

  1. Build a larger canvas
  2. Place the meme in a center band
  3. Fully protect that band using a mask
  4. Allow edits only in the left and right gutters within the band
  5. Outpaint background only in those gutters
  6. Resize and crop exactly to 1584 × 396

Stage 2 UI failures masks zooming and leakage

Common problems:

  • edits leaked outside the masked region
  • facial color shifts (I got blushed)
  • text corruption (Finally lost an l)

Why Stage 2 failed with the API

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.

TLDR the hybrid pipeline that actually worked

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

Key insight

Code should enforce structure.
Models should only be allowed to add texture.

Related work and references

This project sits at the intersection of image inpainting, meme generation, and prompt-controlled editing.

Adjacent tools closest in intent but not in guarantees

Face swap and meme style tools

AI LinkedIn cover generators

These tools generate new compositions and branding graphics.
They do not aim to preserve an existing scene pixel-for-pixel.

Masking and outpainting failure modes

The failures encountered here align with known reports:

These issues are inherent to how diffusion-based models condition on context.

Models used

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.

What I would do differently next time

  • 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

How to use this code

Prepare your inputs

Put your files in the assets folder:

assets/
├── meme.png      # the original meme
├── me.jpeg       # your reference photo

Outputs are written to:

  • Intermediate files in outputs/
  • Final cover in assets/cover.png

Install dependencies

Don't forget to set your API key.

pip install pillow openai python-dotenv
export OPENAI_API_KEY="your_key_here"

Stage 1 optional API attempt

This attempts character replacement using the API:

python make_cover.py stage1

Output: 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.

Stage 2 prepare canvas and mask (no AI)

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_prepare

You will get: outputs/stage2_canvas.png outputs/stage2_mask.png

Stage 2 outpaint either via API or UI

Optional API call:

python make_cover.py stage2_edit

Output: outputs/stage2_outpainted.png

If you prefer the UI for Stage 2, use:

  • outputs/stage2_canvas.png as the image
  • outputs/stage2_mask.png as the mask
  • prompt in gpt_prompts/stage2_ui.md

Then save your UI result as: outputs/stage2_outpainted.png

6 Finalize (crop and pad to LinkedIn size)

This step uses no AI and is fully reproducible.

python make_cover.py finalize

Final output: assets/cover.png

Why I am sharing this

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.

Final note

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

About

Create a LinkedIn cover image (exactly **1584×396**) by editing a meme with a reference photo using the OpenAI Images API, then cropping/resizing to the LinkedIn banner format.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages