from __future__ import annotations import argparse import json import mimetypes import threading import traceback from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from itertools import count from pathlib import Path from typing import Any from urllib.parse import parse_qs, unquote, urlparse import pipeline STATIC_DIR = Path(__file__).resolve().parent / "static" JOBS: dict[str, dict[str, Any]] = {} JOB_IDS = count(1) JOB_LOCK = threading.Lock() def _json_bytes(payload: Any) -> bytes: return json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8") def _new_job(kind: str, target, *args, **kwargs) -> dict[str, Any]: job_id = str(next(JOB_IDS)) job = { "id": job_id, "kind": kind, "status": "running", "logs": [], "result": None, "error": None, } with JOB_LOCK: JOBS[job_id] = job def log(message: str) -> None: with JOB_LOCK: job["logs"].append(message) job["logs"] = job["logs"][-400:] def wrapped() -> None: try: job["result"] = target(log, *args, **kwargs) job["status"] = "done" except Exception as exc: job["status"] = "error" job["error"] = str(exc) log(traceback.format_exc()) threading.Thread(target=wrapped, daemon=True).start() return job def _run_dehaze_job(log, payload: dict[str, Any]) -> dict[str, Any]: filename = payload["image"] methods = payload.get("methods") or [] options = payload.get("options") or {} postprocessors = payload.get("postprocessors") or [] post_sources = payload.get("post_sources") or [] reference = payload.get("reference") or filename completed = [] failed = [] for method in methods: try: result = pipeline.run_dehaze_method(filename, method, options.get(method, options), log) completed.append({"method": method, "path": pipeline.relpath(result)}) except Exception as exc: failed.append({"method": method, "error": str(exc)}) log(f"[{method}] 失败: {exc}") if postprocessors: sources = post_sources or [item["method"] for item in completed] for source in sources: try: pipeline.run_postprocessors_for_source( filename, source, postprocessors, params=payload.get("post_params") or {}, reference_filename=reference, log=log, ) except Exception as exc: failed.append({"postprocess_source": source, "error": str(exc)}) log(f"[后处理/{source}] 失败: {exc}") return {"completed": completed, "failed": failed, "results": pipeline.get_results(filename)} def _run_post_job(log, payload: dict[str, Any]) -> dict[str, Any]: filename = payload["image"] source = payload.get("source") or "DCP" processors = payload.get("processors") or [] reference = payload.get("reference") or filename outputs = pipeline.run_postprocessors_for_source( filename, source, processors, params=payload.get("params") or {}, reference_filename=reference, log=log, ) return { "outputs": [pipeline.relpath(path) for path in outputs], "results": pipeline.get_results(filename), } class DehazeRequestHandler(BaseHTTPRequestHandler): server_version = "DehazeWeb/1.0" def log_message(self, fmt: str, *args) -> None: print("[%s] %s" % (self.log_date_time_string(), fmt % args)) def _send(self, status: int, body: bytes, content_type: str = "application/json; charset=utf-8") -> None: self.send_response(status) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def _send_json(self, payload: Any, status: int = HTTPStatus.OK) -> None: self._send(int(status), _json_bytes(payload)) def _send_error_json(self, status: int, message: str) -> None: self._send_json({"error": message}, status) def do_HEAD(self) -> None: parsed = urlparse(self.path) target = STATIC_DIR / "index.html" if parsed.path == "/" else STATIC_DIR / parsed.path.removeprefix("/static/") if parsed.path == "/" or parsed.path.startswith("/static/"): target = target.resolve() static_root = STATIC_DIR.resolve() if static_root not in target.parents and target != static_root: self.send_response(HTTPStatus.FORBIDDEN) self.end_headers() return if target.exists() and target.is_file(): content_type = mimetypes.guess_type(target.name)[0] or "application/octet-stream" if target.suffix == ".html": content_type = "text/html; charset=utf-8" self.send_response(HTTPStatus.OK) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(target.stat().st_size)) self.end_headers() return self.send_response(HTTPStatus.NOT_FOUND) self.end_headers() def _read_json(self) -> dict[str, Any]: length = int(self.headers.get("Content-Length", "0")) if length <= 0: return {} raw = self.rfile.read(length).decode("utf-8") return json.loads(raw) def do_GET(self) -> None: parsed = urlparse(self.path) path = parsed.path query = parse_qs(parsed.query) try: if path == "/": self._serve_static("index.html") elif path.startswith("/static/"): self._serve_static(path.removeprefix("/static/")) elif path == "/asset": rel = unquote(query.get("path", [""])[0]) self._serve_asset(rel) elif path == "/api/images": self._send_json({"images": pipeline.list_images()}) elif path == "/api/capabilities": self._send_json(pipeline.capabilities()) elif path == "/api/results": filename = query.get("image", [""])[0] self._send_json(pipeline.get_results(filename)) elif path == "/api/job": job_id = query.get("id", [""])[0] with JOB_LOCK: job = JOBS.get(job_id) payload = dict(job) if job else None if payload is None: self._send_error_json(HTTPStatus.NOT_FOUND, "job not found") else: self._send_json(payload) else: self._send_error_json(HTTPStatus.NOT_FOUND, "not found") except Exception as exc: self._send_error_json(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc)) def do_POST(self) -> None: parsed = urlparse(self.path) try: payload = self._read_json() if parsed.path == "/api/run": job = _new_job("dehaze", _run_dehaze_job, payload) self._send_json({"job": job}) elif parsed.path == "/api/postprocess": job = _new_job("postprocess", _run_post_job, payload) self._send_json({"job": job}) else: self._send_error_json(HTTPStatus.NOT_FOUND, "not found") except Exception as exc: self._send_error_json(HTTPStatus.BAD_REQUEST, str(exc)) def _serve_static(self, name: str) -> None: target = (STATIC_DIR / name).resolve() if STATIC_DIR.resolve() not in target.parents and target != STATIC_DIR.resolve(): self._send_error_json(HTTPStatus.FORBIDDEN, "forbidden") return if not target.exists() or not target.is_file(): self._send_error_json(HTTPStatus.NOT_FOUND, "static file not found") return content_type = mimetypes.guess_type(target.name)[0] or "application/octet-stream" if target.suffix == ".html": content_type = "text/html; charset=utf-8" elif target.suffix == ".css": content_type = "text/css; charset=utf-8" elif target.suffix == ".js": content_type = "application/javascript; charset=utf-8" self._send(HTTPStatus.OK, target.read_bytes(), content_type) def _serve_asset(self, rel: str) -> None: target = pipeline.resolve_asset(rel) content_type = mimetypes.guess_type(target.name)[0] or "application/octet-stream" self._send(HTTPStatus.OK, target.read_bytes(), content_type) def main() -> None: parser = argparse.ArgumentParser(description="Local dehaze web console") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=7860) args = parser.parse_args() server = ThreadingHTTPServer((args.host, args.port), DehazeRequestHandler) print(f"Dehaze web console: http://{args.host}:{args.port}") print(f"Images: {pipeline.IMAGE_DIR}") print(f"Results: {pipeline.RESULTS_DIR}") server.serve_forever() if __name__ == "__main__": main()