Comfy Apps: 12-workflow image-gen web GUI + payment frontend

- Flask frontend + serialized GPU worker + BTCPay invoice/webhook
- 12 example images rendered from the real ComfyUI workflows
- fix: ipadapter workflow uses modern IPAdapter node (was broken IPAdapterApply)
- helper scripts: generate/optimize examples
- README with deploy recipe
This commit is contained in:
drjones
2026-08-27 20:37:11 -07:00
commit 55bea5757c
23 changed files with 800 additions and 0 deletions

137
templates/app.html Normal file
View File

@@ -0,0 +1,137 @@
<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{ name.replace('_',' ').title() }} — Comfy Apps</title>
<link rel="stylesheet" href="/static/style.css"></head>
<body>
<div class="wrap">
<header>
<div class="logo"><a href="/">Comfy<span>Apps</span></a></div>
<div class="nav"><a href="/jobs">Gallery</a></div>
</header>
<div class="hero" style="padding-bottom:10px">
<h1>{{ name.replace('_',' ').title() }}</h1>
<p>{{ meta.desc }} — <b style="color:var(--gold)">{{ meta.price_sats }} sats</b></p>
</div>
<div class="example">
<img src="/static/examples/{{ name }}.jpg" alt="{{ name }} example" onerror="this.parentElement.style.display='none'">
<div class="example-caption">Example output — generated with this workflow</div>
</div>
<form class="panel" action="/run" method="post" enctype="multipart/form-data">
<input type="hidden" name="app" value="{{ name }}">
{% set is_t2i = name in ['logo','hero','article','portrait'] %}
{% set is_img = name in ['upscale','inpaint','outpaint','restore_face','ipadapter','controlnet','face_swap','banner'] %}
{% set is_flux = name in ['hero','banner','inpaint','outpaint'] %}
{% if is_img %}
<div class="field">
<label>{% if name in ['ipadapter'] %}Style image{% elif name in ['controlnet'] %}Control image (pose/edge/depth){% elif name in ['face_swap'] %}Face to swap{% elif name == 'banner' %}Image to extend{% else %}Source image{% endif %}</label>
<input type="file" name="image" accept="image/*" required>
{% if name == 'inpaint' %}
<label style="margin-top:12px">Mask (white = edit this region)</label>
<input type="file" name="mask" accept="image/*" required>
{% endif %}
</div>
{% endif %}
<div class="field">
<label>Prompt</label>
<textarea name="prompt" placeholder="Describe what you want..."></textarea>
</div>
{% if is_t2i or name in ['controlnet','ipadapter','face_swap'] %}
<div class="field">
<label>Negative prompt <span class="muted">(what to avoid)</span></label>
<textarea name="negative" placeholder="blurry, low quality, extra fingers..." style="min-height:56px"></textarea>
</div>
{% endif %}
{% if is_t2i %}
<div class="row">
<div class="field">
<label>Width</label>
<input type="number" name="w" value="{{ 1024 if name=='logo' else (1344 if name=='hero' else (1152 if name=='article' else 768)) }}" min="256" max="2048" step="64">
</div>
<div class="field">
<label>Height</label>
<input type="number" name="h" value="{{ 1024 if name=='logo' else (768 if name=='hero' else (768 if name=='article' else 1024)) }}" min="256" max="2048" step="64">
</div>
</div>
{% endif %}
{% if name == 'banner' or name == 'outpaint' %}
<div class="row">
<div class="field"><label>Extend left</label><input type="number" name="left" value="192" min="0" max="1024" step="64"></div>
<div class="field"><label>Extend right</label><input type="number" name="right" value="192" min="0" max="1024" step="64"></div>
<div class="field"><label>Extend top</label><input type="number" name="top" value="0" min="0" max="1024" step="64"></div>
<div class="field"><label>Extend bottom</label><input type="number" name="bottom" value="0" min="0" max="1024" step="64"></div>
</div>
{% endif %}
{% if name == 'upscale' or name == 'restore_face' or name == 'logo' %}
<div class="field">
<label>Upscaler</label>
<select name="upscaler">
<option>4x-UltraSharp.pth</option><option>RealESRGAN_x4.pth</option>
<option>4x_foolhardy_Remacri.pth</option><option>4x_NMKD-Siax_200k.pth</option>
<option>4x_NMKD-Superscale-SP_178000_G.pth</option>
</select>
</div>
{% endif %}
{% if name == 'controlnet' %}
<div class="field">
<label>Control type</label>
<select name="control_type">
<option>openpose</option><option>canny</option><option>depth</option>
<option>lineart</option><option>tile</option>
</select>
</div>
{% endif %}
<div class="row">
<div class="field">
<label>Steps</label>
<input type="number" name="steps" value="{% if is_flux or name in ['controlnet','ipadapter','face_swap'] %}20{% else %}8{% endif %}" min="1" max="60">
</div>
<div class="field">
<label>Seed <span class="muted">(blank = random)</span></label>
<input type="number" name="seed" placeholder="random">
</div>
</div>
{% if name in ['inpaint','outpaint'] %}
<div class="field">
<label>Denoise <span class="muted">(how much to change — {{ '0.7' if name=='inpaint' else '0.85' }} default)</span></label>
<input type="number" name="denoise" value="{{ '0.7' if name=='inpaint' else '0.85' }}" min="0" max="1" step="0.05">
</div>
{% endif %}
{% if name in ['controlnet','ipadapter'] %}
<div class="field">
<label>CFG scale <span class="muted">(prompt adherence)</span></label>
<input type="number" name="cfg" value="7.0" min="1" max="15" step="0.5">
</div>
{% endif %}
{% if name == 'controlnet' %}
<div class="field">
<label>Control strength</label>
<input type="number" name="strength" value="1.0" min="0" max="2" step="0.1">
</div>
{% endif %}
{% if name in ['ipadapter','face_swap'] %}
<div class="field">
<label>{% if name=='ipadapter' %}Style weight{% else %}Identity weight{% endif %}</label>
<input type="number" name="weight" value="{{ '1.0' if name=='ipadapter' else '0.8' }}" min="0" max="1.5" step="0.05">
</div>
{% endif %}
<button class="btn" type="submit">Pay {{ meta.price_sats }} sats &amp; generate</button>
</form>
</div>
</body></html>

