Spaces:
Sleeping
Sleeping
| """Local web UI for Image Processor Pro. | |
| A thin Flask front-end over the EXISTING pipeline. It does not change any | |
| processing logic — it builds a Config and calls ImagePipeline.process_many() | |
| exactly like the CLI (main.py) does, then shows the results in the browser. | |
| Run: | |
| .venv/bin/python webapp.py # http://127.0.0.1:5001 | |
| .venv/bin/python webapp.py --port 8080 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import threading | |
| from pathlib import Path | |
| from flask import Flask, jsonify, render_template_string, request, send_from_directory | |
| from config import Config | |
| from pipeline import ImagePipeline | |
| from utils import human_size, setup_logging | |
| app = Flask(__name__) | |
| OUTPUT_ROOT = Path("output").resolve() | |
| # One pipeline per (remove_watermark, engine) combo so the LaMa model is loaded | |
| # once and reused across requests. Batches are serialised — this is a local, | |
| # single-user tool and it keeps us clear of any shared-state questions. | |
| _pipelines: dict[tuple[bool, str], ImagePipeline] = {} | |
| _pipelines_lock = threading.Lock() | |
| _process_lock = threading.Lock() | |
| def get_pipeline(remove_watermark: bool, engine: str) -> ImagePipeline: | |
| key = (remove_watermark, engine) | |
| with _pipelines_lock: | |
| pipeline = _pipelines.get(key) | |
| if pipeline is None: | |
| config = Config() | |
| config.remove_watermark = remove_watermark | |
| config.watermark_engine = engine | |
| pipeline = ImagePipeline(config) | |
| _pipelines[key] = pipeline | |
| return pipeline | |
| def _output_url(output_path: Path) -> str | None: | |
| try: | |
| rel = output_path.resolve().relative_to(OUTPUT_ROOT) | |
| except (ValueError, OSError): | |
| return None | |
| return "/output/" + str(rel).replace("\\", "/") | |
| def index() -> str: | |
| return render_template_string(PAGE) | |
| def serve_output(subpath: str): | |
| # send_from_directory rejects path traversal outside OUTPUT_ROOT. | |
| return send_from_directory(OUTPUT_ROOT, subpath) | |
| def process(): | |
| data = request.get_json(silent=True) or {} | |
| raw = data.get("urls", "") | |
| urls = [line.strip() for line in raw.splitlines() if line.strip()] | |
| if not urls: | |
| return jsonify(error="Please paste at least one image URL."), 400 | |
| remove_watermark = bool(data.get("remove_watermark", True)) | |
| engine = "classical" if data.get("engine") == "classical" else "lama" | |
| pipeline = get_pipeline(remove_watermark, engine) | |
| with _process_lock: # one batch at a time | |
| results = pipeline.process_many(urls) | |
| by_url: dict[str, object] = {} | |
| for r in results: | |
| by_url.setdefault(r.url, r) | |
| payload = [] | |
| for url in urls: | |
| r = by_url.get(url) | |
| if r is None: | |
| payload.append(dict(url=url, success=False, error="No result returned.")) | |
| continue | |
| dims = f"{r.final_dimensions[0]}×{r.final_dimensions[1]}" if r.final_dimensions else None | |
| payload.append(dict( | |
| url=url, | |
| success=r.success, | |
| output_url=_output_url(r.output_path) if r.output_path else None, | |
| dimensions=dims, | |
| size=human_size(r.final_size_bytes) if r.success else None, | |
| wm_seconds=round(r.watermark_seconds, 2), | |
| error=r.error, | |
| )) | |
| succeeded = sum(1 for p in payload if p["success"]) | |
| return jsonify(results=payload, succeeded=succeeded, failed=len(payload) - succeeded, | |
| engine=engine, remove_watermark=remove_watermark) | |
| PAGE = r""" | |
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Image Processor Pro</title> | |
| <style> | |
| :root { color-scheme: light dark; } | |
| * { box-sizing: border-box; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| margin: 0; background: #f6f7f9; color: #1c1d22; } | |
| header { background: linear-gradient(135deg,#4f46e5,#7c3aed); color:#fff; padding: 22px 28px; } | |
| header h1 { margin: 0; font-size: 20px; font-weight: 650; } | |
| header p { margin: 6px 0 0; opacity: .85; font-size: 13px; } | |
| main { max-width: 1100px; margin: 0 auto; padding: 24px 20px 64px; } | |
| .panel { background:#fff; border:1px solid #e6e7eb; border-radius:14px; padding:20px; | |
| box-shadow:0 1px 3px rgba(0,0,0,.05); } | |
| textarea { width:100%; min-height:150px; resize:vertical; padding:12px 14px; font:13px/1.5 ui-monospace, | |
| SFMono-Regular,Menlo,monospace; border:1px solid #d6d8df; border-radius:10px; background:#fbfbfc; color:#1c1d22;} | |
| .row { display:flex; flex-wrap:wrap; gap:18px; align-items:center; margin-top:14px; } | |
| label.opt { display:flex; align-items:center; gap:7px; font-size:14px; cursor:pointer; } | |
| select { padding:7px 10px; border-radius:8px; border:1px solid #d6d8df; background:#fff; font-size:14px; } | |
| button { margin-left:auto; background:#4f46e5; color:#fff; border:0; border-radius:10px; | |
| padding:11px 22px; font-size:15px; font-weight:600; cursor:pointer; } | |
| button:disabled { opacity:.55; cursor:not-allowed; } | |
| .hint { color:#6b7280; font-size:12.5px; margin:4px 2px 0; } | |
| #status { margin:20px 2px 0; font-size:14px; min-height:20px; } | |
| .spinner { display:inline-block; width:15px; height:15px; border:2px solid #c7c9d1; | |
| border-top-color:#4f46e5; border-radius:50%; animation:spin .8s linear infinite; vertical-align:-3px; margin-right:7px;} | |
| @keyframes spin { to { transform: rotate(360deg);} } | |
| .grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(320px,1fr)); gap:16px; margin-top:18px; } | |
| .card { background:#fff; border:1px solid #e6e7eb; border-radius:14px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,.05); } | |
| .card .imgs { display:grid; grid-template-columns:1fr 1fr; gap:1px; background:#eceef2; } | |
| .card figure { margin:0; background:#fff; } | |
| .card figcaption { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:#8a8f9a; | |
| text-align:center; padding:6px 0 2px; } | |
| .card img { width:100%; aspect-ratio:1/1; object-fit:contain; display:block; background:#fafbfc; } | |
| .card .meta { padding:11px 13px 13px; } | |
| .card .u { font-size:11.5px; color:#6b7280; word-break:break-all; line-height:1.35; } | |
| .card .stat { margin-top:8px; font-size:13px; display:flex; align-items:center; gap:8px; flex-wrap:wrap; } | |
| .ok { color:#15803d; font-weight:600; } .bad { color:#b91c1c; font-weight:600; } | |
| .dl { margin-left:auto; font-size:12.5px; color:#4f46e5; text-decoration:none; font-weight:600; } | |
| .err { padding:14px; font-size:13px; color:#b91c1c; background:#fef2f2; } | |
| .summary { margin:6px 2px 0; font-size:14px; font-weight:600; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Image Processor Pro</h1> | |
| <p>Paste image URLs (one per line) — they’re downloaded, watermark-removed, and saved as optimized JPEGs.</p> | |
| </header> | |
| <main> | |
| <div class="panel"> | |
| <textarea id="urls" placeholder="https://images.meesho.com/images/products/.../xxxxx_512.avif https://images.meesho.com/images/products/.../yyyyy_512.avif ... one URL per line, add as many as you like"></textarea> | |
| <p class="hint">Tip: paste 10+ URLs at once — they’re processed together.</p> | |
| <div class="row"> | |
| <label class="opt"><input type="checkbox" id="rmwm" checked> Remove watermark</label> | |
| <button id="go">Process</button> | |
| </div> | |
| </div> | |
| <div id="status"></div> | |
| <div class="summary" id="summary"></div> | |
| <div class="grid" id="grid"></div> | |
| </main> | |
| <script> | |
| const $ = id => document.getElementById(id); | |
| const go = $("go"), statusEl = $("status"), grid = $("grid"), summary = $("summary"); | |
| function escapeHtml(s){ return s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } | |
| function card(r){ | |
| const u = escapeHtml(r.url); | |
| if(!r.success){ | |
| return `<div class="card"><div class="err"><b>Failed</b><br>${escapeHtml(r.error||'unknown error')}</div> | |
| <div class="meta"><div class="u">${u}</div></div></div>`; | |
| } | |
| return `<div class="card"> | |
| <div class="imgs"> | |
| <figure><figcaption>Original</figcaption><img loading="lazy" src="${u}" alt="original"></figure> | |
| <figure><figcaption>Processed</figcaption><img loading="lazy" src="${r.output_url}" alt="processed"></figure> | |
| </div> | |
| <div class="meta"> | |
| <div class="u">${u}</div> | |
| <div class="stat"><span class="ok">✓ done</span> | |
| <span>${r.dimensions||''} · ${r.size||''}</span> | |
| <span style="color:#9aa0ac">wm ${r.wm_seconds}s</span> | |
| <a class="dl" href="${r.output_url}" download>Download</a></div> | |
| </div></div>`; | |
| } | |
| async function run(){ | |
| const urls = $("urls").value; | |
| const list = urls.split("\n").map(s=>s.trim()).filter(Boolean); | |
| if(!list.length){ statusEl.textContent = "Paste at least one URL first."; return; } | |
| go.disabled = true; grid.innerHTML = ""; summary.textContent = ""; | |
| statusEl.innerHTML = `<span class="spinner"></span>Processing ${list.length} image(s)… LaMa runs ~2–6s each, please wait.`; | |
| try{ | |
| const resp = await fetch("/process", { | |
| method:"POST", headers:{"Content-Type":"application/json"}, | |
| body: JSON.stringify({ urls, remove_watermark:$("rmwm").checked, engine:"lama" }) | |
| }); | |
| const data = await resp.json(); | |
| if(!resp.ok){ statusEl.textContent = data.error || "Something went wrong."; return; } | |
| statusEl.textContent = ""; | |
| summary.textContent = `${data.succeeded} succeeded` + (data.failed ? `, ${data.failed} failed` : "") + | |
| (data.remove_watermark ? "" : " · watermark removal off"); | |
| grid.innerHTML = data.results.map(card).join(""); | |
| }catch(e){ | |
| statusEl.textContent = "Request failed: " + e.message; | |
| }finally{ | |
| go.disabled = false; | |
| } | |
| } | |
| go.addEventListener("click", run); | |
| $("urls").addEventListener("keydown", e => { if((e.metaKey||e.ctrlKey) && e.key==="Enter") run(); }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Local web UI for Image Processor Pro.") | |
| parser.add_argument("--host", default="127.0.0.1", help="Bind host (default 127.0.0.1, local only).") | |
| parser.add_argument("--port", type=int, default=5001, help="Port (default 5001).") | |
| args = parser.parse_args() | |
| setup_logging(Path("logs"), verbose=False) | |
| OUTPUT_ROOT.mkdir(parents=True, exist_ok=True) | |
| print(f"\n Image Processor Pro UI -> http://{args.host}:{args.port}\n") | |
| app.run(host=args.host, port=args.port, threaded=True) | |
| if __name__ == "__main__": | |
| main() | |