Init: 导入NaviGlassServer源码

This commit is contained in:
Kevin Wong
2025-12-31 15:42:30 +08:00
parent 5baf812ed3
commit 2b6dd49a59
233 changed files with 20236 additions and 178 deletions

715
templates/index.html Normal file
View File

@@ -0,0 +1,715 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>NaviGlass 导盲系统可视化</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0b0f14;
--card: #121821;
--text: #e6edf3;
--muted: #9fb0c3;
--ok: #7ee787;
--err: #ff8080;
--warn: #fbbf24;
--line: #1f2937;
--panel: rgba(18, 24, 33, .75);
--primary: #3b82f6;
--primary-glow: rgba(59, 130, 246, 0.4);
}
* {
box-sizing: border-box
}
body {
margin: 0;
background: linear-gradient(180deg, #0b0f14, #0b0f14 60%, #0e1621);
color: var(--text);
font: 16px/1.6 system-ui, -apple-system, Segoe UI, Roboto, "Noto Sans", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial;
}
/* 两栏布局 - Day 20 修复:防止滚动条 */
html,
body {
height: 100%;
overflow: hidden;
}
.app {
display: grid;
grid-template-columns: 1fr 700px;
gap: 16px;
height: 100vh;
padding: 16px;
box-sizing: border-box;
overflow: hidden;
}
.stage {
position: relative;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--card);
/* Day 20 修复: 统一使用 --card 背景色 */
box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
overflow: hidden;
}
.canvas-wrap,
#canvas {
position: absolute;
inset: 1px;
/* Day 20 修复: 留出边框空间 */
width: calc(100% - 2px);
height: calc(100% - 2px);
display: block;
background: #0a1017;
/* 视频区域保持深色 */
border-radius: 13px;
/* 比外框小 1px */
z-index: 10
}
/* 右上角:参数 + 日志 */
.tri-panels {
position: absolute;
right: 12px;
top: 12px;
z-index: 30;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
max-width: min(720px, 60vw);
}
.box {
border: 2px dashed rgba(255, 255, 255, .35);
border-radius: 12px;
padding: 10px;
background: var(--panel);
backdrop-filter: blur(8px) saturate(130%);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06);
}
.box h4 {
margin: 0 0 6px 0;
font-size: 12px;
color: #ffd769
}
.kv {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px dashed rgba(255, 255, 255, .15);
padding-top: 6px;
margin-top: 6px;
color: var(--muted);
font-size: 12px
}
/* 左上角 IMU 浮窗 */
.imu-float {
position: absolute;
left: 12px;
top: 12px;
z-index: 35;
width: 600px;
/* 恢复合理尺寸 */
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 12px;
background: rgba(18, 24, 33, 0.9);
backdrop-filter: blur(12px) saturate(140%);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
padding: 12px;
transition: all 0.3s ease;
}
.imu-float:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
/* Day 20: 优化折叠状态 - 只显示标题和按钮 */
.imu-float.collapsed {
width: 180px;
height: 40px;
padding: 8px 12px;
overflow: hidden;
}
.imu-float.collapsed .imu-header {
margin: 0;
font-size: 12px;
}
.imu-float.collapsed .imu-row,
.imu-float.collapsed #imu_top_status {
display: none !important;
}
.imu-toggle {
position: absolute;
top: 4px;
right: 8px;
z-index: 10;
width: 32px;
height: 32px;
border-radius: 8px;
background: rgba(255, 255, 255, .1);
border: none;
cursor: pointer;
color: var(--muted);
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.imu-toggle:hover {
background: rgba(255, 255, 255, .2);
color: var(--text);
}
.imu-row {
display: grid;
grid-template-columns: 1.5fr 1fr;
/* 左侧 3D 模型稍大 */
gap: 12px;
align-items: stretch;
min-height: 200px;
}
#imu_view {
position: relative;
min-height: 200px;
background: #0a1017;
border-radius: 10px;
}
#imu_hud {
border-radius: 10px;
background: rgba(18, 24, 33, 0.8);
border: none;
padding: 8px;
overflow: visible;
/* Day 20: 允许内容自然显示 */
min-width: 150px;
/* Day 20: 缩小最小宽度 */
max-height: 300px;
}
#imu_hud::-webkit-scrollbar {
width: 8px;
/* 滚动条的宽度 */
}
#imu_hud::-webkit-scrollbar-thumb {
background-color: #2a6df4;
/* 滑块的颜色 */
border-radius: 4px;
/* 滑块的圆角 */
}
#imu_hud::-webkit-scrollbar-track {
background-color: #111a2e;
/* 滚动条轨道的颜色 */
border-radius: 4px;
/* 滚动条轨道的圆角 */
}
#imu_top_status {
position: absolute;
top: 10px;
right: 10px;
z-index: 2
}
#imu_top_status .badge {
background: rgba(0, 0, 0, .35);
border-color: #2d3b50
}
/* 角标 + 当前指令(角标在浮窗下) */
.badge-tag {
position: absolute;
left: 12px;
top: calc(12px + 300px + 28px);
z-index: 20;
background: linear-gradient(120deg, rgba(47, 134, 255, .9), rgba(26, 88, 255, .9));
color: #fff;
padding: 6px 10px;
font-weight: 700;
font-size: 12px;
border-radius: 999px;
box-shadow: 0 6px 16px rgba(47, 134, 255, .35)
}
.command {
position: absolute;
left: 50%;
bottom: 46px;
transform: translateX(-50%);
z-index: 25;
display: flex;
gap: 10px;
align-items: center;
padding: 12px 18px;
border-radius: 999px;
background: rgba(18, 24, 33, .75);
border: 1px solid rgba(255, 255, 255, .14);
backdrop-filter: blur(8px) saturate(140%);
box-shadow: 0 12px 28px rgba(0, 0, 0, .6)
}
.dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: #2f86ff;
box-shadow: 0 0 16px #2f86ff
}
/* 右侧聊天(左右气泡)- Day 20 修复:移除 height:100vh */
.chat {
display: flex;
flex-direction: column;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--card);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
overflow: hidden;
/* 不再使用 height:100vh让 grid 自动分配高度 */
}
.chat-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid var(--line)
}
.badges {
display: flex;
gap: 8px;
flex-wrap: wrap
}
/* Day 20: 美化状态 badge */
.badge {
font-size: 11px;
font-weight: 500;
padding: 5px 10px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, .1);
color: var(--muted);
background: rgba(255, 255, 255, .05);
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
}
.badge::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
.badge.ok {
color: var(--ok);
border-color: rgba(126, 231, 135, .3);
background: rgba(126, 231, 135, .1);
}
.badge.ok::before {
animation: pulse 2s infinite;
}
.badge.err {
color: var(--err);
border-color: rgba(255, 128, 128, .3);
background: rgba(255, 128, 128, .1);
}
.badge.connecting {
color: var(--warn);
border-color: rgba(251, 191, 36, .3);
background: rgba(251, 191, 36, .1);
}
.badge.connecting::before {
animation: blink 1s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: .6;
transform: scale(1.2);
}
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: .2;
}
}
.chat-list {
flex: 1;
overflow: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
/* 确保 .chat-list 不会被外部内容撑大 */
}
.live {
padding: 12px;
border: 1px dashed #2d3b50;
border-radius: 12px;
background: #0c121a
}
.live h2 {
margin: 0 0 8px 0;
font-size: 14px;
color: var(--muted);
font-weight: 600
}
.partial {
font-size: 20px;
min-height: 2.2em;
letter-spacing: .2px
}
.finals {
padding: 12px;
border: 1px solid #1f2937;
border-radius: 12px;
background: #0c121a;
display: flex;
flex-direction: column;
flex: 1;
/* 使用 flex: 1 而不是 flex-grow: 1 */
min-height: 0;
/* 允许收缩到小于内容高度 */
}
.finals h2 {
margin: 0 0 8px 0;
font-size: 14px;
color: var(--muted);
font-weight: 600
}
.finals ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px
}
.bubble {
max-width: 82%;
padding: 10px 12px;
border-radius: 14px;
border: 1px solid #1f2937
}
.from-bot {
align-self: flex-start;
background: #0d1729
}
.from-me {
align-self: flex-end;
background: #12263a
}
.controls {
display: flex;
gap: 8px
}
button {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
border: none;
color: #fff;
border-radius: 10px;
padding: 8px 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(37, 99, 235, .3);
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, .4);
}
button:active {
transform: translateY(0);
}
.ghost {
background: transparent;
border: 1px solid #304057;
color: #9fb0c3;
box-shadow: none;
}
.ghost:hover {
background: rgba(255, 255, 255, .05);
border-color: #405570;
}
/* 隐藏但保留:校准/滑杆(供 main.js 使用) */
.hidden-controls {
display: none
}
/* Day 20: 改善移动端适配 */
@media (max-width:1100px) {
.app {
grid-template-columns: 1fr;
height: auto;
padding: 8px;
overflow: visible;
}
/* 关键修复:确保视频区域有足够高度 */
.stage {
min-height: 50vh;
height: 50vh;
position: relative;
}
.chat {
height: 50vh;
min-height: 400px;
}
/* 移动端 IMU 浮窗缩小并移到右下角 */
.imu-float {
width: 160px;
left: auto;
right: 8px;
top: auto;
bottom: 8px;
padding: 8px;
}
/* 移动端默认折叠状态 */
.imu-float:not(.expanded) {
width: 160px;
height: 40px;
overflow: hidden;
}
.imu-float:not(.expanded) .imu-row,
.imu-float:not(.expanded) #imu_top_status {
display: none !important;
}
.imu-row {
grid-template-columns: 1fr;
}
#imu_view {
min-height: 200px;
}
}
@media (max-width:600px) {
.badges {
gap: 4px;
}
.badge {
font-size: 10px;
padding: 4px 8px;
}
.controls {
flex-wrap: wrap;
}
button {
padding: 6px 10px;
font-size: 12px;
}
/* 手机端视频区域更大 */
.stage {
min-height: 45vh;
height: 45vh;
}
.chat {
height: 45vh;
min-height: 300px;
}
/* 手机端 IMU 浮窗始终小巧 */
.imu-float {
width: 140px;
right: 6px;
bottom: 6px;
left: auto;
top: auto;
padding: 6px;
}
.imu-float.collapsed,
.imu-float:not(.expanded) {
width: 140px;
height: 36px;
}
}
</style>
</head>
<body>
<div class="app">
<!-- 左侧主舞台 -->
<section class="stage">
<div class="canvas-wrap"><canvas id="canvas"></canvas></div>
<!-- 左上角IMU 浮窗横向Day 20 优化 -->
<div class="imu-float" id="imuFloat">
<button class="imu-toggle" id="imuToggle" title="折叠/展开"></button>
<div class="imu-header" style="font-size:12px;color:#61dafb;margin-bottom:8px;font-weight:600;">📊 IMU 姿态可视化
</div>
<div class="imu-row">
<div id="imu_view"></div>
<div id="imu_hud"><!-- JS 会把“IMU 实时数据面板”插到这里 --></div>
</div>
<div id="imu_top_status" style="display: none;">
<span class="badge">UDP: <code>12345</code></span>
<span class="badge">Browser WS: <code>/ws</code></span>
<span class="badge" id="imu_ws_state">connecting…</span>
</div>
</div>
</section>
<!-- 右侧聊天 -->
<aside class="chat">
<div class="chat-head">
<div class="badges">
<span id="camStatus" class="badge">Camera: connecting…</span>
<span id="asrStatus" class="badge">ASR: connecting…</span>
<span id="fps" class="badge">FPS: --</span>
</div>
<div class="controls">
<button class="ghost" id="btnClear">清空 Final</button>
<button id="btnReconnect">重连</button>
</div>
</div>
<div class="chat-list">
<div class="live">
<h2>流式识别Partial</h2>
<div class="partial" id="partial">(等待音频…)</div>
</div>
<div class="finals">
<h2>最终文本Final</h2>
<ul id="finalList"></ul>
</div>
</div>
</aside>
</div>
<!-- 隐藏但保留:校准/滑杆(供 main.js 使用) -->
<div class="hidden-controls">
<button id="btn_zero"></button><button id="btn_reset"></button><button id="btn_bias_now"></button>
<input id="auto_rezero" type="checkbox" checked /><input id="auto_bias" type="checkbox" checked />
<input id="use_proj" type="checkbox" checked /><input id="freeze_still" type="checkbox" checked />
<select id="medn">
<option>3</option>
<option selected>5</option>
<option>7</option>
</select>
<select id="ang_ema">
<option value="0">0</option>
<option value="0.15" selected>0.15</option>
<option value="0.30">0.30</option>
<option value="0.5">0.5</option>
</select>
<select id="grav_beta">
<option value="0.95">0.95</option>
<option value="0.97">0.97</option>
<option value="0.98" selected>0.98</option>
<option value="0.99">0.99</option>
</select>
<select id="yaw_db">
<option value="0.05">0.05</option>
<option value="0.08" selected>0.08</option>
<option value="0.15">0.15</option>
<option value="0.30">0.30</option>
</select>
<select id="still_w">
<option value="0.4" selected>0.4</option>
<option value="0.6">0.6</option>
<option value="1.0">1.0</option>
</select>
<select id="yaw_leak">
<option value="0">0</option>
<option value="0.1">0.1</option>
<option value="0.2" selected>0.2</option>
<option value="0.5">0.5</option>
</select>
<input id="roll_sl" type="range"><span id="roll_val"></span>
<input id="pitch_sl" type="range"><span id="pitch_val"></span>
<input id="yaw_sl" type="range"><span id="yaw_val"></span>
<input id="gx_sl" type="range"><span id="gx_val"></span>
<input id="gy_sl" type="range"><span id="gy_val"></span>
<input id="gz_sl" type="range"><span id="gz_val"></span>
<input id="ax_sl" type="range"><span id="ax_val"></span>
<input id="ay_sl" type="range"><span id="ay_val"></span>
<input id="az_sl" type="range"><span id="az_val"></span>
</div>
<!-- three.js importmap -->
<script type="importmap">{
"imports": { "three": "https://unpkg.com/three@0.155.0/build/three.module.js" }
}</script>
<!-- 主脚本 -->
<script type="module" src="/static/main.js"></script>
</body>
</html>