50
templates/index.html Normal file
View File

@@ -0,0 +1,50 @@
<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Comfy Apps — AI Image Studio</title>
<link rel="stylesheet" href="/static/style.css"></head>
<body>
<div class="wrap">
<header>
<div class="logo">Comfy<span>Apps</span></div>
<div class="nav"><a href="#apps">Apps</a><a href="/jobs">Gallery</a></div>
</header>
<div class="hero">
<h1>Generate <b>anything</b>.<br>Pay in Bitcoin.</h1>
<p>12 AI image apps running on a local RTX 4080 — logos, heroes, face swaps, upscales, style transfers. Pay a few sats, get your image in under a minute. No account. No tracking.</p>
<div class="stats">
<div class="stat"><b>12</b><span>AI apps</span></div>
<div class="stat"><b>4080</b><span>local GPU</span></div>
<div class="stat"><b>₿</b><span>sats-only</span></div>
<div class="stat"><b>~60s</b><span>per render</span></div>
</div>
</div>
<div class="how" id="how">
<div class="step"><div class="n">1</div><h3>Pick an app</h3><p>Logo, hero, face swap, upscale — whatever you need.</p></div>
<div class="step"><div class="n">2</div><h3>Pay sats</h3><p>Bitcoin on-chain, no account. A few thousand sats.</p></div>
<div class="step"><div class="n">3</div><h3>Get your image</h3><p>The 4080 renders it in ~60s. Download and go.</p></div>
</div>
<div class="grid" id="apps">
{% set emoji = {'logo':'🎨','hero':'🎬','banner':'🖼️','article':'📰','portrait':'🧑','upscale':'🔍','inpaint':'🖌️','outpaint':'↔️','controlnet':'🦴','ipadapter':'🎭','face_swap':'👤','restore_face':'✨'} %}
{% for name, meta in apps.items() %}
<div class="card">
<div class="thumb">
<img src="/static/examples/{{ name }}.jpg" alt="{{ name }}" loading="lazy" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
<div class="emoji" style="display:none">{{ emoji.get(name,'✨') }}</div>
</div>
<h3>{{ name.replace('_',' ').title() }}</h3>
<div class="desc">{{ meta.desc }}</div>
<div class="price">{{ meta.price_sats }} sats</div>
<div class="models">{% for m in meta.models %}<span class="badge">{{ m }}</span>{% endfor %}</div>
<a class="go" href="/app/{{ name }}">Create →</a>
</div>
{% endfor %}
</div>
<footer>
<p>Comfy Apps · self-hosted · Bitcoin only · no accounts, no tracking</p>
</footer>
</div>
</body></html>

24
templates/job.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Job — Comfy Apps</title>
<link rel="stylesheet" href="/static/style.css">
<meta http-equiv="refresh" content="4"></head>
<body>
<div class="wrap">
<header><div class="logo"><a href="/">Comfy<span>Apps</span></a></div><div class="nav"><a href="/jobs">Gallery</a></div></header>
<div class="hero" style="padding-bottom:10px">
<h1>Job <span class="status {{ job['status'] }}">{{ job['status'] }}</span></h1>
<p class="muted">{{ job['app'].replace('_',' ').title() }} · created {{ '%.0f' % job['created_at'] }}</p>
</div>
{% if job['status'] == 'done' and job['result_file'] %}
<div class="result" style="max-width:900px;margin-bottom:40px">
<img src="/image/{{ job['result_file'] }}">
</div>
{% elif job['status'] == 'failed' %}
<div style="max-width:680px"><p style="color:var(--danger)">{{ job['error'] }}</p></div>
{% else %}
<div style="max-width:680px"><p class="muted">Working on it… the 4080 is rendering your image. This page refreshes automatically.</p></div>
{% endif %}
<p class="muted" style="padding-bottom:40px">{% if job['prompt'] %}<em>“{{ job['prompt'] }}”</em>{% endif %}</p>
</div>
</body></html>

24
templates/jobs.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Gallery — Comfy Apps</title>
<link rel="stylesheet" href="/static/style.css"></head>
<body>
<div class="wrap">
<header><div class="logo"><a href="/">Comfy<span>Apps</span></a></div><div class="nav"><a href="/">Home</a></div></header>
<div class="hero" style="padding-bottom:10px"><h1>Gallery</h1></div>
<div class="grid">
{% for j in jobs %}
<div class="card">
<div class="emoji">🖼️</div>
<h3>{{ j['app'].replace('_',' ').title() }}</h3>
<div><span class="status {{ j['status'] }}">{{ j['status'] }}</span></div>
{% if j['result_file'] %}
<a href="/image/{{ j['result_file'] }}" style="display:block;margin-top:14px"><img src="/image/{{ j['result_file'] }}" style="width:100%;border-radius:10px"></a>
{% else %}
<p class="muted" style="margin-top:12px;font-size:13px">{% if j['prompt'] %}“{{ j['prompt'][:70] }}…”{% else %}in progress{% endif %}</p>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</body></html>