Composite headline + subtitle onto branded PNG templates OR directly onto client-supplied photos via the shared task_server pipeline. Local Pillow compositor — no FAL, zero render cost. Two modes (template_compose + photo_compose) behind one wrapper.
Composite headline + subtitle onto branded PNG templates OR directly onto client-supplied photos via the shared task_server pipeline. Local Pillow compositor — no FAL, zero render cost. Two modes (template_compose + photo_compose) behind one wrapper.
ONLY call scripts/design_tool.py. Do NOT invent curl commands; the script
handles auth, the brief shape, and the task lifecycle correctly.
NEVER cat ~/.hermes/secrets/.task_token — the wrapper reads the token from
$TASK_TOKEN_FILE without echoing it.
NEVER claim a template was rendered unless the wrapper printed
✓ completed — file://.... Then verify with ls -la <path> before telling the
user the file exists.
If the wrapper exits non-zero, tell the user the real error. Do not fall back
to pmax-image — they are different skills with different voice+brand intent.
When to use this skill
Brand-template posts with a consistent visual identity (logo, colors, typography)
GMB posts where the headline IS the post
Announcements / promos where a designer-made PNG template already exists
When you want deterministic output (same headline → same pixels)
When NOT to use this skill
Fresh AI-generated imagery → pmax-image (FAL)
Video / reels → pmax-video (FAL)
When no template PNG exists for the client → onboard one first, or use pmax-image
Backend
Same task_server + worker as pmax-image / pmax-video. One queue, one lifecycle.
Render path: render_mode="template_compose" + render_model="template/v1" → the
worker dispatches to TemplateComposeBackend (local Pillow) instead of FAL.
The moment --image is present, the wrapper flips to render_mode=photo_compose. The photo becomes the base layer, and the client-card brand_kit drives the overlay stack:
Contrast overlay (darken text zone for legibility) — optional per brand_kit
Logo (transparent PNG at configured corner) — optional per brand_kit
CTA pill (text + color + position) — optional per brand_kit
Rules:
Local path only in v1. URLs rejected.
--image path is canonicalized via ~/mem0-server/asset_intake.py to ~/mem0-server/assets/uploads/YYYY-MM/<client>/<hash>.<ext> before submission — deterministic dedupe.
Text-box coords scale from a 1080 reference canvas to the actual photo dimensions, so any aspect ratio works.
3) Override the template / template_id
design_tool.py generate --client mock --template-id lux-barbershop --headline "X"
Rare — prefer one template per client. Useful for A/B tests.
brand_kit schema (client card)
Add to ~/.hermes/profiles/pmax-dareen/clients/<slug>.yaml (or the equivalent
for Tarek / Mousa when they wire client-specific brand data):
brand_kit:contrast_overlay:enabled:truezone:bottom# top | bottom | left | right | fullstrength:0.55# 0.0-1.0 opacity of the dark rectanglelogo:path:/Users/majana-agent/mem0-server/assets/logos/<client>.pngposition:top-right# corners + top-center/bottom-center/centerscale:0.14# fraction of canvas widthcta:text:"Book Now"color:"#D4A85C"text_color:"#111111"position:bottom-rightfont_size:28
All three sub-blocks are optional. Missing block → overlay skipped silently. Missing logo file → skipped. Invalid position → defaults to top-left.
3) Fire and return (don't block on render)
design_tool.py generate --client mock --headline "X" --no-wait
# returns immediately with task_id; poll with `status`
4) List templates (honest view of what's actually wired)
design_tool.py list-templates
Output shows ✓ for templates with real PNGs installed, ⚠ PNG missing for
configs pointing at TODO placeholder names. Do not try to render against a
⚠ template — the worker will fail with FileNotFoundError.
5) Status / list
design_tool.py status 42
design_tool.py list --client mock --limit 5
Template onboarding (adding a new client)
Designer delivers 1080×1080 PNG (or any square; the compositor respects it).
Drop at ~/Projects/design-agent/templates/<client-slug>.png.
Verify with design_tool.py list-templates that the new client shows ✓.
Aspect ratio note
--aspect is NOT a template-compose option. Templates already have fixed
dimensions (the PNG defines them). Aspect-ratio routing is for FAL-based
image/video jobs. If you need a different aspect ratio, onboard a separate
template PNG at those dimensions.
Under the hood
design_tool.py is a thin wrapper — it calls task_server directly.
image_worker.py has a TemplateComposeBackend class that dynamically loads
~/Projects/design-agent/design_agent.py::compose_brief().
compose_brief() is a pure function: (brief: dict, asset_path: Path) -> str.
RTL/Arabic is supported if arabic_reshaper + python-bidi are installed.
COMPOSITOR_VERSION in design_agent.py bumps when text-rendering behavior changes.
Current live-template inventory (2026-04-18)
Only mock has a real PNG. The other 3 configs (kiddos-urgent-care,
lux-barbershop, nail-box) have pending-designer placeholder paths.
Honest status: template_compose plumbing is done; product-done is pending
real client templates being delivered by the designer.