Fully automated browser-based image generation with stealth Playwright
A completely automated image generation pipeline for Higgsfield.ai that runs in a Docker container with stealth Playwright. It authenticates via 1Password + Gmail OTP (no human needed), automates the Higgsfield web UI, intercepts API responses to capture job IDs, and downloads full-resolution images from CloudFront CDN.
Calls generate.js with prompt, model, ratio, resolution
Runs on residential network with Xvfb virtual display
Loads authenticated session, navigates to higgsfield.ai/image
Selects model, ratio, resolution, enables Unlimited, types prompt, clicks Generate
Captures POST /jobs/{model} response and polls GET /jobs/{id}/status
Waits for primary job (matched by job_set_type), detects bonus jobs
Extracts thumbnail URLs from page, tries full-res PNG then webp fallback
Images saved to /srv/stacks/higgsfield/output/ (shared volume)
/srv/stacks/higgsfield/
├── docker-compose.yml # Service definition
├── Dockerfile # Playwright 1.52 + stealth + Xvfb
├── scripts/ # Mounted read-only at /app/scripts
│ ├── generate.js # Main generation script
│ └── login.sh # Authentication script
├── auth/ # Mounted at /app/auth
│ └── authenticated.json # Browser session state
└── output/ # Mounted at /app/output
├── *.png / *.webp # Generated images
└── login-result.png # Auth verification screenshot
mcr.microsoft.com/playwright:v1.52.0-noble
residential (external)
Xvfb :99 @ 1920×1080×24
2 GB
Fully automated self-service login via scripts/login.sh — no human interaction required.
1Password CLI retrieves email + password from Higgsfield item in Bastion vault
Queries Gmail via gog CLI to get latest Higgsfield message ID (to detect new OTP)
Stealth Chromium navigates to sign-in page, fills email + password, clicks submit
Higgsfield sends 6-digit verification code to Gmail (invalidates all previous codes)
Polls gog gmail messages search "from:higgsfield" until new message ID appears, extracts code via regex
Browser types 6-digit code into verify page, presses Enter
Playwright saves cookies + localStorage to /app/auth/authenticated.json
| Issue | Solution |
|---|---|
| Cookie banner blocks clicks | Block at network level: page.route("**/*cookiescript*", r => r.abort()) |
| Submit button invisible | Has opacity-0 invisible classes — use click({force: true}) |
| Each login = new code | NEVER re-login between getting code and entering it |
networkidle times out |
Use waitUntil: "domcontentloaded" instead |
| Automation detected | Stealth plugin required: playwright-extra + puppeteer-extra-plugin-stealth |
| Cookie elements re-inject | Nuke DOM before every interaction + MutationObserver for persistent removal |
If generate.js detects a redirect to /auth/, it exits with code 2 to signal session expiry. Re-run login.sh to refresh authentication.
gog CLI is unavailable, the browser script polls /app/output/code.txt for 90 seconds. Write the 6-digit code there manually or prompt the user.
The generate.js script automates the entire Higgsfield web UI workflow:
Reads /app/auth/authenticated.json, launches stealth Chromium, navigates to higgsfield.ai/image
Clicks toolbar model button, types search term in filter input, clicks matching dropdown option
// Find model button, click, filter, select
await p.evaluate(() => {
[...document.querySelectorAll("button")]
.find(e => e.innerText.match(/Nano|Seedream|.../) && e.getBoundingClientRect().top > 550)?.click();
});
Clicks ratio button in toolbar, selects target ratio from popover (1:1, 3:4, 16:9, etc.)
Clicks resolution button, selects 1K/2K/4K from "Select quality" popover (falls back to default if unavailable)
Checks if Unlimited toggle exists for selected model, enables if available (avoids credit usage)
Finds [contenteditable=true][role=textbox], clears existing text via Ctrl+A → Backspace, types new prompt character-by-character
Finds "Generate" button, clicks it (via evaluate for reliability when layout shifts)
Response listener captures POST /jobs/{model} to extract job_sets array
p.on("response", async (resp) => {
const body = await resp.json();
if (body.job_sets) {
console.log(`Submitted: ${body.job_sets.map(js => js.type).join(", ")}`);
}
});
Intercepts GET /jobs/{id}/status responses, tracks completion by matching job_set_type to primary model
if (url.includes("/status")) {
const body = await resp.json();
if (body.status === "completed" && body.job_set_type === primaryType) {
primaryDone = true;
}
}
Scrapes page for thumbnail <img> elements containing job IDs, extracts CloudFront URLs from url= query param
Tries PNG (replace _min.webp with .png), falls back to webp if PNG 403s. Downloads via curl (no auth needed for CloudFront)
const pngUrl = thumbUrl.replace("_min.webp", ".png");
execSync(`curl -sS -f -o "${filepath}" "${pngUrl}"`);
Instead of polling external API endpoints (would need auth headers), the script intercepts browser network responses to capture job IDs and completion status in real-time.
POST fnf.higgsfield.ai/jobs/{model}
Response body contains job_sets[] array with job types
GET fnf.higgsfield.ai/jobs/{id}/status
Browser polls automatically — we just listen for status: "completed"
Reference images are passed via --ref /app/output/filename.png. The file must exist inside the container's shared volume.
Clicking the first input[type=file] triggers a filechooser event. Setting files here creates a reference image slot in the UI.
A new input[type=file] appears for the slot. Triggering its filechooser uploads the image to CloudFront CDN and displays the thumbnail.
The upload mechanism sometimes creates 2 reference slots instead of 1. The script attempts to detect and clear extras by:
img[alt="object image"]Impact: Generation still works — it just uses 2 copies of the same reference. Not ideal, but functional.
# 1. Copy reference image into shared volume
cp ~/cat.png /srv/stacks/higgsfield/output/ref-cat.png
# 2. Pass container path to generate.js
docker exec -e DISPLAY=:99 higgsfield-browser node /app/scripts/generate.js \
--prompt "reimagine as watercolor painting" \
--ref /app/output/ref-cat.png \
--model nano-banana-pro
Use hyphenated slugs with --model parameter:
| Slug | Display Name | job_set_type | Best For | Unlimited |
|---|---|---|---|---|
nano-banana-pro |
Nano Banana Pro | nano_banana_2 |
General-purpose (default) | ✓ |
nano-banana-2 |
Nano Banana 2 | nano_banana_flash |
Flash speed | ✓ |
nano-banana |
Nano Banana | nano_banana |
Legacy | ✓ |
seedream-4.5 |
Seedream 4.5 | seedream_v4_5 |
Photorealistic 4K | ? |
seedream-5.0-lite |
Seedream 5.0 lite | seedream_v5_lite |
Visual reasoning | ? |
flux-2-pro |
FLUX.2 Pro | flux_2_pro |
Speed-optimized detail | ✗ |
flux-2-flex |
FLUX.2 Flex | flux_2 |
Next-gen generation | ✗ |
flux-2-max |
FLUX.2 Max | flux_2 |
Ultimate precision | ✗ |
gpt-image-1.5 |
GPT Image 1.5 | gpt_image_1_5 |
Text rendering | ? |
z-image |
Z-Image | z_image |
Instant lifelike portraits | ? |
kling-o1 |
Kling O1 | kling_o1 |
Photorealistic | ? |
wan-2.2 |
WAN 2.2 | wan_2_2 |
Cinematic visuals | ? |
auto |
Auto | image_auto |
Best model for prompt | ✓ |
The script automatically detects whether the selected model supports Unlimited mode by checking for the toggle switch in the UI. If unavailable, it warns that credits will be used.
Submit a generation job. The browser makes this call when you click Generate.
// Response body:
{
"job_sets": [
{ "id": "...", "type": "nano_banana_2", ... },
{ "id": "...", "type": "seedream_v5_lite", ... } // bonus job
]
}
Poll job status. The browser calls this automatically (~1-2s intervals).
// Response body:
{
"id": "abc123",
"job_set_type": "nano_banana_2",
"status": "completed",
...
}
Full job result (not used by script — we extract URLs from page thumbnails instead).
job_set (POST response) vs job (status polls). The script matches jobs by job_set_type instead of IDs to correctly identify which job completed.
Higgsfield may spawn additional free generations alongside your requested model. For example, requesting Nano Banana Pro might also generate Seedream 4.5 and Seedream 5.0 lite at no cost.
The model you requested. Script blocks until this completes.
Higgsfield freebies (not related to Unlimited mode). Script downloads these too if they complete.
Images are served from d8j0ntlcm91z4.cloudfront.net and don't require authentication.
https://d8j0ntlcm91z4.cloudfront.net/user_{userId}/hf_{timestamp}_{jobId}_min.webp
| Suffix | Format | Resolution | Availability |
|---|---|---|---|
_min.webp |
WebP | 1K (e.g., 896×1200 @ 3:4) | Always |
.png |
PNG | Higher-res (depends on model/settings) | Sometimes |
Download Strategy: Script tries PNG first (replace _min.webp with .png), falls back to webp if PNG returns 403.
| Host Path | Container Path | Mode | Purpose |
|---|---|---|---|
/srv/stacks/higgsfield/scripts/ |
/app/scripts |
Read-only | Generation and auth scripts |
/srv/stacks/higgsfield/output/ |
/app/output |
Read-write | Generated images + reference images |
/srv/stacks/higgsfield/auth/ |
/app/auth |
Read-write | Browser session state |
/app/auth/authenticated.json
Playwright storage state (cookies + localStorage). Created by login.sh, read by generate.js.
/app/output/{jobId}.png
/app/output/{jobId}.webp
Full-resolution downloads. Script prints FIRST_IMAGE= path on last line.
/app/output/*.png
Copy reference images here before passing to --ref.
/app/output/login-result.png
Verification screenshot taken after OTP entry.
# Host → Container (reference image)
cp ~/my-ref.png /srv/stacks/higgsfield/output/ref.png
docker exec ... --ref /app/output/ref.png
# Container → Host (generated image)
# Automatic — generated files appear in /srv/stacks/higgsfield/output/
# Self-service login (automated)
bash ~/.openclaw/skills/higgsfield/scripts/login.sh
# Check auth status
docker exec -e DISPLAY=:99 higgsfield-browser node -e '
const { chromium } = require("playwright-extra");
const s = require("puppeteer-extra-plugin-stealth");
chromium.use(s());
(async () => {
const b = await chromium.launch({headless:false,args:["--no-sandbox"]});
const c = await b.newContext({storageState:"/app/auth/authenticated.json"});
const p = await c.newPage();
await p.goto("https://higgsfield.ai",{timeout:30000,waitUntil:"domcontentloaded"});
await p.waitForTimeout(3000);
console.log(p.url().includes("auth") ? "EXPIRED" : "VALID");
await b.close();
})()'
# Basic generation (Unlimited mode when available)
docker exec -e DISPLAY=:99 higgsfield-browser node /app/scripts/generate.js \
--prompt "a cat floating in space, oil painting" \
--model nano-banana-pro --ratio 1:1 --res 1k
# With reference image
cp ~/ref.png /srv/stacks/higgsfield/output/ref.png
docker exec -e DISPLAY=:99 higgsfield-browser node /app/scripts/generate.js \
--prompt "reimagine as watercolor" \
--ref /app/output/ref.png \
--model nano-banana-pro --ratio 3:4 --res 2k
# Different model + aspect ratio
docker exec -e DISPLAY=:99 higgsfield-browser node /app/scripts/generate.js \
--prompt "cinematic movie poster" \
--model flux-2-pro --ratio 2:3 --res 4k
# Start/rebuild container
cd /srv/stacks/higgsfield && docker compose up -d --build
# Check status
docker ps | grep higgsfield
# View logs
docker logs higgsfield-browser
# Stop container
docker compose down
| Code | Meaning | Action |
|---|---|---|
0 |
Success | Image(s) downloaded successfully |
1 |
Error | Login failed, generation failed, or no images downloaded |
2 |
Session expired | Re-run login.sh to re-authenticate |
bash ~/.openclaw/skills/higgsfield/scripts/login.sh
MODEL_SEARCH_MAP
🎨 Higgsfield Pipeline Documentation
Automated browser-based image generation with stealth Playwright
Built with ❤️ for seamless AI image generation