VPS headless browser images
Use this when the user wants browser-driven image generation through ChatGPT web or Gemini web on a Linux VPS, and the goal is to keep the workflow working over time rather than hard-freezing one brittle set of selectors forever.
This skill is intentionally adaptive.
The point is not to memorize one exact UI forever. The point is to preserve the working architecture, verify the current live UI, and update the workflow when the providers change.
Core principle
Treat provider-specific selectors and click paths as temporary observations, not permanent contracts.
What should stay stable:
- self-hosted Playwright on the VPS
- headed Chromium under Xvfb rather than pure headless
- one persistent Chromium profile per provider
- separate login/bootstrap flow from usage flow
- local screenshots and local image downloads
- human-in-the-loop pause for OTP / 2FA when needed
- verification by real artifacts, not optimistic assumptions
What may change at any time:
- button labels
- CSS selectors
- prompt composer structure
- image-generation entry points
- download controls
- auth challenges
- page URLs and layout
So the correct operator mindset is:
- preserve the working architecture
- inspect the current UI state
- adapt the provider-specific steps surgically
- keep outputs and verification strong
Scope
This skill covers:
- ChatGPT web image generation on a Linux VPS
- Gemini web image generation on a Linux VPS
- Playwright + Xvfb execution model
- persistent-profile login reuse
- local file downloads
- maintenance guidance when the UI changes
This is intentionally a single combined skill.
The default assumption is that one agent should be able to use this skill to:
- log into ChatGPT
- log into Gemini
- generate images with ChatGPT web
- generate images with Gemini web
- maintain both workflows over time
Do not split it just for neatness.
If needed, a future agent can rewrite and split this skill later. For example:
- if the human only wants ChatGPT image generation, keep the ChatGPT portions and trim out Gemini details
- if the human only wants Gemini image generation, keep the Gemini portions and trim out ChatGPT details
- if context bloat becomes a practical problem, split shared VPS/browser foundations from provider-specific guidance
But the starting point should remain one skill that teaches the full VPS workflow for both providers.
This skill does not promise:
- permanent unattended reliability
- immunity to Cloudflare / Google / OpenAI challenges
- selector stability over time
- that browser automation is better than the official API for every use case
Recommended workspace shape
Use one shared browser automation workspace and separate provider profiles.
Example layout:
/opt/browser-agent-use/
.venv/
out/
downloads/
browser_human.py # optional shared timing helper
chatgpt/
bootstrap_login.py
generate_image.py
gemini/
bootstrap_login.py
generate_image.py
profiles/
chatgpt/
gemini/
This is an example shape, not a mandatory filesystem law.
Important invariants:
- ChatGPT and Gemini must not share the same browser profile
- login/bootstrap and usage should be separate flows
- screenshots and downloads should be written to disk every run
Fresh VPS setup
Minimum base setup:
mkdir -p /opt/browser-agent-use
cd /opt/browser-agent-use
python3 -m venv .venv
. .venv/bin/activate
pip install playwright
playwright install chromium
System requirements:
xvfb-run- a Linux environment capable of running Chromium under Playwright
- enough shared memory / tmp space for Chromium
On a fresh VPS, also expect to need distro-specific browser runtime dependencies. Do not assume pip install playwright alone is sufficient.
Practical guidance:
- if Playwright/Chromium fails before page load, suspect missing system packages first
- if layout is strange, suspect Xvfb display geometry before suspecting selectors
Xvfb / headed-browser guidance
For these consumer web apps, headed Chromium under Xvfb is usually more reliable than plain headless mode.
Typical launch form:
xvfb-run -a /opt/browser-agent-use/.venv/bin/python path/to/script.py
If the UI renders oddly or important controls are missing, use an explicit virtual screen size. Example pattern:
xvfb-run -a --server-args='-screen 0 1920x1080x24' /opt/browser-agent-use/.venv/bin/python path/to/script.py
Treat display geometry as part of the automation environment. A tiny or unusual virtual display can change what is visible and clickable.
Browser launch pattern
Use a persistent Chromium context.
Minimum pattern:
- headed mode
- dedicated profile dir per provider
- dedicated downloads dir
accept_downloads=True- conservative viewport and timeouts
- mild, not excessive, anti-bot hardening
Reference skeleton:
templates/playwright_persistent_context.py
Keep launch defaults conservative. Avoid overengineering stealth tricks before verifying the real failure mode.
Persistent profile rules
Persistent profiles are central to this workflow, but they come with rules:
- only one running browser should use a given profile at a time
- concurrent use of one profile is dangerous
- unclean exits can leave stale locks or partially-saved state
- if a profile behaves strangely after a crash, inspect for lock/corruption issues before rewriting selectors
- provider sessions may still expire server-side even if the local profile is intact
Recovery model:
- check whether the session is still authenticated
- if not, rerun the login/bootstrap flow
- only patch selectors after confirming the problem is not simply sign-out or profile damage
Two-flow model per provider
Each provider should have two scripts or entry points.
1. bootstrap_login
Purpose:
- launch persistent browser
- navigate to provider
- complete first-time login or re-auth
- pause for OTP / 2FA if needed
- exit cleanly so session persists
2. generate_image
Purpose:
- reopen the saved profile
- detect whether the session is still authenticated
- enter the provider’s image workflow
- submit prompt
- wait until generation is truly complete
- download the final image locally
- return machine-readable result metadata
Do not collapse these into one monolith unless there is a strong reason.
Human-in-the-loop checkpoints
OTP / 2FA is normal here.
When a provider requests OTP, email verification, device approval, or Google 2-Step Verification:
- do not guess
- do not aggressively retry
- save a screenshot
- surface a clear state like
needs_otporneeds_manual_2fa - continue in the same live browser context if possible
Good implementation pattern:
- detect challenge page
- capture screenshot + current URL
- emit structured status
- wait for operator input or exit cleanly with enough state to resume
Output contract
Prefer a stable JSON-style result contract so other agents can consume the workflow.
Suggested shape:
{
"status": "ok",
"provider": "chatgpt",
"prompt": "Create a watercolor fox mascot",
"page_url": "https://...",
"screenshots": ["/opt/browser-agent-use/out/step3.png"],
"downloaded_files": ["/opt/browser-agent-use/downloads/fox.png"],
"completion_reason": "stable_after_generation",
"notes": []
}
Other useful statuses:
needs_otpneeds_manual_2fanot_authenticatedgeneration_faileddownload_failed
Verification rules
Never claim success from navigation alone.
Before declaring success, verify one or more real artifacts:
- authenticated state is visible
- prompt is visibly present in the live page
- the page entered the generation phase
- an image appeared in the DOM
- the file was actually downloaded locally
- the downloaded file exists on disk
Always capture screenshots on failure.
Better failure artifacts:
- screenshot
- current URL
- short HTML/DOM dump when selectors fail
- whether the session looked authenticated
Provider-specific guidance: ChatGPT
Current operational truths that have been useful:
- the visible prompt composer may be a contenteditable container rather than the hidden fallback textarea
- image download is more reliable from the authenticated browser context than from an unauthenticated external fetch
- the first new image may be an intermediate render or preview
- completion should be based on settled generation, not merely first image appearance
Current practical workflow:
- open ChatGPT with the persistent profile
- verify authenticated state
- navigate to the current image-generation entry point
- locate the visible composer that actually accepts input
- submit the prompt
- confirm the page has entered a post-submit/generation state
- wait for generation indicators to disappear
- require the visible image set to remain stable for multiple polls
- download using the live authenticated browser context
- verify the file exists locally
Adaptive rule:
- if ChatGPT changes the entry path or composer markup, keep the overall workflow and re-discover the current visible path instead of rewriting the whole architecture
Provider-specific guidance: Gemini
Current operational truths that have been useful:
- Google login is often the fragile part, not Gemini prompt submission
- the image-generation path may involve
ToolsthenCreate image - download can lag significantly after the UI looks done
- generous download timeouts are reasonable
Current practical workflow:
- open Gemini with the persistent profile
- verify authenticated state
- locate the current image-generation path, often through
Tools->Create image - submit the prompt
- confirm the page entered generation
- wait for the final image/download controls
- use a long enough download timeout before declaring failure
- verify the file exists locally
Adaptive rule:
- if Gemini changes the location of image mode, keep the provider split, session handling, and download/verification model intact while re-discovering the new path
Selector strategy
Do not overbind the skill to one selector set.
Use this selector order of preference:
- accessibility roles / labels / visible text
- stable semantic attributes
- fallback DOM structure selectors only when necessary
When selectors fail:
- inspect the current live DOM
- capture screenshot + URL
- patch only the failing step
- keep the rest of the flow unchanged if it still works
Humanization guidance
A mild shared helper can improve reliability:
- small randomized settle delays
- humanized typing
- hover before click
- jittered polling intervals
But do not let “humanization” become chaos.
Bad pattern:
- large random delays everywhere
- rewriting the flow for aesthetics
- adding stealth tweaks before diagnosing the actual failure mode
Troubleshooting table
Symptom: browser fails before site interaction
Likely cause:
- missing Linux browser dependencies
- Playwright/Chromium runtime issue
Inspect:
- stderr from script
- Playwright launch failure details
Fix:
- install missing system packages
- re-check Xvfb and Chromium runtime dependencies
Symptom: page layout looks wrong / buttons missing
Likely cause:
- Xvfb display size or viewport issue
Inspect:
- screenshot
- virtual display size
- viewport config
Fix:
- use explicit Xvfb screen size
- re-test with a desktop viewport
Symptom: prompt entry fails
Likely cause:
- targeted hidden textarea or stale selector
Inspect:
- screenshot
- current visible composer
- DOM around the prompt area
Fix:
- switch to the visible composer
- prefer role/label/text-based targeting
Symptom: image visible but no file downloaded
Likely cause:
- wrong download mechanism
- insufficient download wait
- download not triggered from authenticated context
Inspect:
- current page URL
- whether download event fired
- whether the image is only a preview
Fix:
- trigger download from the logged-in Playwright context
- extend timeout
- require settled completion before download
Symptom: sudden auth failure after working previously
Likely cause:
- expired server-side session
- profile damage after crash
- provider risk/challenge logic
Inspect:
- auth state in UI
- current URL
- whether profile was reused concurrently
Fix:
- rerun bootstrap login
- avoid concurrent profile use
- only patch selectors if auth is confirmed healthy
Maintenance rule
This skill is supposed to evolve.
If a provider flow changes and a new approach works better:
- update the skill
- update the runbook
- update the template if needed
- preserve the architectural invariants
- avoid locking future agents into stale UI assumptions
In other words:
- adapt the provider path
- keep the workflow discipline
References
See also:
references/runbook.mdtemplates/playwright_persistent_context.py