Merge branch 'master' of http://git.zhenyangtang.com.cn/ZYT/zyt
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""Render the real native client with synthetic data for UI design review."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
os.environ.setdefault("DOCTOR_MOTION", "off")
|
||||
|
||||
from PySide6.QtCore import QSettings, QThreadPool
|
||||
from PySide6.QtGui import QImage
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from doctor_workstation.services import DemoDoctorRepository
|
||||
from doctor_workstation.ui import ShellWindow, apply_theme
|
||||
from doctor_workstation.ui.login import LoginWindow
|
||||
|
||||
ROUTES = {
|
||||
"appointments": "挂号列表",
|
||||
"consultations": "问诊列表",
|
||||
"patients": "我的患者",
|
||||
"prescription_library": "我的处方库",
|
||||
"prescriptions": "已开处方",
|
||||
"reception": "接诊台",
|
||||
}
|
||||
|
||||
|
||||
def settle(app: QApplication) -> None:
|
||||
for _ in range(4):
|
||||
QThreadPool.globalInstance().waitForDone(3000)
|
||||
app.processEvents()
|
||||
QTest.qWait(75)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, default=Path("artifacts/client-design"))
|
||||
args = parser.parse_args()
|
||||
output = args.output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
app = QApplication.instance() or QApplication([])
|
||||
apply_theme(app)
|
||||
repo = DemoDoctorRepository()
|
||||
records = []
|
||||
|
||||
def capture(window, name: str, title: str) -> None:
|
||||
settle(app)
|
||||
filename = f"{name}.png"
|
||||
if not window.grab().save(str(output / filename)):
|
||||
raise RuntimeError(f"Could not save {filename}")
|
||||
records.append({
|
||||
"title": title, "file": filename,
|
||||
"width": window.width(), "height": window.height(),
|
||||
})
|
||||
|
||||
# Isolate preview settings from the user's saved account and credentials.
|
||||
settings = QSettings(str(output / "preview-login.ini"), QSettings.Format.IniFormat)
|
||||
settings.clear()
|
||||
login = LoginWindow(repo, settings=settings)
|
||||
login.show()
|
||||
for width, height in ((1280, 800), (860, 640)):
|
||||
login.resize(width, height)
|
||||
capture(login, f"login-{width}", "登录工作站")
|
||||
login.close()
|
||||
|
||||
session = repo.login(repo.DEMO_ACCOUNT, repo.DEMO_PASSWORD)
|
||||
shell = ShellWindow(
|
||||
repo, {"session": session, "demo_mode": True}, permissions=session.permissions,
|
||||
)
|
||||
shell.show()
|
||||
try:
|
||||
for width, height in ((1440, 900), (1024, 640)):
|
||||
shell.resize(width, height)
|
||||
for route, title in ROUTES.items():
|
||||
shell.navigate(route)
|
||||
capture(shell, f"{route}-{width}", title)
|
||||
finally:
|
||||
settle(app)
|
||||
shell.close()
|
||||
|
||||
# Include independently rendered dialog/media fixtures when available.
|
||||
supplementary = [
|
||||
("patient_orders_1366x768.png", "患者业务订单"),
|
||||
("patient_progress_1366x768.png", "患者进度"),
|
||||
("clinical-after/appointment_drawer_1440x900.png", "预约编辑"),
|
||||
("clinical-after/diagnosis_edit_1440x900.png", "诊单编辑"),
|
||||
("clinical-after/diagnosis_edit_1024x640.png", "诊单编辑 · 紧凑窗口"),
|
||||
("clinical-after/diagnosis_index_1440x900.png", "诊单列表"),
|
||||
("assistant-1440.png", "AI 问诊"),
|
||||
("assistant-1080.png", "AI 问诊 · 紧凑窗口"),
|
||||
("../subwindow_exact/prescription_editor_860x900.png", "处方编辑"),
|
||||
("../subwindow_exact/prescription_ai_report_920x760.png", "处方报告"),
|
||||
("video-call-1120x760.png", "视频面诊"),
|
||||
("video-call-440x300.png", "视频面诊 · 小窗"),
|
||||
("video-chat-440x300.png", "视频聊天 · 小窗"),
|
||||
("video-preview-1120x760.png", "视频截图确认"),
|
||||
]
|
||||
for filename, title in supplementary:
|
||||
preview = QImage(str(output / filename))
|
||||
if not preview.isNull():
|
||||
records.append({
|
||||
"title": title, "file": filename,
|
||||
"width": preview.width(), "height": preview.height(),
|
||||
})
|
||||
|
||||
(output / "gallery.json").write_text(
|
||||
json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8",
|
||||
)
|
||||
cards = []
|
||||
for record in records:
|
||||
filename = html.escape(record["file"])
|
||||
title = html.escape(record["title"])
|
||||
cards.append(
|
||||
f'<section><h2>{title} <small>{record["width"]} × {record["height"]}</small>'
|
||||
f'</h2><a href="{filename}"><img src="{filename}" alt="{title}" loading="lazy">'
|
||||
'</a></section>'
|
||||
)
|
||||
(output / "index.html").write_text(
|
||||
'<!doctype html><html lang="zh-CN"><meta charset="utf-8">'
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
||||
'<title>医生工作站 · 客户端设计验收</title><style>'
|
||||
'body{margin:0;padding:32px;background:#F5F7FA;color:#223047;'
|
||||
'font-family:"Microsoft YaHei",sans-serif}main{max-width:1440px;margin:auto}'
|
||||
'h1{font-size:28px}p,small{color:#647287}small{font-weight:400;font-size:14px}'
|
||||
'section{margin:32px 0 48px}h2{font-size:20px;display:flex;gap:20px;align-items:baseline}'
|
||||
'img{width:100%;display:block;border:1px solid #E6EAF0;border-radius:16px}'
|
||||
'a:focus-visible{outline:3px solid #6D8CCB}footer{padding:24px 0}'
|
||||
'</style><main><h1>医生工作站 · 客户端设计验收</h1>'
|
||||
'<p>使用演示数据渲染的真实客户端。点击图片查看原始尺寸。</p>'
|
||||
+ "".join(cards)
|
||||
+ '<footer>主窗口与紧凑窗口均按实际控件排版生成。</footer></main></html>',
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(output / "index.html")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,193 @@
|
||||
"""Explicit installation/download/check CLI; never invoked by a live call.
|
||||
|
||||
Run with --help for examples. With no action flags this only prints help.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import wave
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
APP_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(APP_ROOT / "src"))
|
||||
|
||||
MODEL_REPOS = {
|
||||
size: f"Systran/faster-whisper-{size}"
|
||||
for size in ("tiny", "base", "small", "medium", "large-v3")
|
||||
}
|
||||
RUNTIME_REQUIREMENT = "faster-whisper==1.2.1"
|
||||
TEXT_REQUIREMENT = "opencc-python-reimplemented==0.1.7"
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Install or verify local CPU/int8 speech recognition before making calls.",
|
||||
epilog=(
|
||||
"Example: python scripts/setup_local_transcription.py "
|
||||
"--install-dependencies --download --model-size small --check. "
|
||||
"Offline check: python scripts/setup_local_transcription.py --check. "
|
||||
"A packaged app must ship the runtime; installing into another Python "
|
||||
"does not alter a frozen executable."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-dependencies",
|
||||
action="store_true",
|
||||
help="Explicitly run pip in this Python environment.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--download",
|
||||
action="store_true",
|
||||
help="Explicitly download the selected official model from Hugging Face.",
|
||||
)
|
||||
parser.add_argument("--model-size", choices=tuple(MODEL_REPOS), default="small")
|
||||
parser.add_argument(
|
||||
"--model-path",
|
||||
type=Path,
|
||||
help="Override DOCTOR_ASR_MODEL_PATH or the app data model directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--revision", default="main", help="Official repository revision/commit for model download."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Load the local model and VAD on CPU with networking disabled.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--smoke-wav",
|
||||
type=Path,
|
||||
help="Optional non-sensitive mono 16 kHz PCM16 WAV, at most 12 seconds; recognized text is printed.",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
if not (args.install_dependencies or args.download or args.check or args.smoke_wav):
|
||||
parser.print_help()
|
||||
return args
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
if not (args.install_dependencies or args.download or args.check or args.smoke_wav):
|
||||
return 0
|
||||
if args.install_dependencies:
|
||||
installer = [sys.executable, "-m", "pip", "install"]
|
||||
if importlib.util.find_spec("pip") is None:
|
||||
uv = shutil.which("uv")
|
||||
if not uv:
|
||||
print(
|
||||
"This environment has no pip. Install with uv pip install --python <python> "
|
||||
f"{RUNTIME_REQUIREMENT} {TEXT_REQUIREMENT}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
installer = [uv, "pip", "install", "--python", sys.executable]
|
||||
subprocess.run(
|
||||
[*installer, RUNTIME_REQUIREMENT, TEXT_REQUIREMENT, "platformdirs>=4.3,<5"],
|
||||
check=True,
|
||||
)
|
||||
|
||||
from doctor_workstation.core.transcript_text import simplified_transcript
|
||||
from doctor_workstation.video.local_transcription import (
|
||||
MAX_PCM_BYTES,
|
||||
SAMPLE_RATE,
|
||||
FasterWhisperEngine,
|
||||
LocalTranscriptionError,
|
||||
default_model_path,
|
||||
local_transcription_readiness,
|
||||
)
|
||||
|
||||
path = args.model_path.expanduser().resolve() if args.model_path else default_model_path()
|
||||
try:
|
||||
if args.download:
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
repo = MODEL_REPOS[args.model_size]
|
||||
print(f"Downloading {repo} into {path}")
|
||||
snapshot_download(
|
||||
repo_id=repo,
|
||||
revision=args.revision,
|
||||
local_dir=str(path),
|
||||
allow_patterns=[
|
||||
"config.json",
|
||||
"preprocessor_config.json",
|
||||
"model.bin",
|
||||
"tokenizer.json",
|
||||
"vocabulary.*",
|
||||
"README.md",
|
||||
"LICENSE*",
|
||||
],
|
||||
token=False,
|
||||
)
|
||||
(path / "doctor-asr-install.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"repository": repo,
|
||||
"requestedRevision": args.revision,
|
||||
"runtimeRequirement": RUNTIME_REQUIREMENT,
|
||||
"textRequirement": TEXT_REQUIREMENT,
|
||||
"installedAt": datetime.now(UTC).isoformat(),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
if args.check or args.smoke_wav or args.download:
|
||||
# This affects only the explicit setup/check process. The runtime
|
||||
# already requires complete local files and local_files_only=True.
|
||||
os.environ["HF_HUB_OFFLINE"] = "1"
|
||||
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
|
||||
print(json.dumps(local_transcription_readiness(path), ensure_ascii=False, indent=2))
|
||||
# The hub may already have been imported during --download, so its
|
||||
# offline environment flag alone is not a sufficient verification.
|
||||
# Block sockets in this standalone check process as well.
|
||||
blocked = RuntimeError("Network access is disabled during local ASR verification")
|
||||
with (
|
||||
patch("socket.socket.connect", side_effect=blocked),
|
||||
patch("socket.socket.connect_ex", side_effect=blocked),
|
||||
patch("socket.getaddrinfo", side_effect=blocked),
|
||||
):
|
||||
engine = FasterWhisperEngine(path)
|
||||
print("Local model and VAD loaded successfully (CPU/int8; network blocked).")
|
||||
if args.smoke_wav:
|
||||
with wave.open(str(args.smoke_wav), "rb") as audio:
|
||||
if (
|
||||
audio.getnchannels(),
|
||||
audio.getsampwidth(),
|
||||
audio.getframerate(),
|
||||
audio.getcomptype(),
|
||||
) != (1, 2, SAMPLE_RATE, "NONE"):
|
||||
raise ValueError("Smoke WAV must be mono 16 kHz uncompressed PCM16.")
|
||||
if not 0 < audio.getnframes() <= MAX_PCM_BYTES // 2:
|
||||
raise ValueError(
|
||||
"Smoke WAV must contain between one sample and 12 seconds."
|
||||
)
|
||||
pcm = audio.readframes(audio.getnframes())
|
||||
print(f"Recognized text: {simplified_transcript(engine.transcribe(pcm))}")
|
||||
return 0
|
||||
except LocalTranscriptionError as error:
|
||||
print(f"Local ASR setup failed ({error.code}): {error}", file=sys.stderr)
|
||||
except ImportError:
|
||||
print(
|
||||
"Required packages are missing; run with --install-dependencies first.", file=sys.stderr
|
||||
)
|
||||
except (OSError, ValueError, RuntimeError) as error:
|
||||
print(
|
||||
f"Local ASR setup failed: {type(error).__name__}. Check model files, WAV format, and connectivity for explicit downloads.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user