ver0.1
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the voice confirmation desktop client with PyInstaller (run on target OS)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
spec = root / "voice_client.spec"
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--clean",
|
||||
action="store_true",
|
||||
help="Remove build/ and dist/ before building",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.clean:
|
||||
for name in ("build", "dist"):
|
||||
p = root / name
|
||||
if p.is_dir():
|
||||
shutil.rmtree(p)
|
||||
if not spec.is_file():
|
||||
print(f"Missing {spec}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
cmd = [sys.executable, "-m", "PyInstaller", str(spec), "--noconfirm"]
|
||||
print("Running:", " ".join(cmd))
|
||||
raise SystemExit(subprocess.call(cmd, cwd=root))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -46,7 +46,7 @@ python3 scripts/demo_client/fake_rtsp_from_file.py --port 18554 \
|
||||
- 给该服务容器加 `--add-host=host.docker.internal:host-gateway`(Docker 20.10+),或
|
||||
- 直接把 URL 写成宿主在 **docker0/桥接网** 上可达的局域网 IP(如 `192.168.x.x`),保证从容器内 `curl`/`ffprobe` 能通
|
||||
|
||||
生产/容器环境请使用 **`OR_SITE_CONFIG_JSON_FILE`** 指向完整站点 JSON(含 `video_rtsp_urls` 与 `voice_or_room_bindings`)。**不要**在仅容器可解析的配置里写 `127.0.0.1` 去指宿主机上的 RTSP(`127.0.0.1` 在容器内是容器自己)。
|
||||
生产/容器环境请使用 `**OR_SITE_CONFIG_JSON_FILE`** 指向完整站点 JSON(含 `video_rtsp_urls` 与 `voice_or_room_bindings`)。**不要**在仅容器可解析的配置里写 `127.0.0.1` 去指宿主机上的 RTSP(`127.0.0.1` 在容器内是容器自己)。
|
||||
|
||||
若监控与假 RTSP **都在宿主机同一系统**里直接跑(非容器),则用 `rtsp://127.0.0.1:...` 即可;否则应使用上面「容器连宿主」的写法。
|
||||
|
||||
@@ -64,27 +64,30 @@ Demo 页面「调试:两路视频」中可用 **选择视频** / **拖放**
|
||||
|
||||
在 §4.1 勾选 **「一键联调」** 后,在「调试」里为**路1/路2**各选一段视频,再点 **开始手术**,浏览器会把两路视频 **multipart 上传到监控 API**(`POST /internal/demo/orchestrate-and-start`),由服务进程依次:
|
||||
|
||||
1. 落盘两路视频到临时目录
|
||||
2. 用 Docker 起 MediaMTX、两路 ffmpeg 推 RTSP(与 `fake_rtsp_from_file.py` 等效)
|
||||
3. 把当前假流的 **video_rtsp_urls** 合并写入 `OR_SITE_CONFIG_JSON_FILE`(保留已有 `voice_or_room_bindings`;与开录/拉流同进程,固定本机回环)
|
||||
4. 调用与普通开录相同逻辑
|
||||
1. 落盘两路视频到临时目录
|
||||
2. 用 Docker 起 MediaMTX、两路 ffmpeg 推 RTSP(与 `fake_rtsp_from_file.py` 等效)
|
||||
3. 把当前假流的 **video_rtsp_urls** 合并写入 `OR_SITE_CONFIG_JSON_FILE`(保留已有 `voice_or_room_bindings`;与开录/拉流同进程,固定本机回环)
|
||||
4. 调用与普通开录相同逻辑
|
||||
|
||||
**需同时满足**:
|
||||
|
||||
- `.env` 中 `DEMO_ORCHESTRATOR_ENABLED=true`(并重启 API)
|
||||
- 已设置 `OR_SITE_CONFIG_JSON_FILE` 指向**可写**的站点 JSON;Docker 中请用 **bind-mount** 到容器内同一路径
|
||||
- **运行 `main.py` 的进程**能执行本机 `docker` 与 `ffmpeg`(与手动跑 `fake_rtsp_from_file` 相同)。**仅将 API 放 Docker、且不挂载** ` /var/run/docker.sock` 时,容器内往往无法为你在宿主机起 MediaMTX,此时请继续用手动假流方式。
|
||||
- **运行 `main.py` 的进程**能执行本机 `docker` 与 `ffmpeg`(与手动跑 `fake_rtsp_from_file` 相同)。**仅将 API 放 Docker、且不挂载** `/var/run/docker.sock` 时,容器内往往无法为你在宿主机起 MediaMTX,此时请继续用手动假流方式。
|
||||
|
||||
由于每次解析都会重新读取 `video_rtsp_url_map()`,覆盖 JSON 后**无需重启**主服务即可被下一次开录用到。
|
||||
|
||||
## 运行方式
|
||||
|
||||
```bash
|
||||
# 1) 启动后端(默认 38080)。CORS 中间件在 settings.demo_cors_enabled=True 时自动挂载。
|
||||
# 1) 启动后端(默认 38080)。开发时建议开启热重载(.env: UVICORN_RELOAD=true 与 python main.py 等效),或:
|
||||
# uv run uvicorn main:app --host 0.0.0.0 --port 38080 --reload
|
||||
uv run python main.py
|
||||
|
||||
# 2) 启动 demo 客户端静态服务(默认 127.0.0.1:38081)。
|
||||
# 2) 启动 demo 客户端静态服务(默认 127.0.0.1:38081)
|
||||
python scripts/demo_client/server.py
|
||||
# 浏览器热重载(需 dev 依赖;会生成本目录 labels.json 供静态托管):
|
||||
uv run --group dev python scripts/demo_client/server.py --live
|
||||
# 或指定端口:
|
||||
python scripts/demo_client/server.py -p 9000 --host 0.0.0.0
|
||||
|
||||
@@ -94,6 +97,12 @@ open http://localhost:38081/
|
||||
|
||||
页面顶部的「服务端 Base URL」默认是 `http://localhost:38080`;如果后端部署在其他主机/端口,直接改这里即可。
|
||||
|
||||
### 与「语音确认」页一致的热重载
|
||||
|
||||
- **API**:环境变量 `UVICORN_RELOAD=true` 或 `server_reload=true`(`python -m main` 使用 `app.config.Settings`)。
|
||||
- **本 Demo**:`--live`(livereload,监视本目录与 `app/resources/consumable_classifier_labels.yaml`)。
|
||||
- **web/voice-confirmation**:`./start_voice_confirmation_web.sh` 或 [web/voice-confirmation/README.md](../../web/voice-confirmation/README.md)。
|
||||
|
||||
## 页面包含什么
|
||||
|
||||
- `GET /health` 连通性检查
|
||||
@@ -130,3 +139,4 @@ open http://localhost:38081/
|
||||
|
||||
- `DEMO_CORS_ENABLED`(默认 `True`,生产请在 `.env` 里置 `false`)
|
||||
- `DEMO_CORS_ORIGINS`(默认 `*`,可写 `http://my-host:38081,https://or-demo.example.com`)
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
- Exposes `GET /labels.json`, which parses the repo's
|
||||
`app/resources/consumable_classifier_labels.yaml` and returns its label
|
||||
list so the page can prefill the candidate-consumables input.
|
||||
- ``--live`` 使用 dev 依赖 ``livereload``:修改本目录下 HTML/JS/CSS
|
||||
或更新 ``labels.json``/YAML 后,浏览器自动刷新(需 ``uv run --group dev``)。
|
||||
|
||||
Run:
|
||||
python scripts/demo_client/server.py # 127.0.0.1:38081
|
||||
python scripts/demo_client/server.py -p 9000 # custom port
|
||||
uv run --group dev python scripts/demo_client/server.py --live
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,6 +19,8 @@ import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
@@ -24,6 +29,8 @@ from typing import Any
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
REPO_ROOT = SCRIPT_DIR.parents[1]
|
||||
LABELS_YAML = REPO_ROOT / "app" / "resources" / "consumable_classifier_labels.yaml"
|
||||
# ``--live`` 模式写入静态 `labels.json`,与 livereload 兼容(勿提交,见 .gitignore)
|
||||
LABELS_JSON_ARTIFACT = SCRIPT_DIR / "labels.json"
|
||||
|
||||
|
||||
def _load_labels_with_pyyaml(path: Path) -> list[str] | None:
|
||||
@@ -85,6 +92,66 @@ def load_labels() -> list[str]:
|
||||
return labels
|
||||
|
||||
|
||||
def write_labels_json_artifact() -> None:
|
||||
body = json.dumps({"labels": load_labels()}, ensure_ascii=False) + "\n"
|
||||
LABELS_JSON_ARTIFACT.write_text(body, encoding="utf-8")
|
||||
|
||||
|
||||
def _spawn_labels_yaml_poll() -> None:
|
||||
p = LABELS_YAML
|
||||
state: list[float] = [0.0]
|
||||
if p.is_file():
|
||||
state[0] = p.stat().st_mtime
|
||||
|
||||
def loop() -> None:
|
||||
while True:
|
||||
time.sleep(1.0)
|
||||
if not p.is_file():
|
||||
continue
|
||||
m = p.stat().st_mtime
|
||||
if m > state[0]:
|
||||
state[0] = m
|
||||
try:
|
||||
write_labels_json_artifact()
|
||||
except OSError as e:
|
||||
print(
|
||||
f"[demo-client] labels.json 写入失败: {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
threading.Thread(target=loop, daemon=True, name="DemoClientLabelsYamlWatch").start()
|
||||
|
||||
|
||||
def run_livereload(host: str, port: int) -> None:
|
||||
try:
|
||||
from livereload import Server
|
||||
except ImportError as exc: # pragma: no cover
|
||||
print(
|
||||
"``--live`` 需要 dev 依赖。请执行: uv sync --group dev",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
write_labels_json_artifact()
|
||||
_spawn_labels_yaml_poll()
|
||||
server = Server()
|
||||
server.watch(str(SCRIPT_DIR))
|
||||
if LABELS_YAML.is_file():
|
||||
server.watch(str(LABELS_YAML))
|
||||
url = f"http://{host}:{port}/"
|
||||
print(f"Demo client (livereload) at {url}")
|
||||
print(f" static dir : {SCRIPT_DIR}")
|
||||
print(f" labels yaml: {LABELS_YAML} -> {LABELS_JSON_ARTIFACT.name}")
|
||||
print("Press Ctrl+C to stop.")
|
||||
server.serve(
|
||||
root=str(SCRIPT_DIR),
|
||||
host=host,
|
||||
port=port,
|
||||
open_url=False,
|
||||
debug=False,
|
||||
)
|
||||
|
||||
|
||||
class DemoHandler(SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, directory=str(SCRIPT_DIR), **kwargs)
|
||||
@@ -110,14 +177,9 @@ class DemoHandler(SimpleHTTPRequestHandler):
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Operation room demo client server")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("-p", "--port", type=int, default=38081)
|
||||
args = parser.parse_args()
|
||||
|
||||
server = ThreadingHTTPServer((args.host, args.port), DemoHandler)
|
||||
url = f"http://{args.host}:{args.port}/"
|
||||
def run_plain(host: str, port: int) -> None:
|
||||
server = ThreadingHTTPServer((host, port), DemoHandler)
|
||||
url = f"http://{host}:{port}/"
|
||||
print(f"Demo client serving at {url}")
|
||||
print(f" static dir : {SCRIPT_DIR}")
|
||||
print(f" labels yaml: {LABELS_YAML}")
|
||||
@@ -130,5 +192,21 @@ def main() -> None:
|
||||
server.server_close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Operation room demo client server")
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("-p", "--port", type=int, default=38081)
|
||||
parser.add_argument(
|
||||
"--live",
|
||||
action="store_true",
|
||||
help="浏览器热重载(需 uv sync --group dev;会生成同目录 labels.json)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if args.live:
|
||||
run_livereload(args.host, args.port)
|
||||
else:
|
||||
run_plain(args.host, args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
70
scripts/dev_static_livereload.py
Normal file
70
scripts/dev_static_livereload.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""以 livereload 提供静态资源目录:保存 HTML/JS/CSS 时浏览器自动刷新。
|
||||
|
||||
需 dev 依赖:``uv sync --group dev``(``livereload``)。
|
||||
|
||||
用法:
|
||||
uv run --group dev python scripts/dev_static_livereload.py --root web/voice-confirmation
|
||||
uv run --group dev python scripts/dev_static_livereload.py --root web/voice-confirmation -p 8080 --host 127.0.0.1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
from livereload import Server
|
||||
except ImportError as exc: # pragma: no cover
|
||||
print(
|
||||
"需要安装 dev 依赖: uv sync --group dev (需含 livereload)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
p = argparse.ArgumentParser(description="Static site + browser live reload (livereload)")
|
||||
p.add_argument(
|
||||
"--root",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="要托管的目录(如 web/voice-confirmation)",
|
||||
)
|
||||
p.add_argument("--host", default="127.0.0.1")
|
||||
p.add_argument("-p", "--port", type=int, default=8080)
|
||||
p.add_argument(
|
||||
"--extra-watch",
|
||||
type=Path,
|
||||
action="append",
|
||||
default=[],
|
||||
help="额外监视路径(可重复),变更时触发自动刷新",
|
||||
)
|
||||
args = p.parse_args()
|
||||
root: Path = args.root.resolve()
|
||||
if not root.is_dir():
|
||||
print(f"不是目录: {root}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
|
||||
server = Server()
|
||||
server.watch(str(root))
|
||||
for w in args.extra_watch:
|
||||
wp = w.resolve()
|
||||
if wp.exists():
|
||||
server.watch(str(wp if wp.is_file() else wp))
|
||||
|
||||
url = f"http://{args.host}:{args.port}/"
|
||||
print(f"Livereload 静态根目录: {root}")
|
||||
print(f"访问: {url} (编辑目录内文件后应自动刷新浏览器;首次请手动打开)")
|
||||
print("按 Ctrl+C 停止。")
|
||||
server.serve(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
root=str(root),
|
||||
open_url=False,
|
||||
debug=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user