This commit is contained in:
gr
2026-10-30 13:43:29 +08:00
144 changed files with 16467 additions and 2457 deletions
+143
View File
@@ -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()
+193
View File
@@ -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())