Merge static frontend and a template/generator with basic options
This commit is contained in:
parent
f97a3e541a
commit
3c581efd4e
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
.aider*
|
.aider*
|
||||||
|
frontend/static/
|
||||||
|
|
||||||
# Ripped from https://github.com/github/gitignore/blob/main/Python.gitignore
|
# Ripped from https://github.com/github/gitignore/blob/main/Python.gitignore
|
||||||
# Some things removed because I know they're unused
|
# Some things removed because I know they're unused
|
||||||
|
78
frontend/build_static.py
Normal file
78
frontend/build_static.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
DEFAULT_TITLE = 'ComfyKiosk - Sample Frontend'
|
||||||
|
DEFAULT_BACKEND_URL = 'http://localhost:8000'
|
||||||
|
DEFAULT_OUTPUT_DIR = 'frontend/static'
|
||||||
|
DEFAULT_FIXED_WORKFLOW_ID = None
|
||||||
|
DEFAULT_USE_CDN = True
|
||||||
|
DEFAULT_HIDE_SEED = False
|
||||||
|
|
||||||
|
def build_static_site(api_url=DEFAULT_BACKEND_URL,
|
||||||
|
fixed_workflow=DEFAULT_FIXED_WORKFLOW_ID,
|
||||||
|
use_cdn=DEFAULT_USE_CDN,
|
||||||
|
title=DEFAULT_TITLE,
|
||||||
|
hide_seed=DEFAULT_HIDE_SEED):
|
||||||
|
# Ensure api_url is absolute
|
||||||
|
if not api_url.startswith(('http://', 'https://')):
|
||||||
|
api_url = 'http://' + api_url
|
||||||
|
if not use_cdn:
|
||||||
|
if __name__ != '__main__':
|
||||||
|
import warnings
|
||||||
|
warnings.warn("""
|
||||||
|
It looks like you're calling build_static_site() as a module while using `use_cdn=False`
|
||||||
|
This function will only output our html, you must pick up simple.min.css from disk after build_static_site() downloads it.
|
||||||
|
""", UserWarning)
|
||||||
|
|
||||||
|
# Download simple.min.css from GitHub
|
||||||
|
css_url = 'https://github.com/kevquirk/simple.css/raw/refs/heads/main/simple.min.css'
|
||||||
|
css_response = requests.get(css_url)
|
||||||
|
css_response.raise_for_status()
|
||||||
|
|
||||||
|
# Save simple.css
|
||||||
|
css_path = os.path.join(output_dir, 'simple.min.css')
|
||||||
|
with open(css_path, 'wb') as f:
|
||||||
|
f.write(css_response.content)
|
||||||
|
|
||||||
|
# Set up Jinja environment
|
||||||
|
env = Environment(loader=FileSystemLoader('frontend/templates'))
|
||||||
|
|
||||||
|
# Render index.html with configuration
|
||||||
|
template = env.get_template('index.html')
|
||||||
|
output = template.render(
|
||||||
|
api_url=api_url,
|
||||||
|
fixed_workflow=fixed_workflow,
|
||||||
|
use_cdn=use_cdn,
|
||||||
|
title=title,
|
||||||
|
hide_seed=hide_seed
|
||||||
|
)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description='Build static frontend for ComfyKiosk')
|
||||||
|
parser.add_argument('--api-url', help=f'URL of the ComfyKiosk API (default: {DEFAULT_BACKEND_URL})')
|
||||||
|
parser.add_argument('--title', help=f'Page title (default: {DEFAULT_TITLE})', type=str, nargs='+')
|
||||||
|
parser.add_argument('--workflow', help=f'Fix to a specific workflow ID (default: {DEFAULT_FIXED_WORKFLOW_ID})')
|
||||||
|
parser.add_argument('--output-dir', help=f'Output directory (default: {DEFAULT_OUTPUT_DIR})')
|
||||||
|
parser.add_argument('--download-css', action='store_true', help='Download and serve simple.min.css locally instead of using CDN')
|
||||||
|
parser.add_argument('--hide-seed', action='store_true', help='Hide the seed input field')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Create output directory if it doesn't exist
|
||||||
|
output_dir = args.output_dir or DEFAULT_OUTPUT_DIR
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
output = build_static_site(
|
||||||
|
api_url=args.api_url or DEFAULT_BACKEND_URL,
|
||||||
|
fixed_workflow=args.workflow,
|
||||||
|
use_cdn=not args.download_css,
|
||||||
|
title=' '.join(args.title) if args.title else DEFAULT_TITLE,
|
||||||
|
hide_seed=args.hide_seed
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write to static file
|
||||||
|
with open(os.path.join(output_dir, 'index.html'), 'w') as f:
|
||||||
|
f.write(output)
|
57
frontend/templates/base.html
Normal file
57
frontend/templates/base.html
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Image Generator{% endblock %}</title>
|
||||||
|
<link rel="stylesheet" href="{% if use_cdn %}https://cdn.simplecss.org/simple.min.css{% else %}simple.min.css{% endif %}">
|
||||||
|
<style>
|
||||||
|
#result {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#loading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.loading-overlay {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 5px solid #f3f3f3;
|
||||||
|
border-top: 5px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.generating img {
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
137
frontend/templates/index.html
Normal file
137
frontend/templates/index.html
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
|
<div id="error" class="error" style="display: none;"></div>
|
||||||
|
|
||||||
|
<form id="generateForm" onsubmit="generateImage(event)">
|
||||||
|
<div class="form-group" style="text-align: center;">
|
||||||
|
{% if not fixed_workflow %}
|
||||||
|
<select name="workflow" required>
|
||||||
|
<option value="">Select a workflow...</option>
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if not hide_seed %}
|
||||||
|
<input type="number" name="seed" min="0" placeholder="Leave empty for random seed">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button type="submit">Generate Image</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="loading" style="display: none;"></div>
|
||||||
|
<div id="result">
|
||||||
|
<div class="image-container">
|
||||||
|
<img id="generatedImage" style="max-width: 512px; max-height: 512px; display: none; cursor: pointer;">
|
||||||
|
<div class="loading-overlay">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function submitGenerate() {
|
||||||
|
const form = document.getElementById('generateForm');
|
||||||
|
const event = new Event('submit', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
form.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
const API_URL = '{{ api_url }}';
|
||||||
|
{% if fixed_workflow %}const FIXED_WORKFLOW = '{{ fixed_workflow }}';{% endif %}
|
||||||
|
|
||||||
|
function generateImage(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const form = event.target;
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const image = document.getElementById('generatedImage');
|
||||||
|
const workflowId = {% if fixed_workflow %}FIXED_WORKFLOW{% else %}form.workflow.value{% endif %};
|
||||||
|
const seedInput = form.querySelector('input[name="seed"]');
|
||||||
|
const seed = seedInput ? seedInput.value : null;
|
||||||
|
|
||||||
|
loading.style.display = 'block';
|
||||||
|
const imageContainer = document.querySelector('.image-container');
|
||||||
|
const loadingOverlay = document.querySelector('.loading-overlay');
|
||||||
|
if (imageContainer) {
|
||||||
|
imageContainer.classList.add('generating');
|
||||||
|
}
|
||||||
|
if (loadingOverlay) {
|
||||||
|
loadingOverlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the appropriate URL based on whether we have a seed
|
||||||
|
// Ensure we're using the full URL
|
||||||
|
const baseUrl = API_URL.replace(/\/$/, ''); // Remove trailing slash if present
|
||||||
|
const url = seed
|
||||||
|
? `${baseUrl}/workflows/${workflowId}/image/${seed}`
|
||||||
|
: `${baseUrl}/workflows/${workflowId}/image`;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Generation failed');
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
image.src = url;
|
||||||
|
image.style.display = 'block';
|
||||||
|
image.onclick = submitGenerate;
|
||||||
|
const imageContainer = document.querySelector('.image-container');
|
||||||
|
const loadingOverlay = document.querySelector('.loading-overlay');
|
||||||
|
if (imageContainer) {
|
||||||
|
imageContainer.classList.remove('generating');
|
||||||
|
}
|
||||||
|
if (loadingOverlay) {
|
||||||
|
loadingOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert('Error: ' + error.message);
|
||||||
|
const imageContainer = document.querySelector('.image-container');
|
||||||
|
const loadingOverlay = document.querySelector('.loading-overlay');
|
||||||
|
if (imageContainer) {
|
||||||
|
imageContainer.classList.remove('generating');
|
||||||
|
}
|
||||||
|
if (loadingOverlay) {
|
||||||
|
loadingOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Load workflows when page loads
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
{% if fixed_workflow %}
|
||||||
|
// Skip workflow loading if we're using a fixed workflow
|
||||||
|
return;
|
||||||
|
{% else %}
|
||||||
|
try {
|
||||||
|
const baseUrl = API_URL.replace(/\/$/, ''); // Remove trailing slash if present
|
||||||
|
const response = await fetch(`${baseUrl}/workflows`);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch workflows');
|
||||||
|
|
||||||
|
const workflows = await response.json();
|
||||||
|
const select = document.querySelector('select[name="workflow"]');
|
||||||
|
|
||||||
|
workflows.forEach(workflow => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = workflow.id;
|
||||||
|
option.textContent = `${workflow.id} - ${workflow.handle}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there's only one workflow, select it automatically
|
||||||
|
if (workflows.length === 1) {
|
||||||
|
select.value = workflows[0].id;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorDiv = document.getElementById('error');
|
||||||
|
errorDiv.textContent = `Error: ${error.message}`;
|
||||||
|
errorDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -6,3 +6,4 @@ starlette~=0.41.3
|
|||||||
pillow~=10.4.0
|
pillow~=10.4.0
|
||||||
websockets~=14.1
|
websockets~=14.1
|
||||||
more-itertools
|
more-itertools
|
||||||
|
jinja2~=3.1.3
|
@ -2,6 +2,7 @@ from comfykiosk import ComfyKiosk, ComfyGenerator, SimpleWorkflowLoader
|
|||||||
from comfykiosk.image_sources.filesystem import FileSystemImageSource
|
from comfykiosk.image_sources.filesystem import FileSystemImageSource
|
||||||
from comfykiosk.image_sources.pregenerate import PreparedGenPool
|
from comfykiosk.image_sources.pregenerate import PreparedGenPool
|
||||||
from comfykiosk.fastapi import create_app
|
from comfykiosk.fastapi import create_app
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
# You can compose ComfyKiosk as an alternate way to override defaults
|
# You can compose ComfyKiosk as an alternate way to override defaults
|
||||||
filesystem_source = FileSystemImageSource(directory="./output")
|
filesystem_source = FileSystemImageSource(directory="./output")
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from comfykiosk import EasyComfyKiosk
|
from comfykiosk import EasyComfyKiosk
|
||||||
from comfykiosk.fastapi import create_app
|
from comfykiosk.fastapi import create_app
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
# Create the image generation service
|
# Create the image generation service
|
||||||
comfy = EasyComfyKiosk()
|
comfy = EasyComfyKiosk()
|
||||||
|
Loading…
Reference in New Issue
Block a user