hkjh
This commit is contained in:
251
MPV/pipeline_helper.py
Normal file
251
MPV/pipeline_helper.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""Persistent MPV pipeline helper.
|
||||
|
||||
This process connects to MPV's IPC server, observes a user-data property for
|
||||
pipeline execution requests, runs the pipeline in-process, and posts results
|
||||
back to MPV via user-data properties.
|
||||
|
||||
Why:
|
||||
- Avoid spawning a new Python process for every MPV action.
|
||||
- Enable MPV Lua scripts to trigger any cmdlet pipeline cheaply.
|
||||
|
||||
Protocol (user-data properties):
|
||||
- Request: user-data/medeia-pipeline-request (JSON string)
|
||||
{"id": "...", "pipeline": "...", "seeds": [...]} (seeds optional)
|
||||
- Response: user-data/medeia-pipeline-response (JSON string)
|
||||
{"id": "...", "success": bool, "stdout": "...", "stderr": "...", "error": "..."}
|
||||
- Ready: user-data/medeia-pipeline-ready ("1")
|
||||
|
||||
This helper is intentionally minimal: one request at a time, last-write-wins.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Make repo-local packages importable even when mpv starts us from another cwd.
|
||||
_ROOT = str(_repo_root())
|
||||
if _ROOT not in sys.path:
|
||||
sys.path.insert(0, _ROOT)
|
||||
|
||||
|
||||
from SYS.tasks import connect_ipc # noqa: E402
|
||||
|
||||
|
||||
REQUEST_PROP = "user-data/medeia-pipeline-request"
|
||||
RESPONSE_PROP = "user-data/medeia-pipeline-response"
|
||||
READY_PROP = "user-data/medeia-pipeline-ready"
|
||||
|
||||
OBS_ID_REQUEST = 1001
|
||||
|
||||
|
||||
def _json_line(payload: Dict[str, Any]) -> bytes:
|
||||
return (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
class MPVWire:
|
||||
def __init__(self, ipc_path: str, *, timeout: float = 5.0) -> None:
|
||||
self.ipc_path = ipc_path
|
||||
self.timeout = timeout
|
||||
self._fh: Optional[Any] = None
|
||||
self._req_id = 1
|
||||
|
||||
def connect(self) -> bool:
|
||||
self._fh = connect_ipc(self.ipc_path, timeout=self.timeout)
|
||||
return self._fh is not None
|
||||
|
||||
@property
|
||||
def fh(self):
|
||||
if self._fh is None:
|
||||
raise RuntimeError("Not connected")
|
||||
return self._fh
|
||||
|
||||
def send(self, command: list[Any]) -> int:
|
||||
self._req_id = (self._req_id + 1) % 1000000
|
||||
req_id = self._req_id
|
||||
self.fh.write(_json_line({"command": command, "request_id": req_id}))
|
||||
self.fh.flush()
|
||||
return req_id
|
||||
|
||||
def set_property(self, name: str, value: Any) -> int:
|
||||
return self.send(["set_property", name, value])
|
||||
|
||||
def observe_property(self, obs_id: int, name: str, fmt: str = "string") -> int:
|
||||
# mpv requires an explicit format argument.
|
||||
return self.send(["observe_property", obs_id, name, fmt])
|
||||
|
||||
def read_message(self) -> Optional[Dict[str, Any]]:
|
||||
raw = self.fh.readline()
|
||||
if raw == b"":
|
||||
return {"event": "__eof__"}
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw.decode("utf-8", errors="replace"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _run_pipeline(pipeline_text: str, *, seeds: Any = None) -> Dict[str, Any]:
|
||||
# Import after sys.path fix.
|
||||
from TUI.pipeline_runner import PipelineExecutor # noqa: WPS433
|
||||
|
||||
executor = PipelineExecutor()
|
||||
result = executor.run_pipeline(pipeline_text, seeds=seeds)
|
||||
return {
|
||||
"success": bool(result.success),
|
||||
"stdout": result.stdout or "",
|
||||
"stderr": result.stderr or "",
|
||||
"error": result.error,
|
||||
}
|
||||
|
||||
|
||||
def _parse_request(data: Any) -> Optional[Dict[str, Any]]:
|
||||
if data is None:
|
||||
return None
|
||||
if isinstance(data, str):
|
||||
text = data.strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
obj = json.loads(text)
|
||||
except Exception:
|
||||
return None
|
||||
return obj if isinstance(obj, dict) else None
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
def main(argv: Optional[list[str]] = None) -> int:
|
||||
parser = argparse.ArgumentParser(prog="mpv-pipeline-helper")
|
||||
parser.add_argument("--ipc", required=True, help="mpv --input-ipc-server path")
|
||||
parser.add_argument("--timeout", type=float, default=5.0)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
# Ensure all in-process cmdlets that talk to MPV pick up the exact IPC server
|
||||
# path used by this helper (which comes from the running MPV instance).
|
||||
os.environ["MEDEIA_MPV_IPC"] = str(args.ipc)
|
||||
|
||||
error_log_dir = Path(tempfile.gettempdir())
|
||||
last_error_log = error_log_dir / "medeia-mpv-pipeline-last-error.log"
|
||||
|
||||
def _write_error_log(text: str, *, req_id: str) -> Optional[str]:
|
||||
try:
|
||||
error_log_dir.mkdir(parents=True, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
payload = (text or "").strip()
|
||||
if not payload:
|
||||
return None
|
||||
|
||||
stamped = error_log_dir / f"medeia-mpv-pipeline-error-{req_id}.log"
|
||||
try:
|
||||
stamped.write_text(payload, encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
stamped = None
|
||||
|
||||
try:
|
||||
last_error_log.write_text(payload, encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return str(stamped) if stamped else str(last_error_log)
|
||||
|
||||
wire = MPVWire(args.ipc, timeout=float(args.timeout))
|
||||
if not wire.connect():
|
||||
return 2
|
||||
|
||||
# Mark ready ASAP.
|
||||
try:
|
||||
wire.set_property(READY_PROP, "1")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Observe request property changes.
|
||||
try:
|
||||
wire.observe_property(OBS_ID_REQUEST, REQUEST_PROP, "string")
|
||||
except Exception:
|
||||
return 3
|
||||
|
||||
last_seen_id: Optional[str] = None
|
||||
|
||||
while True:
|
||||
msg = wire.read_message()
|
||||
if msg is None:
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
|
||||
if msg.get("event") == "__eof__":
|
||||
return 0
|
||||
|
||||
if msg.get("event") != "property-change":
|
||||
continue
|
||||
|
||||
if msg.get("id") != OBS_ID_REQUEST:
|
||||
continue
|
||||
|
||||
req = _parse_request(msg.get("data"))
|
||||
if not req:
|
||||
continue
|
||||
|
||||
req_id = str(req.get("id") or "")
|
||||
pipeline_text = str(req.get("pipeline") or "").strip()
|
||||
seeds = req.get("seeds")
|
||||
|
||||
if not req_id or not pipeline_text:
|
||||
continue
|
||||
|
||||
if last_seen_id == req_id:
|
||||
continue
|
||||
last_seen_id = req_id
|
||||
|
||||
try:
|
||||
run = _run_pipeline(pipeline_text, seeds=seeds)
|
||||
resp = {
|
||||
"id": req_id,
|
||||
"success": bool(run.get("success")),
|
||||
"stdout": run.get("stdout", ""),
|
||||
"stderr": run.get("stderr", ""),
|
||||
"error": run.get("error"),
|
||||
}
|
||||
except Exception as exc:
|
||||
resp = {
|
||||
"id": req_id,
|
||||
"success": False,
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"error": f"{type(exc).__name__}: {exc}",
|
||||
}
|
||||
|
||||
if not resp.get("success"):
|
||||
details = ""
|
||||
if resp.get("error"):
|
||||
details += str(resp.get("error"))
|
||||
if resp.get("stderr"):
|
||||
details = (details + "\n" if details else "") + str(resp.get("stderr"))
|
||||
log_path = _write_error_log(details, req_id=req_id)
|
||||
if log_path:
|
||||
resp["log_path"] = log_path
|
||||
|
||||
try:
|
||||
wire.set_property(RESPONSE_PROP, json.dumps(resp, ensure_ascii=False))
|
||||
except Exception:
|
||||
# If posting results fails, there's nothing more useful to do.
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user