统一 Docker Compose 部署,并将客户端拆分为独立子项目。
移除宿主机/conda 启动脚本与 dev 联调工具,后端仅通过 docker compose 部署并默认启用 GPU。模拟客户端与语音确认页迁入 clients/ 下自包含目录,切断对后端源码路径的依赖。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
43
clients/voice-confirmation/README.md
Executable file
43
clients/voice-confirmation/README.md
Executable file
@@ -0,0 +1,43 @@
|
||||
# 手术室耗材 — 语音确认(独立客户端)
|
||||
|
||||
**与监控 API 分宿**的浏览器静态页(`index.html` + `voice_app.js`,Tailwind 使用 CDN)。复制本目录或 `dist/` 即可独立分发。
|
||||
|
||||
- 功能:播放 TTS、并行录音、`POST .../resolve` 上传 WAV;WebSocket 收 `voice_assignment` / `voice_pending` 等。协议见 [`../../docs/客户端手术通信接口说明.md`](../../docs/客户端手术通信接口说明.md)。
|
||||
- 界面:默认 **术间模式**;接口地址与调试信息在 **「高级设置」** 内。
|
||||
- **禁止使用 `file://` 打开**;须通过 HTTP(S) 访问。
|
||||
|
||||
## 启动
|
||||
|
||||
仅需 **Python 3**:
|
||||
|
||||
```bash
|
||||
./start.sh # 默认 0.0.0.0:8080
|
||||
./start.sh 9000 # 指定端口
|
||||
./start.sh --single # 打包单 HTML 后启动 dist/
|
||||
```
|
||||
|
||||
Windows:`start.bat`(用法同上)。
|
||||
|
||||
也可直接使用本目录内的 `start_http.sh` / `start_http.bat`。
|
||||
|
||||
环境变量 **`VOCH_HTTP_BIND`**:默认 `0.0.0.0`(局域网终端访问);仅本机调试可设 `127.0.0.1`。
|
||||
|
||||
浏览器将 **Base URL** 指向监控 API,例如 `http://192.168.1.100:38080`(后端由仓库根目录 `docker compose up -d --build` 启动,详见 [`../../docs/Docker部署.md`](../../docs/Docker部署.md))。
|
||||
|
||||
## 单 HTML 分发(`dist/`)
|
||||
|
||||
```bash
|
||||
python3 scripts/bundle_single_html.py
|
||||
# 或:./start.sh --single
|
||||
```
|
||||
|
||||
生成 `dist/index.html`(内联脚本)及 `dist/start_http.*`。对外只发包 **`dist/`** 即可;该目录被 `.gitignore` 忽略。
|
||||
|
||||
## 生产部署
|
||||
|
||||
- 本目录整体或 **`dist/`** 可交给 Nginx / Caddy / CDN;非 localhost 通常需 **HTTPS** 才能稳定使用麦克风。
|
||||
- **CORS**:后端需 `DEMO_CORS_ENABLED=true`;生产建议收窄 `DEMO_CORS_ORIGINS`。
|
||||
|
||||
## 与 Demo 客户端的关系
|
||||
|
||||
流程联调见 [`../demo-client/`](../demo-client/)(默认 `:38081`)。语音闭环仅在本目录或 `dist/` 完成。
|
||||
359
clients/voice-confirmation/index.html
Executable file
359
clients/voice-confirmation/index.html
Executable file
@@ -0,0 +1,359 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>手术室耗材 — 语音确认</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
clinical: {
|
||||
fg: "#0f172a",
|
||||
muted: "#475569",
|
||||
border: "#e2e8f0",
|
||||
accent: "#1d4ed8",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
@keyframes activityBarWave {
|
||||
0%,
|
||||
100% {
|
||||
transform: scaleY(0.22);
|
||||
}
|
||||
50% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes activityDotPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
.activity-banner-active .activity-wave-bar {
|
||||
display: inline-block;
|
||||
width: 0.45rem;
|
||||
min-height: 2.5rem;
|
||||
height: 2.5rem;
|
||||
transform-origin: bottom center;
|
||||
will-change: transform;
|
||||
border-radius: 0.2rem;
|
||||
animation: activityBarWave 0.52s ease-in-out infinite;
|
||||
}
|
||||
.activity-banner-active .activity-wave-bar:nth-child(1) {
|
||||
animation-delay: 0ms;
|
||||
}
|
||||
.activity-banner-active .activity-wave-bar:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.activity-banner-active .activity-wave-bar:nth-child(3) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.activity-banner-active .activity-wave-bar:nth-child(4) {
|
||||
animation-delay: 0.05s;
|
||||
}
|
||||
.activity-banner-active .activity-wave-bar:nth-child(5) {
|
||||
animation-delay: 0.28s;
|
||||
}
|
||||
.activity-banner-active .activity-wave-bar:nth-child(6) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.activity-banner-active .activity-wave-bar:nth-child(7) {
|
||||
animation-delay: 0.08s;
|
||||
}
|
||||
.activity-dot {
|
||||
animation: activityDotPulse 0.9s ease-in-out infinite;
|
||||
}
|
||||
#advancedPanel summary {
|
||||
list-style: none;
|
||||
}
|
||||
#advancedPanel summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.activity-banner-active .activity-wave-bar {
|
||||
animation: none;
|
||||
transform: scaleY(0.65);
|
||||
}
|
||||
.activity-dot {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-slate-50 text-slate-900 antialiased">
|
||||
<div class="mx-auto max-w-xl md:max-w-2xl px-4 py-6 md:px-8 md:py-10">
|
||||
<header class="mb-8 border-b border-slate-200 pb-6">
|
||||
<h1 id="pageTitle" class="text-xl font-semibold text-slate-900">语音确认</h1>
|
||||
<p id="surgeryIdLine" class="mt-2 hidden text-base text-slate-600">
|
||||
当前手术号:<span id="surgeryIdValue" class="font-medium text-slate-900"></span>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main class="space-y-6">
|
||||
<section class="space-y-4" aria-labelledby="status-heading">
|
||||
<h2 id="status-heading" class="sr-only">当前状态</h2>
|
||||
<p
|
||||
id="statusClinical"
|
||||
class="text-2xl font-semibold leading-snug text-slate-900 md:text-3xl"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
等待下一个确认
|
||||
</p>
|
||||
<p id="queueClinical" class="text-sm leading-relaxed text-slate-600">暂无排队信息</p>
|
||||
|
||||
<div
|
||||
id="recBanner"
|
||||
class="activity-banner mb-3 hidden overflow-hidden rounded-xl border border-red-200 bg-red-50 px-4 py-4 text-center"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label="语音流程状态"
|
||||
>
|
||||
<div
|
||||
id="recWave"
|
||||
class="mb-3 flex h-14 items-end justify-center gap-1.5 sm:gap-2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="activity-wave-bar bg-red-400/90"></span>
|
||||
<span class="activity-wave-bar bg-red-400"></span>
|
||||
<span class="activity-wave-bar bg-red-300"></span>
|
||||
<span class="activity-wave-bar bg-red-200"></span>
|
||||
<span class="activity-wave-bar bg-red-300"></span>
|
||||
<span class="activity-wave-bar bg-red-400"></span>
|
||||
<span class="activity-wave-bar bg-red-400/90"></span>
|
||||
</div>
|
||||
<p id="activityTitle" class="text-sm font-semibold text-red-900">
|
||||
<span id="activityDot" class="activity-dot mr-1 inline-block text-red-600" aria-hidden="true">●</span>
|
||||
正在采集语音
|
||||
</p>
|
||||
<p id="activityDesc" class="mt-1 text-xs leading-relaxed text-red-800">
|
||||
请对着麦克风清楚回答
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="resultClinical"
|
||||
class="rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm leading-relaxed text-slate-700 shadow-sm"
|
||||
role="region"
|
||||
aria-label="最近一次确认结果"
|
||||
>
|
||||
尚无确认结果
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
id="btnRetry"
|
||||
class="min-h-11 rounded-lg bg-blue-700 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
id="btnReplay"
|
||||
class="min-h-11 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-800 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
||||
>
|
||||
重播提示
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<details
|
||||
id="advancedPanel"
|
||||
class="rounded-xl border border-slate-200 bg-white shadow-sm"
|
||||
>
|
||||
<summary
|
||||
class="cursor-pointer select-none px-4 py-3 text-sm font-medium text-slate-800 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-600 rounded-xl"
|
||||
>
|
||||
高级设置(运维 / 调试)
|
||||
</summary>
|
||||
<div class="space-y-5 border-t border-slate-200 px-4 py-4">
|
||||
<p class="text-sm leading-relaxed text-slate-600">
|
||||
以下供信息科或现场调试使用。术间日常使用请保持折叠。页面需用
|
||||
<code class="text-xs text-slate-700">http://</code> 或
|
||||
<code class="text-xs text-slate-700">https://</code> 打开(勿用本地文件协议);语音接口端口常与页面不同(例如页面
|
||||
8080、接口 38080)。
|
||||
</p>
|
||||
<div>
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-slate-500"
|
||||
>原始状态(调试)</span
|
||||
>
|
||||
<p
|
||||
id="statusRaw"
|
||||
class="mt-1 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 font-mono text-xs text-slate-800"
|
||||
>
|
||||
待机
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h3 class="mb-3 text-sm font-medium text-slate-800">连接</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-slate-600" for="baseUrl"
|
||||
>服务端 Base URL</label
|
||||
>
|
||||
<input
|
||||
id="baseUrl"
|
||||
type="url"
|
||||
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder-slate-400 focus:border-blue-600 focus:outline-none focus:ring-1 focus:ring-blue-600"
|
||||
placeholder="http://192.168.1.100:38080"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<p class="mt-1 text-xs leading-relaxed text-slate-500">
|
||||
填监控 API 的局域网地址;若本页是 :8080,Base URL 通常应为同一主机的 :38080。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-slate-600" for="terminalId"
|
||||
>本机语音终端 ID</label
|
||||
>
|
||||
<input
|
||||
id="terminalId"
|
||||
type="text"
|
||||
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900"
|
||||
placeholder="与站点配置中 voice_terminal_id 一致"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<label class="flex cursor-pointer items-start gap-2 text-sm text-slate-700">
|
||||
<input
|
||||
id="autoAssign"
|
||||
type="checkbox"
|
||||
class="mt-1 rounded border-slate-400 text-blue-700"
|
||||
/>
|
||||
<span
|
||||
>启用服务端自动指派(录制开始后通过长连接接收指派,
|
||||
<code class="text-xs text-slate-600">voice_assignment</code>)</span
|
||||
>
|
||||
</label>
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3">
|
||||
<label class="mb-1 block text-xs font-medium text-slate-600" for="manualSurgeryId"
|
||||
>手动手术号(6 位,未开录或未指派时)</label
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<input
|
||||
id="manualSurgeryId"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
class="w-36 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm tabular-nums text-slate-900"
|
||||
placeholder="123456"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
id="btnStartManual"
|
||||
class="rounded-lg bg-blue-700 px-3 py-2 text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
||||
>
|
||||
开始监控(手动)
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
可与自动指派同时使用;若长连接暂无数据,会定时请求队首作为补充。
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-slate-600" for="recordSec"
|
||||
>TTS 播报结束后录制回答的秒数(默认 8)</label
|
||||
>
|
||||
<input
|
||||
id="recordSec"
|
||||
type="number"
|
||||
min="0"
|
||||
max="60"
|
||||
step="0.5"
|
||||
value="8"
|
||||
class="w-32 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm text-slate-600">
|
||||
<input id="dryRun" type="checkbox" class="rounded border-slate-400" />
|
||||
调试:只录音,不上传
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="mb-3 text-sm font-medium text-slate-800">本机操作</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
id="btnStop"
|
||||
disabled
|
||||
class="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-800 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
|
||||
>
|
||||
停止监控(本机)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||||
<h3 class="text-xs font-medium text-slate-500">排队(原始字段)</h3>
|
||||
<p id="queuePositionHint" class="mt-1 text-sm font-medium text-slate-800">无待确认</p>
|
||||
<h3 class="mt-3 text-xs font-medium text-slate-500">本场累积序号</h3>
|
||||
<p id="cumulativeHint" class="mt-1 text-sm font-medium text-slate-800">无待确认</p>
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
<code class="text-slate-600">pending_queue_position</code> /
|
||||
<code class="text-slate-600">pending_queue_length</code>;
|
||||
<code class="text-slate-600">pending_cumulative_ordinal</code>
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||||
<h3 class="text-xs font-medium text-slate-500">最近接口响应(JSON)</h3>
|
||||
<pre
|
||||
id="resolveResult"
|
||||
class="mt-2 max-h-36 overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-slate-700"
|
||||
>—</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<section class="min-h-[180px] rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||||
<h3 class="mb-2 text-xs font-medium text-slate-500">队首待确认(JSON)</h3>
|
||||
<pre
|
||||
id="pendingJson"
|
||||
class="max-h-64 overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-slate-700"
|
||||
>
|
||||
—</pre>
|
||||
</section>
|
||||
<section class="min-h-[180px] rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h3 class="text-xs font-medium text-slate-500">详细日志</h3>
|
||||
<button
|
||||
type="button"
|
||||
id="btnClearLog"
|
||||
class="rounded border border-slate-300 bg-white px-2 py-1 text-[11px] text-slate-700 hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-blue-600"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
<pre
|
||||
id="log"
|
||||
class="max-h-64 overflow-auto whitespace-pre-wrap font-mono text-xs text-slate-600"
|
||||
></pre>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="voice_app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
45
clients/voice-confirmation/scripts/bundle_single_html.py
Executable file
45
clients/voice-confirmation/scripts/bundle_single_html.py
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""将本目录下的 index.html + voice_app.js 打成单文件 dist/index.html。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
index = root / "index.html"
|
||||
app_js = root / "voice_app.js"
|
||||
out_dir = root / "dist"
|
||||
out_file = out_dir / "index.html"
|
||||
if not index.is_file() or not app_js.is_file():
|
||||
raise SystemExit(f"missing {index} or {app_js}")
|
||||
html = index.read_text(encoding="utf-8")
|
||||
js = app_js.read_text(encoding="utf-8")
|
||||
pat = re.compile(
|
||||
r'<script\s+src=["\']voice_app\.js["\']\s+defer\s*>\s*</script>',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
if not pat.search(html):
|
||||
raise SystemExit("index.html must contain <script src=\"voice_app.js\" defer></script>")
|
||||
|
||||
def _repl(_m):
|
||||
return "<script defer>\n" + js + "\n</script>"
|
||||
|
||||
inlined = pat.sub(_repl, html, count=1)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_file.write_text(inlined, encoding="utf-8")
|
||||
for name in ("start_http.sh", "start_http.bat"):
|
||||
src = root / name
|
||||
dst = out_dir / name
|
||||
shutil.copy2(src, dst)
|
||||
sh = out_dir / "start_http.sh"
|
||||
sh.chmod(sh.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
print(f"wrote {out_file} (+ start_http.* in dist/)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
19
clients/voice-confirmation/start.bat
Normal file
19
clients/voice-confirmation/start.bat
Normal file
@@ -0,0 +1,19 @@
|
||||
@echo off
|
||||
REM 语音确认入口;详见 README.md
|
||||
REM start.bat [端口] 默认 start_http.bat
|
||||
REM start.bat --single [端口] 打包 dist 后等价 dist\start_http.bat
|
||||
REM VOCH_HTTP_BIND=127.0.0.1 可仅监听本机;默认 0.0.0.0 供局域网终端访问
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
if /I "%~1"=="--single" (
|
||||
shift
|
||||
set "PORT=%~1"
|
||||
if "%PORT%"=="" set "PORT=8080"
|
||||
if "%VOCH_HTTP_BIND%"=="" set "VOCH_HTTP_BIND=0.0.0.0"
|
||||
python scripts\bundle_single_html.py
|
||||
call dist\start_http.bat %PORT%
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
set "PORT=%~1"
|
||||
if "%PORT%"=="" set "PORT=8080"
|
||||
call start_http.bat %PORT%
|
||||
21
clients/voice-confirmation/start.sh
Executable file
21
clients/voice-confirmation/start.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# 语音确认静态页入口。勿用 file:// 打开(麦克风等行为异常)。
|
||||
# 用法:
|
||||
# ./start.sh [端口] 默认:./start_http.sh
|
||||
# ./start.sh --single [端口] 打包 dist/ 后等价于 dist/start_http.sh
|
||||
# VOCH_HTTP_BIND=127.0.0.1 可仅监听本机;默认 0.0.0.0 供局域网终端访问
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
if [ "${1:-}" = "--single" ]; then
|
||||
shift
|
||||
PORT="${1:-8080}"
|
||||
BIND="${VOCH_HTTP_BIND:-0.0.0.0}"
|
||||
python3 scripts/bundle_single_html.py
|
||||
VOCH_HTTP_BIND="${BIND}" exec bash "${ROOT}/dist/start_http.sh" "$PORT"
|
||||
fi
|
||||
|
||||
PORT="${1:-8080}"
|
||||
BIND="${VOCH_HTTP_BIND:-0.0.0.0}"
|
||||
VOCH_HTTP_BIND="${BIND}" exec bash "${ROOT}/start_http.sh" "$PORT"
|
||||
23
clients/voice-confirmation/start_http.bat
Executable file
23
clients/voice-confirmation/start_http.bat
Executable file
@@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
REM 纯静态语音确认页:必须用 HTTP 打开(勿用 file://)
|
||||
REM 用法: start_http.bat [端口],默认 8080;可选环境变量 VOCH_HTTP_BIND(默认 0.0.0.0)
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
set "PORT=%~1"
|
||||
if "%PORT%"=="" set "PORT=8080"
|
||||
if "%VOCH_HTTP_BIND%"=="" set "VOCH_HTTP_BIND=0.0.0.0"
|
||||
set "LAN_IP="
|
||||
for /f "usebackq delims=" %%I in (`powershell -NoProfile -Command "Get-NetIPAddress -AddressFamily IPv4 ^| Where-Object {$_.IPAddress -notlike '127.*' -and $_.PrefixOrigin -ne 'WellKnown'} ^| Select-Object -First 1 -ExpandProperty IPAddress" 2^>nul`) do set "LAN_IP=%%I"
|
||||
echo 语音确认静态页监听: %VOCH_HTTP_BIND%:%PORT%
|
||||
if "%VOCH_HTTP_BIND%"=="0.0.0.0" (
|
||||
echo 本机访问: http://127.0.0.1:%PORT%/
|
||||
if not "%LAN_IP%"=="" (
|
||||
echo 局域网访问: http://%LAN_IP%:%PORT%/
|
||||
echo 服务端 Base URL 常用: http://%LAN_IP%:38080
|
||||
)
|
||||
) else (
|
||||
echo 访问: http://%VOCH_HTTP_BIND%:%PORT%/
|
||||
)
|
||||
echo Ctrl+C 停止
|
||||
python -m http.server %PORT% --bind %VOCH_HTTP_BIND% --directory "%CD%"
|
||||
exit /b %ERRORLEVEL%
|
||||
35
clients/voice-confirmation/start_http.sh
Executable file
35
clients/voice-confirmation/start_http.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# 纯静态语音确认页:必须用 HTTP(S) 打开(勿用 file://),否则麦克风等行为异常。
|
||||
# 用法: ./start_http.sh [端口],默认 8080;监听地址由环境变量 VOCH_HTTP_BIND 指定,默认 0.0.0.0。
|
||||
set -euo pipefail
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$DIR"
|
||||
PORT="${1:-8080}"
|
||||
BIND="${VOCH_HTTP_BIND:-0.0.0.0}"
|
||||
|
||||
LAN_IP="$(python3 - <<'PY'
|
||||
import socket
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
print(s.getsockname()[0])
|
||||
except OSError:
|
||||
print("")
|
||||
finally:
|
||||
s.close()
|
||||
PY
|
||||
)"
|
||||
|
||||
echo "语音确认静态页监听: ${BIND}:${PORT}"
|
||||
if [[ "${BIND}" == "0.0.0.0" || "${BIND}" == "::" ]]; then
|
||||
echo "本机访问: http://127.0.0.1:${PORT}/"
|
||||
if [[ -n "${LAN_IP}" ]]; then
|
||||
echo "局域网访问: http://${LAN_IP}:${PORT}/"
|
||||
echo "服务端 Base URL 常用: http://${LAN_IP}:38080"
|
||||
fi
|
||||
else
|
||||
echo "访问: http://${BIND}:${PORT}/"
|
||||
fi
|
||||
echo "Ctrl+C 停止"
|
||||
exec python3 -m http.server "$PORT" --bind "$BIND" --directory "$DIR"
|
||||
1480
clients/voice-confirmation/voice_app.js
Executable file
1480
clients/voice-confirmation/voice_app.js
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user