feat(canvas): 图片转换插件 - 增加本地媒体转换节点与运行时
This commit is contained in:
134
canvas-agent/python/depth_anything_runtime.py
Normal file
134
canvas-agent/python/depth_anything_runtime.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Small persistent worker for the local Depth Anything V2 Small model.
|
||||
|
||||
The Node Local Runtime owns the process and sends one JSON request per line.
|
||||
Keeping the pipeline alive avoids loading the model for every canvas conversion.
|
||||
The worker is deliberately offline-only: the model must already exist in the
|
||||
configured Hugging Face cache.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
MODEL_ID = os.environ.get("CANVAS_DEPTH_MODEL_ID", "depth-anything/Depth-Anything-V2-Small-hf")
|
||||
MAX_INPUT_BYTES = 12 * 1024 * 1024
|
||||
DATA_URL_PATTERN = re.compile(r"^data:(image/(?:png|jpeg|webp));base64,([A-Za-z0-9+/=_-]+)$")
|
||||
PIPELINE: Any = None
|
||||
|
||||
|
||||
def emit(value: dict[str, Any]) -> None:
|
||||
sys.stdout.write(json.dumps(value, ensure_ascii=False, separators=(",", ":")) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def load_pipeline() -> Any:
|
||||
global PIPELINE
|
||||
if PIPELINE is not None:
|
||||
return PIPELINE
|
||||
|
||||
try:
|
||||
from transformers import pipeline
|
||||
from transformers.utils import logging as transformers_logging
|
||||
|
||||
transformers_logging.set_verbosity_error()
|
||||
# Transformers/tqdm can write progress text while loading weights.
|
||||
# Keep stdout reserved for the JSON-lines protocol.
|
||||
with contextlib.redirect_stdout(sys.stderr):
|
||||
PIPELINE = pipeline(
|
||||
"depth-estimation",
|
||||
model=MODEL_ID,
|
||||
device=-1,
|
||||
local_files_only=True,
|
||||
)
|
||||
except Exception as error: # pragma: no cover - depends on local model files
|
||||
raise RuntimeError("depth_model_missing") from error
|
||||
return PIPELINE
|
||||
|
||||
|
||||
def image_from_data_url(value: Any) -> Image.Image:
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("depth_input_invalid")
|
||||
match = DATA_URL_PATTERN.fullmatch(value)
|
||||
if not match:
|
||||
raise ValueError("depth_input_invalid")
|
||||
try:
|
||||
raw = base64.urlsafe_b64decode(match.group(2) + "=" * (-len(match.group(2)) % 4))
|
||||
except Exception as error:
|
||||
raise ValueError("depth_input_invalid") from error
|
||||
if not raw or len(raw) > MAX_INPUT_BYTES:
|
||||
raise ValueError("depth_input_invalid")
|
||||
try:
|
||||
image = Image.open(io.BytesIO(raw)).convert("RGB")
|
||||
image.load()
|
||||
return image
|
||||
except Exception as error:
|
||||
raise ValueError("depth_input_invalid") from error
|
||||
|
||||
|
||||
def encode_png(image: Image.Image) -> str:
|
||||
output = io.BytesIO()
|
||||
image.save(output, format="PNG", optimize=True)
|
||||
return base64.b64encode(output.getvalue()).decode("ascii")
|
||||
|
||||
|
||||
def handle(request: dict[str, Any]) -> dict[str, Any]:
|
||||
image = image_from_data_url(request.get("dataUrl"))
|
||||
try:
|
||||
result = load_pipeline()(image)
|
||||
depth = result.get("depth") if isinstance(result, dict) else None
|
||||
if not isinstance(depth, Image.Image):
|
||||
raise RuntimeError("depth_inference_failed")
|
||||
return {
|
||||
"ok": True,
|
||||
"requestId": request.get("requestId"),
|
||||
"mimeType": "image/png",
|
||||
"width": depth.width,
|
||||
"height": depth.height,
|
||||
"modelId": MODEL_ID,
|
||||
"device": "cpu",
|
||||
"pngBase64": encode_png(depth),
|
||||
}
|
||||
except RuntimeError:
|
||||
raise
|
||||
except Exception as error: # pragma: no cover - depends on local runtime
|
||||
raise RuntimeError("depth_inference_failed") from error
|
||||
|
||||
|
||||
def main() -> None:
|
||||
os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
|
||||
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
||||
os.environ.setdefault("HF_HUB_OFFLINE", "1")
|
||||
os.environ.setdefault("TRANSFORMERS_OFFLINE", "1")
|
||||
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
request_id: Any = None
|
||||
try:
|
||||
request = json.loads(line)
|
||||
if not isinstance(request, dict):
|
||||
raise ValueError("depth_input_invalid")
|
||||
request_id = request.get("requestId")
|
||||
emit(handle(request))
|
||||
except ValueError as error:
|
||||
emit({"ok": False, "requestId": request_id, "code": str(error) or "depth_input_invalid", "message": "深度转换输入无效"})
|
||||
except RuntimeError as error:
|
||||
code = str(error) or "depth_inference_failed"
|
||||
emit({"ok": False, "requestId": request_id, "code": code, "message": "本机深度模型不可用" if code == "depth_model_missing" else "本机深度推理失败"})
|
||||
except Exception:
|
||||
emit({"ok": False, "requestId": request_id, "code": "depth_inference_failed", "message": "本机深度推理失败"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user