mirror of
https://github.com/karl0ss/ai_image_frame_server.git
synced 2025-07-19 19:24:59 +01:00
Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
79c7e4d92c | |||
b072ec6c65 |
@ -1,24 +1,19 @@
|
||||
[tool.bumpversion]
|
||||
current_version = "0.2.16"
|
||||
current_version = "0.2.9"
|
||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||
serialize = ["{major}.{minor}.{patch}"]
|
||||
search = "{current_version}"
|
||||
replace = "{new_version}"
|
||||
regex = false
|
||||
ignore_missing_version = false
|
||||
ignore_missing_files = false
|
||||
tag = true
|
||||
commit = true
|
||||
message = "Bump version: {current_version} → {new_version}"
|
||||
sign_tags = false
|
||||
tag_name = "{new_version}"
|
||||
tag_message = "Bump version: {current_version} → {new_version}"
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = ".bumpversion.toml"
|
||||
search = 'current_version = "{current_version}"'
|
||||
replace = 'current_version = "{new_version}"'
|
||||
|
||||
[[tool.bumpversion.files]]
|
||||
filename = "Dockerfile"
|
||||
search = 'ARG VERSION="{current_version}"'
|
||||
replace = 'ARG VERSION="{new_version}"'
|
||||
allow_dirty = false
|
||||
commit = true
|
||||
message = "Bump version: {current_version} → {new_version}"
|
||||
moveable_tags = []
|
||||
commit_args = ""
|
||||
setup_hooks = []
|
||||
|
@ -3,13 +3,9 @@ FROM python:3.11-slim
|
||||
|
||||
# Set the working directory in the container
|
||||
WORKDIR /app
|
||||
# Set version label
|
||||
ARG VERSION="0.2.16"
|
||||
LABEL version=$VERSION
|
||||
|
||||
# Copy project files into the container
|
||||
COPY . /app
|
||||
|
||||
|
||||
# Install dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
108
README.md
108
README.md
@ -1,108 +0,0 @@
|
||||
# AI Image Frame Server
|
||||
|
||||
This project is a Flask-based web server designed to generate and display images from various AI models, primarily interacting with ComfyUI. It can be configured to automatically generate new images at scheduled times and provides a gallery to view the generated images.
|
||||
|
||||
## Features
|
||||
|
||||
* **Web Interface:** A simple web interface to view generated images.
|
||||
* **Image Generation:** Integrates with ComfyUI to generate images based on given prompts and models.
|
||||
* **Scheduled Generation:** Automatically generates new images at a configurable time.
|
||||
* **Docker Support:** Comes with a `Dockerfile` and `docker-compose.yml` for easy setup and deployment.
|
||||
* **Configurable:** Most options can be configured through a `user_config.cfg` file.
|
||||
* **Authentication:** Optional password protection for image creation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Python 3.11
|
||||
* Docker (for containerized deployment)
|
||||
* An instance of ComfyUI running and accessible from the server.
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone <repository_url>
|
||||
cd ai_image_frame_server
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Configure the application:**
|
||||
* Copy the `user_config.cfg.sample` to `user_config.cfg`.
|
||||
* Edit `user_config.cfg` with your settings. See the [Configuration](#configuration) section for more details.
|
||||
|
||||
4. **Run the application:**
|
||||
```bash
|
||||
export SECRET_KEY='a_very_secret_key'
|
||||
python ai_frame_image_server.py
|
||||
```
|
||||
|
||||
### Docker Setup
|
||||
|
||||
1. **Clone the repository:**
|
||||
```bash
|
||||
git clone <repository_url>
|
||||
cd ai_image_frame_server
|
||||
```
|
||||
|
||||
2. **Configure the application:**
|
||||
* Copy the `user_config.cfg.sample` to `user_config.cfg`.
|
||||
* Edit `user_config.cfg` with your settings. The `comfyui_url` should be the address of your ComfyUI instance, accessible from within the Docker network (e.g., `http://host.docker.internal:8188` or your server's IP).
|
||||
|
||||
3. **Build and run with Docker Compose:**
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
The application will be available at `http://localhost:8088`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The application is configured via the `user_config.cfg` file.
|
||||
|
||||
| Section | Key | Description | Default (from sample) |
|
||||
| :-------- | :------------------- | :-------------------------------------------------------------------------- | :-------------------- |
|
||||
| `[frame]` | `reload_interval` | How often the gallery page reloads in milliseconds. | `30000` |
|
||||
| `[frame]` | `auto_regen` | Enable or disable automatic image generation (`True`/`False`). | `True` |
|
||||
| `[frame]` | `regen_time` | The time to automatically generate a new image (HH:MM). | `03:00` |
|
||||
| `[frame]` | `port` | The port the Flask application will run on. | `5000` |
|
||||
| `[frame]` | `create_requires_auth` | Require a password to create images (`True`/`False`). | `False` |
|
||||
| `[frame]` | `password_for_auth` | The password to use for image creation if authentication is enabled. | `create` |
|
||||
| `[comfyui]` | `comfyui_url` | The URL of your ComfyUI instance. | `http://comfyui` |
|
||||
| `[comfyui]` | `models` | A comma-separated list of models to use for generation. | `zavychromaxl_v100.safetensors,ponyDiffusionV6XL_v6StartWithThisOne.safetensors` |
|
||||
| `[comfyui]` | `output_dir` | The directory to save generated images to. | `./output/` |
|
||||
| `[comfyui]` | `prompt` | The prompt to use for generating a random prompt for stable diffusion. | `"Generate a random detailed prompt for stable diffusion."` |
|
||||
| `[comfyui]` | `width` | The width of the generated image. | `1568` |
|
||||
| `[comfyui]` | `height` | The height of the generated image. | `672` |
|
||||
| `[comfyui]` | `topics` | A comma-separated list of topics to generate prompts from. | |
|
||||
| `[comfyui]` | `FLUX` | Enable FLUX models (`True`/`False`). | `False` |
|
||||
| `[comfyui]` | `ONLY_FLUX` | Only use FLUX models (`True`/`False`). | `False` |
|
||||
| `[comfyui:flux]` | `models` | A comma-separated list of FLUX models. | `flux1-dev-Q4_0.gguf,flux1-schnell-Q4_0.gguf` |
|
||||
| `[openwebui]` | `base_url` | The base URL for OpenWebUI. | `https://openwebui` |
|
||||
| `[openwebui]` | `api_key` | The API key for OpenWebUI. | `sk-` |
|
||||
| `[openwebui]` | `models` | A comma-separated list of models for OpenWebUI. | `llama3:latest,cogito:14b,gemma3:12b` |
|
||||
|
||||
## Usage
|
||||
|
||||
* **Gallery:** Open your browser to `http://<server_ip>:<port>` to see the gallery of generated images.
|
||||
* **Create Image:** Navigate to `/create` to manually trigger image generation.
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Flask
|
||||
* comfy_api_simplified
|
||||
* APScheduler
|
||||
* Pillow
|
||||
* And others, see `requirements.txt`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
## License
|
||||
|
||||
This project is unlicensed.
|
@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
# Check if an argument is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <part>"
|
||||
echo "Example: $0 patch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PART=$1
|
||||
|
||||
# Bump the version
|
||||
bump-my-version bump $PART
|
||||
|
||||
# Push the changes
|
||||
git push
|
||||
git push origin --tags
|
@ -103,8 +103,7 @@ def generate_image(
|
||||
# Generate image
|
||||
logging.debug(f"Generating image: {file_name}")
|
||||
results = api.queue_and_wait_images(wf, save_node)
|
||||
rename_image()
|
||||
|
||||
|
||||
for _, image_data in results.items():
|
||||
output_path = os.path.join(
|
||||
user_config["comfyui"]["output_dir"], f"{file_name}.png"
|
||||
@ -112,6 +111,7 @@ def generate_image(
|
||||
with open(output_path, "wb+") as f:
|
||||
f.write(image_data)
|
||||
generate_thumbnail(output_path)
|
||||
rename_image(f"{file_name}.png")
|
||||
|
||||
logging.debug(f"Image generated successfully for UID: {file_name}")
|
||||
|
||||
@ -143,7 +143,7 @@ def select_model(model: str) -> tuple[str, str]:
|
||||
|
||||
def create_image(prompt: str | None = None, model: str = "Random") -> None:
|
||||
"""Generate an image with a chosen workflow (Random, FLUX*, or SDXL*)."""
|
||||
|
||||
from datetime import datetime
|
||||
if prompt is None:
|
||||
prompt = create_prompt_on_openwebui(user_config["comfyui"]["prompt"])
|
||||
|
||||
@ -153,10 +153,14 @@ def create_image(prompt: str | None = None, model: str = "Random") -> None:
|
||||
|
||||
save_prompt(prompt)
|
||||
selected_workflow, model = select_model(model)
|
||||
|
||||
# Generate a unique filename using a timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
file_name = f"{timestamp}_{random.getrandbits(32)}"
|
||||
|
||||
if selected_workflow == "FLUX":
|
||||
generate_image(
|
||||
file_name="image",
|
||||
file_name=file_name,
|
||||
comfy_prompt=prompt,
|
||||
workflow_path="./workflow_flux.json",
|
||||
prompt_node="Positive Prompt T5",
|
||||
@ -169,6 +173,6 @@ def create_image(prompt: str | None = None, model: str = "Random") -> None:
|
||||
model=model
|
||||
)
|
||||
else: # SDXL
|
||||
generate_image("image", comfy_prompt=prompt, model=model)
|
||||
generate_image(file_name, comfy_prompt=prompt, model=model)
|
||||
|
||||
logging.info(f"{selected_workflow} generation started with prompt: {prompt}")
|
||||
|
@ -48,33 +48,41 @@ def load_config() -> configparser.ConfigParser:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def rename_image() -> str | None:
|
||||
"""Renames 'image.png' in the output folder to a timestamped filename if it exists."""
|
||||
old_path = os.path.join(user_config["comfyui"]["output_dir"], "image.png")
|
||||
favourites_file = "./favourites.json"
|
||||
def rename_image(new_filename: str) -> str | None:
|
||||
"""Renames the latest image in the output folder to a timestamped filename."""
|
||||
output_dir = user_config["comfyui"]["output_dir"]
|
||||
image_png_path = os.path.join(output_dir, "image.png")
|
||||
|
||||
# Check if image.png exists and is a favourite
|
||||
if os.path.exists(image_png_path):
|
||||
favourites = get_favourites()
|
||||
if "image.png" in favourites:
|
||||
# It's a favourite, so rename it to preserve it
|
||||
timestamped_filename = f"{datetime.now().strftime('%Y%m%d-%H%M%S')}.png"
|
||||
timestamped_path = os.path.join(output_dir, timestamped_filename)
|
||||
os.rename(image_png_path, timestamped_path)
|
||||
|
||||
# Update favourites list
|
||||
favourites.remove("image.png")
|
||||
favourites.append(timestamped_filename)
|
||||
save_favourites(favourites)
|
||||
print(f"Preserved favourite 'image.png' as '{timestamped_filename}'")
|
||||
|
||||
if os.path.exists(old_path):
|
||||
new_filename = f"{str(time.time())}.png"
|
||||
new_path = os.path.join(user_config["comfyui"]["output_dir"], new_filename)
|
||||
|
||||
# Check if image.png is a favourite
|
||||
if os.path.exists(favourites_file):
|
||||
with open(favourites_file, 'r') as f:
|
||||
favourites = json.load(f)
|
||||
if "image.png" in favourites:
|
||||
favourites.remove("image.png")
|
||||
favourites.append(new_filename)
|
||||
with open(favourites_file, 'w') as f:
|
||||
json.dump(favourites, f)
|
||||
|
||||
os.rename(old_path, new_path)
|
||||
generate_thumbnail(new_path)
|
||||
print(f"Renamed 'image.png' to '{new_filename}'")
|
||||
return new_filename
|
||||
else:
|
||||
print("No image.png found.")
|
||||
# Find the latest generated image (which is the new_filename)
|
||||
latest_image_path = os.path.join(output_dir, new_filename)
|
||||
if not os.path.exists(latest_image_path):
|
||||
print(f"Error: Newly generated image '{new_filename}' not found.")
|
||||
return None
|
||||
|
||||
# Rename the latest image to "image.png"
|
||||
if os.path.exists(image_png_path):
|
||||
os.remove(image_png_path) # remove if it wasn't a favourite
|
||||
|
||||
os.rename(latest_image_path, image_png_path)
|
||||
generate_thumbnail(image_png_path)
|
||||
print(f"Renamed '{new_filename}' to 'image.png'")
|
||||
return "image.png"
|
||||
|
||||
|
||||
def get_details_from_png(path):
|
||||
try:
|
||||
@ -124,5 +132,18 @@ def load_topics_from_config():
|
||||
sorted_topics = sorted(topics, key=str.lower)
|
||||
return sorted_topics
|
||||
|
||||
|
||||
favourites_file = "./favourites.json"
|
||||
|
||||
def get_favourites():
|
||||
if not os.path.exists(favourites_file):
|
||||
return []
|
||||
with open(favourites_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_favourites(favourites):
|
||||
with open(favourites_file, 'w') as f:
|
||||
json.dump(favourites, f)
|
||||
|
||||
user_config = load_config()
|
||||
output_folder = user_config["comfyui"]["output_dir"]
|
@ -4,17 +4,7 @@ import json
|
||||
|
||||
bp = Blueprint("gallery_routes", __name__)
|
||||
image_folder = "./output"
|
||||
favourites_file = "./favourites.json"
|
||||
|
||||
def get_favourites():
|
||||
if not os.path.exists(favourites_file):
|
||||
return []
|
||||
with open(favourites_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_favourites(favourites):
|
||||
with open(favourites_file, 'w') as f:
|
||||
json.dump(favourites, f)
|
||||
from libs.generic import get_favourites, save_favourites
|
||||
|
||||
@bp.route("/images", methods=["GET"])
|
||||
def gallery():
|
||||
|
@ -1,51 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
background: #333;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button-link:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
right: 12px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
user-select: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.version a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.version a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% block title %}AI Image Frame{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<!-- Version number at bottom right -->
|
||||
<div class="version">
|
||||
<a href="{{ url_for('settings_route.config_editor') }}">v{{ version }}</a>
|
||||
</div>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
@ -1,199 +1,219 @@
|
||||
{% extends "base.html" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
{% block title %}Create An Image{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 80vw;
|
||||
height: 200px;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
font-family: monospace;
|
||||
resize: none;
|
||||
margin-bottom: 20px;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
background: #333;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
select:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
#spinner-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
visibility: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 6px solid #555;
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Create An Image</title>
|
||||
<style>
|
||||
/* ---------- reset ---------- */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
/* ---------- layout ---------- */
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
padding-top: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 80vw;
|
||||
height: 200px;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
font-size: 16px;
|
||||
font-family: monospace;
|
||||
resize: none;
|
||||
margin-bottom: 20px;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
width: 100%;
|
||||
background: #333;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 150px;
|
||||
button:hover,
|
||||
select:hover {
|
||||
background: #555;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="margin-bottom: 20px;">Create An Image</h1>
|
||||
/* ---------- spinner ---------- */
|
||||
#spinner-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
visibility: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
<textarea id="prompt-box" placeholder="Enter your custom prompt here..."></textarea>
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 6px solid #555;
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
<div class="button-group">
|
||||
<button onclick="showSpinner(); location.href='/'">Back</button>
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
<button onclick="sendPrompt()">Send Prompt</button>
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
<button onclick="randomPrompt()">Random Prompt</button>
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
<select id="model-select">
|
||||
<option value="" selected>Random</option>
|
||||
<optgroup label="FLUX">
|
||||
{% for m in models if 'flux' in m|lower %}
|
||||
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
<optgroup label="SDXL">
|
||||
{% for m in models if 'flux' not in m|lower %}
|
||||
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
</select>
|
||||
button,
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
<select id="topic-select">
|
||||
<option value="">No Topic</option>
|
||||
<option value="random">Random</option>
|
||||
<optgroup label="Topics">
|
||||
{% for t in topics %}
|
||||
<option value="{{ t }}">{{ t }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
textarea {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<div id="spinner-overlay">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
<body>
|
||||
<h1 style="margin-bottom: 20px;">Create An Image</h1>
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const overlay = document.getElementById('spinner-overlay');
|
||||
<textarea id="prompt-box" placeholder="Enter your custom prompt here..."></textarea>
|
||||
|
||||
function showSpinner() { overlay.style.visibility = 'visible'; }
|
||||
<div class="button-group">
|
||||
<button onclick="showSpinner(); location.href='/'">Back</button>
|
||||
|
||||
function sendPrompt() {
|
||||
showSpinner();
|
||||
const prompt = document.getElementById('prompt-box').value;
|
||||
const model = document.getElementById('model-select').value;
|
||||
<button onclick="sendPrompt()">Send Prompt</button>
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('prompt', prompt);
|
||||
formData.append('model', model);
|
||||
<button onclick="randomPrompt()">Random Prompt</button>
|
||||
|
||||
fetch('/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData.toString()
|
||||
})
|
||||
.then(response => {
|
||||
window.location.href = response.redirected ? response.url : '/create';
|
||||
<select id="model-select">
|
||||
<option value="" selected>Random</option>
|
||||
<!-- Group: FLUX -->
|
||||
<optgroup label="FLUX">
|
||||
{% for m in models if 'flux' in m|lower %}
|
||||
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
<!-- Group: SDXL -->
|
||||
<optgroup label="SDXL">
|
||||
{% for m in models if 'flux' not in m|lower %}
|
||||
<option value="{{ m }}">{{ m.rsplit('.', 1)[0] }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
<select id="topic-select">
|
||||
<option value="">No Topic</option>
|
||||
<option value="random">Random</option>
|
||||
<optgroup label="Topics">
|
||||
{% for t in topics %}
|
||||
<option value="{{ t }}">{{ t }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
</select>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- waiting overlay -->
|
||||
<div id="spinner-overlay">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const overlay = document.getElementById('spinner-overlay');
|
||||
|
||||
function showSpinner() { overlay.style.visibility = 'visible'; }
|
||||
|
||||
function sendPrompt() {
|
||||
showSpinner();
|
||||
const prompt = document.getElementById('prompt-box').value;
|
||||
const model = document.getElementById('model-select').value;
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('prompt', prompt);
|
||||
formData.append('model', model);
|
||||
|
||||
fetch('/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData.toString()
|
||||
})
|
||||
.catch(error => {
|
||||
overlay.style.visibility = 'hidden';
|
||||
alert("Error sending prompt: " + error);
|
||||
});
|
||||
}
|
||||
.then(response => {
|
||||
window.location.href = response.redirected ? response.url : '/create';
|
||||
})
|
||||
.catch(error => {
|
||||
overlay.style.visibility = 'hidden';
|
||||
alert("Error sending prompt: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
function randomPrompt() {
|
||||
showSpinner();
|
||||
const model = document.getElementById('model-select').value;
|
||||
const topic = document.getElementById('topic-select').value;
|
||||
// wrapper for Random Prompt button so it also sends the model
|
||||
function randomPrompt() {
|
||||
showSpinner();
|
||||
const model = document.getElementById('model-select').value;
|
||||
const topic = document.getElementById('topic-select').value; // this line was missing
|
||||
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('model', model);
|
||||
formData.append('topic', topic);
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('model', model);
|
||||
formData.append('topic', topic); // include topic in request
|
||||
|
||||
fetch('/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData.toString()
|
||||
})
|
||||
.then(response => {
|
||||
window.location.href = response.redirected ? response.url : '/create';
|
||||
fetch('/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: formData.toString()
|
||||
})
|
||||
.catch(error => {
|
||||
overlay.style.visibility = 'hidden';
|
||||
alert("Error requesting random prompt: " + error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
.then(response => {
|
||||
window.location.href = response.redirected ? response.url : '/create';
|
||||
})
|
||||
.catch(error => {
|
||||
overlay.style.visibility = 'hidden';
|
||||
alert("Error requesting random prompt: " + error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,9 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
{% block title %}Image Archive{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Image Archive</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
@ -87,14 +102,18 @@
|
||||
text-align: left;
|
||||
margin-top: 20px;
|
||||
max-height: 25vh;
|
||||
/* NEW: restrict height */
|
||||
overflow-y: auto;
|
||||
/* NEW: allow vertical scroll */
|
||||
}
|
||||
|
||||
/* Back button fixed top right */
|
||||
.home-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 500;
|
||||
/* lower than lightbox (999) */
|
||||
}
|
||||
|
||||
.favourites-button {
|
||||
@ -113,6 +132,24 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
background: #333;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button-link:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
@ -144,7 +181,9 @@
|
||||
max-width: 90%;
|
||||
padding: 8px 16px;
|
||||
max-height: 20vh;
|
||||
/* smaller height for mobile */
|
||||
overflow-y: auto;
|
||||
/* keep scroll on mobile too */
|
||||
}
|
||||
|
||||
.button-link {
|
||||
@ -153,16 +192,18 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
{% block content %}
|
||||
<body>
|
||||
<a href="/" class="button-link home-button">Home</a>
|
||||
<button class="button-link favourites-button" id="favourites-button" onclick="toggleFavouritesView()">Show Favourites</button>
|
||||
|
||||
<h1 id="page-title">Image Archive</h1>
|
||||
|
||||
<!-- Empty gallery container; images will be loaded incrementally -->
|
||||
<div class="gallery" id="gallery"></div>
|
||||
|
||||
<!-- Lightbox -->
|
||||
<div class="lightbox" id="lightbox" tabindex="-1" onkeyup="handleLightboxKeys(event)">
|
||||
<span class="close" onclick="closeLightbox()">×</span>
|
||||
<span class="favourite-heart" id="favourite-heart" onclick="toggleFavourite()">♡</span>
|
||||
@ -171,9 +212,8 @@
|
||||
<p id="lightbox-prompt"></p>
|
||||
<span class="arrow right" onclick="nextImage()">❯</span>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<!-- Pass image filenames from Flask to JS -->
|
||||
<script>
|
||||
let allImages = JSON.parse(`[
|
||||
{% for image in images %}
|
||||
@ -184,10 +224,10 @@
|
||||
|
||||
<script>
|
||||
const gallery = document.getElementById('gallery');
|
||||
const batchSize = 9;
|
||||
const batchSize = 9; // images to load per batch
|
||||
let loadedCount = 0;
|
||||
let currentIndex = 0;
|
||||
const detailsCache = {};
|
||||
const detailsCache = {}; // Cache for image details
|
||||
let showingFavourites = false;
|
||||
let filteredImages = allImages;
|
||||
|
||||
@ -236,16 +276,19 @@
|
||||
renderGallery();
|
||||
}
|
||||
|
||||
// Load initial batch
|
||||
renderGallery();
|
||||
|
||||
// Load more images when scrolling near bottom
|
||||
window.addEventListener('scroll', () => {
|
||||
const imagesToLoad = showingFavourites ? filteredImages : allImages;
|
||||
if (loadedCount >= imagesToLoad.length) return;
|
||||
if (loadedCount >= imagesToLoad.length) return; // all loaded
|
||||
if ((window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 100)) {
|
||||
loadNextBatch();
|
||||
}
|
||||
});
|
||||
|
||||
// Get current images in gallery for lightbox navigation
|
||||
function getGalleryImages() {
|
||||
return Array.from(gallery.querySelectorAll('img'));
|
||||
}
|
||||
@ -261,7 +304,7 @@
|
||||
|
||||
function updateFavouriteHeart(isFavourited) {
|
||||
const heart = document.getElementById('favourite-heart');
|
||||
heart.innerHTML = isFavourited ? '♥' : '♡';
|
||||
heart.innerHTML = isFavourited ? '♥' : '♡'; // solid vs outline heart
|
||||
heart.style.color = isFavourited ? 'red' : 'white';
|
||||
}
|
||||
|
||||
@ -318,7 +361,7 @@
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
detailsCache[filename] = data;
|
||||
detailsCache[filename] = data; // Cache the data
|
||||
document.getElementById("lightbox-prompt").textContent =
|
||||
`Model:${data.model} - Created:${data.date}\n\n${data.prompt}`;
|
||||
})
|
||||
@ -333,6 +376,7 @@
|
||||
const imagesToLoad = showingFavourites ? filteredImages : allImages;
|
||||
if (currentIndex + 1 >= images.length && loadedCount < imagesToLoad.length) {
|
||||
loadNextBatch();
|
||||
// Wait briefly to ensure DOM updates
|
||||
setTimeout(() => {
|
||||
const updatedImages = getGalleryImages();
|
||||
if (currentIndex + 1 < updatedImages.length) {
|
||||
@ -352,9 +396,11 @@
|
||||
showImageAndLoadDetails(currentIndex);
|
||||
}
|
||||
|
||||
|
||||
function closeLightbox() {
|
||||
document.getElementById("lightbox").style.display = "none";
|
||||
if (showingFavourites) {
|
||||
// Refresh the gallery if a favourite was removed
|
||||
const currentImage = getGalleryImages()[currentIndex];
|
||||
const wasFavourited = currentImage.dataset.favourited === 'true';
|
||||
const originalImage = allImages.find(img => img.filename === currentImage.dataset.filename);
|
||||
@ -376,4 +422,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,15 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Image Created{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Image Created</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.message {
|
||||
@ -40,13 +50,13 @@
|
||||
background: #555;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="message">Image will be made with <i>{{ model }}</i> using prompt:</div>
|
||||
<div class="prompt-text">
|
||||
{{ prompt }}
|
||||
</div>
|
||||
<button onclick="location.href='/'">Home</button>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -1,20 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
{% block title %}AI Image of the Day{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI Image of the Day</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
position: relative;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.image-container {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
@ -42,7 +54,9 @@
|
||||
max-width: 80vw;
|
||||
text-align: left;
|
||||
max-height: 30vh;
|
||||
/* NEW: limit height */
|
||||
overflow-y: auto;
|
||||
/* NEW: allow scrolling */
|
||||
}
|
||||
|
||||
.button-group {
|
||||
@ -52,6 +66,46 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
background: #333;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-link:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* New style for version number */
|
||||
.version {
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
right: 12px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
user-select: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.version a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.version a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.image-container {
|
||||
max-width: 100vw;
|
||||
@ -80,9 +134,14 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
<script>
|
||||
setInterval(() => {
|
||||
location.reload();
|
||||
}, {{ reload_interval }}); // Refresh every X ms
|
||||
</script>
|
||||
</head>
|
||||
|
||||
{% block content %}
|
||||
<body>
|
||||
{% if image %}
|
||||
<div class="image-container">
|
||||
<img src="{{ url_for('image_routes.serve_image', filename=image) }}" alt="Latest Image" />
|
||||
@ -97,12 +156,11 @@
|
||||
{% else %}
|
||||
<p>No images found</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
setInterval(() => {
|
||||
location.reload();
|
||||
}, {{ reload_interval }}); // Refresh every X ms
|
||||
</script>
|
||||
{% endblock %}
|
||||
<!-- Version number at bottom right -->
|
||||
<div class="version">
|
||||
<a href="{{ url_for('settings_route.config_editor') }}">v{{ version }}</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,15 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.message {
|
||||
@ -49,9 +59,8 @@
|
||||
background: #555;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="message">Please enter the password to continue:</div>
|
||||
<form method="post">
|
||||
<div class="prompt-text">
|
||||
@ -60,4 +69,5 @@
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,10 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
{% block title %}Config Editor{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Config Editor</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
display: flex;
|
||||
@ -63,11 +74,21 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.back-button-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
.button-link {
|
||||
background: #333;
|
||||
height: 40px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: background 0.3s;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-link:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@ -81,10 +102,18 @@
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
.back-button-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="box">
|
||||
<h2>Topics</h2>
|
||||
<form method="post">
|
||||
@ -157,4 +186,7 @@
|
||||
<div class="back-button-wrapper">
|
||||
<a href="/" class="button-link">Back to Home</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user