chore: Update everything

This commit is contained in:
2026-07-24 15:58:31 +02:00
parent 5100ad8365
commit 6bd4d58373
34 changed files with 8829 additions and 1 deletions
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "bottle",
# ]
# ///
"""Browse images one-by-one and assign text prompts via a local web UI.
Saves results to a JSONL file: {"filename": "...", "prompt": "..."}
Images navigated past without a prompt produce no output line.
"""
import argparse
import json
import socket
import sys
import threading
import time
import webbrowser
from pathlib import Path
import bottle
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
CANNED_PROMPTS: list[str] = [
"photorealistic. skin must be flawless",
]
"""Pre-populated prompt history shown in the UI on first launch."""
BUILDING_BLOCKS: list[str] = [
"make her prettier",
"age her by 5 years and make her look like a 25 year old",
"keep the dark skin",
"wet skin",
"skin covered with baby oil, shiny skin",
"soaking wet skin and hair, wet clothes clinging to her body",
"long, flowy hair",
"japanese",
"greek",
"no extra or missing fingers. each hand must have 5 fingers with the same hand pose as the original image",
# expression
"direct her gaze at the camera",
"give her a serious and seductive look",
"give her a playful expression",
"slightly parted lips",
"french kiss, eyes closed, tongue out",
"closed eyes",
"lower her eyelids and slightly part her lips in a seductive fashion",
"exaggerated expression",
"soft dramatic lighting",
# outfit
"replace the outfit with glossy latex",
"remove the tan lines, she doesn't wear white bra",
]
"""Palette of text snippets the user can append to any prompt."""
VALID_EXTENSIONS: frozenset[str] = frozenset({".webp", ".jpg", ".jpeg", ".png"})
HTML: str = (Path(__file__).parent / "prompter.html").read_text(encoding="utf-8")
# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------
def find_free_port() -> int:
"""Bind to port 0 and return the OS-assigned port number."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
class PrompterApp:
def __init__(self, images: list[str], output_path: Path) -> None:
self.images = images
self.output_path = output_path
self.app = bottle.Bottle()
self.app.route("/")(self.index)
self.app.route("/api/images")(self.api_images)
self.app.route("/image/<idx:int>")(self.serve_image)
self.app.route("/api/save", method="POST")(self.api_save)
def index(self) -> str:
return HTML
def api_images(self) -> str:
"""Return image list, canned prompts, building blocks, and existing prompts."""
stem_to_path: dict[str, str] = {Path(p).stem: p for p in self.images}
existing_by_stem = self._load_existing_prompts()
existing_prompts = {
stem_to_path[stem]: prompt
for stem, prompt in existing_by_stem.items()
if stem in stem_to_path
}
bottle.response.content_type = "application/json"
return json.dumps(
{
"images": self.images,
"canned_prompts": CANNED_PROMPTS,
"building_blocks": BUILDING_BLOCKS,
"existing_prompts": existing_prompts,
}
)
def serve_image(self, idx: int) -> bottle.HTTPResponse:
if idx < 0 or idx >= len(self.images):
bottle.abort(404, "Image not found")
path = Path(self.images[idx])
return bottle.static_file(path.name, root=str(path.parent))
def api_save(self) -> str:
data: dict = bottle.request.json or {}
file_path: str = data.get("file_path", "")
prompt: str = data.get("prompt", "")
if file_path and prompt:
self._save_entry(file_path, prompt)
bottle.response.content_type = "application/json"
return json.dumps({"ok": True})
def _load_existing_prompts(self) -> dict[str, str]:
"""Read the output JSONL and return a mapping of stem → prompt.
When a filename appears multiple times the last entry wins, so re-running
the tool and overwriting a previous prompt works naturally.
"""
if not self.output_path.exists():
return {}
prompts: dict[str, str] = {}
with open(self.output_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
if "filename" in entry and "prompt" in entry:
prompts[entry["filename"]] = entry["prompt"]
except json.JSONDecodeError:
pass
return prompts
def _save_entry(self, file_path: str, prompt: str) -> None:
"""Append one {filename, prompt} record to the JSONL output file."""
with open(self.output_path, "a", encoding="utf-8") as f:
f.write(
json.dumps({"filename": Path(file_path).stem, "prompt": prompt}) + "\n"
)
def run(self, port: int) -> None:
bottle.run(self.app, host="localhost", port=port, quiet=True)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Browse images and assign text prompts via a web UI.",
)
parser.add_argument("images", nargs="+", help="Image paths to browse")
parser.add_argument(
"-o",
"--output-path",
default="prompts.jsonl",
metavar="FILE",
help="Output JSONL file path (default: prompts.jsonl)",
)
args = parser.parse_args()
images = [
str(Path(p).resolve())
for p in args.images
if Path(p).suffix.lower() in VALID_EXTENSIONS
]
if not images:
exts = ", ".join(sorted(VALID_EXTENSIONS))
print(
f"Error: no valid images found. Expected extensions: {exts}",
file=sys.stderr,
)
sys.exit(1)
print(f"Loaded {len(images)} image(s).")
port = find_free_port()
url = f"http://localhost:{port}"
print(f"Starting server at {url}")
threading.Thread(
target=lambda: (time.sleep(0.8), webbrowser.open(url)),
daemon=True,
).start()
PrompterApp(images, Path(args.output_path)).run(port)
if __name__ == "__main__":
main()