SignView / templates /index.html
XiaoBai1221's picture
最終修復
cc931c8
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SignView - Neural Sign Recognition Lab</title>
<!-- 字體和圖標 -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;900&family=Rajdhani:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
<!-- 動畫庫 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<style>
:root {
/* 賽博朋克配色系統 */
--neon-blue: #00d4ff;
--neon-cyan: #00ffff;
--neon-green: #00ff88;
--neon-purple: #d400ff;
--neon-pink: #ff0084;
--neon-yellow: #ffdc00;
--neon-red: #ff0055;
/* 背景色彩 */
--bg-primary: #0a0a0a;
--bg-secondary: #1a1a1a;
--bg-tertiary: #2a2a2a;
--bg-glass: rgba(0, 212, 255, 0.05);
--bg-glow: rgba(0, 212, 255, 0.1);
/* 文字顏色 */
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--text-tertiary: #808080;
--text-neon: var(--neon-cyan);
/* 邊框和陰影 */
--border-neon: 1px solid var(--neon-blue);
--border-glow: 0 0 20px rgba(0, 212, 255, 0.5);
--shadow-neon: 0 0 30px rgba(0, 212, 255, 0.3);
--shadow-deep: 0 10px 50px rgba(0, 0, 0, 0.8);
/* 漸變 */
--gradient-neon: linear-gradient(135deg, var(--neon-blue), var(--neon-purple));
--gradient-bg: linear-gradient(45deg, #0a0a0a, #1a1a1a, #0a0a0a);
--gradient-glass: linear-gradient(135deg, rgba(255,255,255,0.1), rgba(255,255,255,0.02));
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Rajdhani', monospace;
background: var(--bg-primary);
color: var(--text-primary);
overflow-x: hidden;
height: 100vh;
position: relative;
}
/* 粒子背景動畫 */
#particle-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.6;
}
/* 掃描線效果 */
.scan-line {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--neon-cyan), transparent);
z-index: 1000;
animation: scan 3s linear infinite;
}
@keyframes scan {
0% { top: 0; opacity: 1; }
50% { opacity: 0.3; }
100% { top: 100vh; opacity: 1; }
}
/* 主容器 */
.cyber-container {
display: grid;
grid-template-columns: 300px 1fr 350px;
grid-template-rows: 80px 1fr;
height: 100vh;
gap: 2px;
background: var(--gradient-bg);
}
/* 頂部標題欄 */
.header-bar {
grid-column: 1 / -1;
background: var(--bg-secondary);
border-bottom: var(--border-neon);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 2rem;
position: relative;
overflow: hidden;
}
.header-bar::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.1), transparent);
animation: sweep 4s ease-in-out infinite;
}
@keyframes sweep {
0% { left: -100%; }
50% { left: 100%; }
100% { left: -100%; }
}
.logo-section {
display: flex;
align-items: center;
gap: 1rem;
}
.logo-icon {
font-size: 2rem;
color: var(--neon-cyan);
filter: drop-shadow(0 0 10px var(--neon-cyan));
animation: pulse-glow 2s ease-in-out infinite alternate;
}
@keyframes pulse-glow {
from { filter: drop-shadow(0 0 10px var(--neon-cyan)); }
to { filter: drop-shadow(0 0 20px var(--neon-cyan)); }
}
.logo-text {
font-family: 'Orbitron', monospace;
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
text-shadow: 0 0 15px var(--neon-blue);
}
.status-indicators {
display: flex;
gap: 1rem;
align-items: center;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--neon-green);
box-shadow: 0 0 15px var(--neon-green);
animation: blink 1.5s ease-in-out infinite;
}
.status-dot.warning {
background: var(--neon-yellow);
box-shadow: 0 0 15px var(--neon-yellow);
}
.status-dot.error {
background: var(--neon-red);
box-shadow: 0 0 15px var(--neon-red);
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* 左側面板 */
.left-panel {
background: var(--bg-secondary);
border-right: var(--border-neon);
padding: 2rem;
overflow-y: auto;
}
.panel-section {
margin-bottom: 2rem;
padding: 1rem;
background: var(--bg-glass);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 8px;
backdrop-filter: blur(10px);
}
.panel-title {
font-family: 'Orbitron', monospace;
font-size: 0.9rem;
color: var(--neon-cyan);
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 2px;
}
.neural-activity {
height: 100px;
background: var(--bg-primary);
border: 1px solid var(--neon-blue);
border-radius: 4px;
position: relative;
overflow: hidden;
}
.wave {
position: absolute;
width: 100%;
height: 2px;
background: var(--neon-cyan);
box-shadow: 0 0 10px var(--neon-cyan);
animation: wave 2s ease-in-out infinite;
}
.wave:nth-child(2) { animation-delay: 0.5s; }
.wave:nth-child(3) { animation-delay: 1s; }
@keyframes wave {
0%, 100% { transform: translateX(-100%); }
50% { transform: translateX(100%); }
}
.system-stats {
list-style: none;
}
.stat-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.stat-label {
color: var(--text-secondary);
}
.stat-value {
color: var(--neon-green);
font-weight: 600;
}
/* 主要工作區域 */
.main-workspace {
background: var(--bg-primary);
padding: 2rem;
display: flex;
flex-direction: column;
position: relative;
}
.workspace-grid {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100%;
gap: 2rem;
}
/* 環境指示器 */
.environment-indicator {
background: var(--bg-glass);
border: 1px solid rgba(255, 220, 0, 0.3);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
text-align: center;
border-left: 4px solid var(--neon-yellow);
backdrop-filter: blur(10px);
}
/* 上傳區域 */
.upload-terminal {
background: var(--bg-secondary);
border: 2px dashed var(--neon-blue);
border-radius: 12px;
padding: 3rem;
text-align: center;
position: relative;
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
}
.upload-terminal::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: var(--gradient-neon);
border-radius: 12px;
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
.upload-terminal:hover::before {
opacity: 1;
animation: border-flow 2s linear infinite;
}
@keyframes border-flow {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
.upload-terminal.dragover {
border-color: var(--neon-green);
box-shadow: var(--border-glow);
}
.upload-icon {
font-size: 4rem;
color: var(--neon-cyan);
margin-bottom: 1rem;
filter: drop-shadow(0 0 20px var(--neon-cyan));
}
.upload-text {
font-family: 'Orbitron', monospace;
font-size: 1.2rem;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.upload-hint {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* 攝像頭區域 */
.camera-terminal {
background: var(--bg-secondary);
border: var(--border-neon);
border-radius: 12px;
padding: 2rem;
position: relative;
}
.video-container {
position: relative;
border-radius: 8px;
overflow: hidden;
margin-bottom: 2rem;
background: #000;
}
#video-display {
width: 100%;
height: auto;
min-height: 300px;
display: block;
border-radius: 8px;
}
.camera-status {
position: absolute;
top: 15px;
right: 15px;
padding: 8px 15px;
border-radius: 20px;
color: white;
font-weight: bold;
font-size: 0.8rem;
font-family: 'Orbitron', monospace;
z-index: 100;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.camera-status.active {
background: rgba(0, 255, 136, 0.2);
border-color: var(--neon-green);
color: var(--neon-green);
box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
}
.camera-status.inactive {
background: rgba(128, 128, 128, 0.2);
border-color: var(--text-tertiary);
color: var(--text-tertiary);
}
/* 視頻預覽區域 */
.video-analyzer {
display: none;
background: var(--bg-secondary);
border: var(--border-neon);
border-radius: 12px;
padding: 2rem;
position: relative;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 48%, var(--neon-cyan) 49%, var(--neon-cyan) 51%, transparent 52%);
opacity: 0;
pointer-events: none;
animation: scan-overlay 3s ease-in-out infinite;
}
@keyframes scan-overlay {
0%, 90%, 100% { opacity: 0; }
5%, 85% { opacity: 0.1; }
}
/* 控制按鈕 */
.cyber-controls {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 2rem;
}
.cyber-btn {
background: var(--bg-secondary);
border: 1px solid var(--neon-blue);
color: var(--text-primary);
padding: 1rem 2rem;
border-radius: 8px;
font-family: 'Orbitron', monospace;
font-weight: 600;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.9rem;
}
.cyber-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.4), transparent);
transition: left 0.5s ease;
}
.cyber-btn:hover::before {
left: 100%;
}
.cyber-btn:hover {
border-color: var(--neon-cyan);
box-shadow: 0 0 25px rgba(0, 212, 255, 0.5);
transform: translateY(-2px);
}
.cyber-btn.primary {
background: var(--gradient-neon);
border-color: var(--neon-cyan);
box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
}
.cyber-btn.danger {
border-color: var(--neon-red);
color: var(--neon-red);
}
.cyber-btn.danger:hover {
border-color: var(--neon-red);
box-shadow: 0 0 25px rgba(255, 0, 85, 0.5);
}
.cyber-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 結果顯示區域 */
.result-panel {
background: var(--bg-glass);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 12px;
padding: 2rem;
margin-top: 1rem;
backdrop-filter: blur(10px);
}
.result-title {
font-family: 'Orbitron', monospace;
color: var(--neon-cyan);
font-size: 1rem;
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 2px;
}
.result-content {
font-size: 1.2rem;
color: var(--text-primary);
padding: 1rem;
background: var(--bg-primary);
border: 1px solid rgba(0, 212, 255, 0.1);
border-radius: 8px;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
}
/* 進度分析器 */
.progress-analyzer {
display: none;
background: var(--bg-secondary);
border: var(--border-neon);
border-radius: 12px;
padding: 2rem;
margin-top: 1rem;
}
.progress-display {
background: var(--bg-primary);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.progress-track {
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
position: relative;
margin-bottom: 1rem;
}
.progress-fill {
height: 100%;
background: var(--gradient-neon);
border-radius: 3px;
width: 0%;
transition: width 0.5s ease;
box-shadow: 0 0 15px var(--neon-cyan);
}
.progress-text {
text-align: center;
color: var(--neon-cyan);
font-family: 'Orbitron', monospace;
font-size: 0.9rem;
}
/* 右側數據面板 */
.data-panel {
background: var(--bg-secondary);
border-left: var(--border-neon);
padding: 2rem;
overflow-y: auto;
}
.data-section {
background: var(--bg-glass);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
backdrop-filter: blur(10px);
}
.data-title {
font-family: 'Orbitron', monospace;
color: var(--neon-cyan);
font-size: 1rem;
margin-bottom: 1rem;
text-transform: uppercase;
letter-spacing: 2px;
}
.result-metric {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: var(--bg-primary);
border: 1px solid rgba(0, 212, 255, 0.1);
border-radius: 6px;
margin-bottom: 0.75rem;
}
.metric-label {
color: var(--text-secondary);
font-size: 0.9rem;
}
.metric-value {
color: var(--neon-green);
font-family: 'Orbitron', monospace;
font-weight: 600;
}
.confidence-meter {
height: 4px;
background: var(--bg-tertiary);
border-radius: 2px;
overflow: hidden;
margin-top: 0.5rem;
}
.confidence-fill {
height: 100%;
background: linear-gradient(90deg, var(--neon-red), var(--neon-yellow), var(--neon-green));
border-radius: 2px;
width: 0%;
transition: width 1s ease;
box-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}
/* 機率條 */
.prob-bar-container {
height: 25px;
background-color: var(--bg-tertiary);
border-radius: 5px;
margin-bottom: 10px;
overflow: hidden;
position: relative;
}
.prob-bar {
height: 100%;
background: var(--gradient-neon);
border-radius: 5px;
transition: width 0.3s ease;
box-shadow: 0 0 15px rgba(0, 212, 255, 0.3);
}
.prob-label {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 0.8rem;
}
/* 通知系統 */
.cyber-notification {
position: fixed;
top: 2rem;
right: 2rem;
background: var(--bg-secondary);
border: var(--border-neon);
border-radius: 8px;
padding: 1rem 1.5rem;
color: var(--text-primary);
box-shadow: var(--shadow-neon);
z-index: 1000;
display: none;
backdrop-filter: blur(10px);
font-family: 'Orbitron', monospace;
font-size: 0.9rem;
}
.cyber-notification.success {
border-color: var(--neon-green);
box-shadow: 0 0 30px rgba(0, 255, 136, 0.3);
}
.cyber-notification.error {
border-color: var(--neon-red);
box-shadow: 0 0 30px rgba(255, 0, 85, 0.3);
}
/* 加載動畫 */
.cyber-loader {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--bg-tertiary);
border-top: 2px solid var(--neon-cyan);
border-radius: 50%;
animation: cyber-spin 1s linear infinite;
margin-right: 0.5rem;
}
@keyframes cyber-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 數據流動畫 */
.data-stream {
position: fixed;
top: 0;
right: 0;
width: 200px;
height: 100vh;
pointer-events: none;
z-index: -1;
}
.stream-line {
position: absolute;
width: 1px;
height: 100px;
background: linear-gradient(to bottom, transparent, var(--neon-cyan), transparent);
animation: stream 4s linear infinite;
}
.stream-line:nth-child(2) { left: 50px; animation-delay: 1s; }
.stream-line:nth-child(3) { left: 100px; animation-delay: 2s; }
.stream-line:nth-child(4) { left: 150px; animation-delay: 3s; }
@keyframes stream {
0% { top: -100px; opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { top: 100vh; opacity: 0; }
}
/* 響應式設計 */
@media (max-width: 1200px) {
.cyber-container {
grid-template-columns: 250px 1fr 300px;
}
}
@media (max-width: 1024px) {
.cyber-container {
grid-template-columns: 1fr;
grid-template-rows: 80px 1fr;
}
.left-panel,
.data-panel {
display: none;
}
.main-workspace {
padding: 1rem;
}
}
@media (max-width: 768px) {
.header-bar {
padding: 0 1rem;
}
.logo-text {
font-size: 1.2rem;
}
.cyber-controls {
flex-direction: column;
gap: 0.5rem;
}
.cyber-btn {
padding: 0.8rem 1.5rem;
}
}
</style>
</head>
<body>
<!-- 粒子背景 -->
<canvas id="particle-canvas"></canvas>
<!-- 掃描線 -->
<div class="scan-line"></div>
<!-- 數據流 -->
<div class="data-stream">
<div class="stream-line"></div>
<div class="stream-line"></div>
<div class="stream-line"></div>
<div class="stream-line"></div>
</div>
<!-- 主容器 -->
<div class="cyber-container">
<!-- 頂部標題欄 -->
<header class="header-bar">
<div class="logo-section">
<i class="fas fa-brain logo-icon"></i>
<div class="logo-text">SignView</div>
<div style="color: var(--text-secondary); font-size: 0.8rem; margin-left: 1rem;">
Neural Sign Recognition Lab
</div>
</div>
<div class="status-indicators">
<div class="status-dot" id="system-status" title="系統狀態"></div>
<div class="status-dot warning" id="ai-status" title="AI模型狀態"></div>
<div class="status-dot" id="network-status" title="網路連接"></div>
<span id="environment-info" style="color: var(--text-secondary); font-size: 0.8rem; margin-left: 1rem;">
檢測中...
</span>
</div>
</header>
<!-- 左側控制面板 -->
<aside class="left-panel">
<div class="panel-section">
<div class="panel-title">神經網路活動</div>
<div class="neural-activity">
<div class="wave"></div>
<div class="wave"></div>
<div class="wave"></div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">系統狀態</div>
<ul class="system-stats">
<li class="stat-item">
<span class="stat-label">處理器使用率</span>
<span class="stat-value" id="cpu-usage">23%</span>
</li>
<li class="stat-item">
<span class="stat-label">記憶體使用</span>
<span class="stat-value" id="memory-usage">2.1GB</span>
</li>
<li class="stat-item">
<span class="stat-label">幀率</span>
<span class="stat-value" id="fps-display">30.0</span>
</li>
<li class="stat-item">
<span class="stat-label">延遲</span>
<span class="stat-value" id="latency-display">12ms</span>
</li>
</ul>
</div>
<div class="panel-section">
<div class="panel-title">AI 模型狀態</div>
<ul class="system-stats">
<li class="stat-item">
<span class="stat-label">模型版本</span>
<span class="stat-value">v3.4.1</span>
</li>
<li class="stat-item">
<span class="stat-label">辨識類別</span>
<span class="stat-value" id="num-classes">4</span>
</li>
<li class="stat-item">
<span class="stat-label">準確率</span>
<span class="stat-value">94.36%</span>
</li>
</ul>
</div>
</aside>
<!-- 主要工作區域 -->
<main class="main-workspace">
<div class="workspace-grid">
<!-- 環境指示器 -->
<div class="environment-indicator">
<strong>🌐 執行環境:</strong><span id="detailed-environment-info">檢測中...</span>
</div>
<!-- 即時攝像頭區域 (本地環境) -->
<div id="camera-section" style="display: none;">
<div class="camera-terminal">
<div class="data-title">即時神經網路分析</div>
<div class="video-container">
<div id="hand-status" class="camera-status inactive">未偵測</div>
<img id="video-display" src="" alt="即時視頻畫面">
<div class="video-overlay"></div>
</div>
<div class="cyber-controls">
<button id="start-btn" class="cyber-btn primary">
<i class="fas fa-play"></i>
啟動分析
</button>
<button id="stop-btn" class="cyber-btn danger" disabled>
<i class="fas fa-stop"></i>
停止分析
</button>
</div>
</div>
</div>
<!-- 影片上傳區域 (雲端環境) -->
<div id="upload-section">
<!-- 上傳終端 -->
<div id="upload-terminal" class="upload-terminal">
<i class="fas fa-upload upload-icon"></i>
<div class="upload-text">拖拽或點擊上傳影片</div>
<div class="upload-hint">支援 MP4, AVI, MOV, WMV | 最大 100MB</div>
<input type="file" id="video-file" accept="video/*" style="display: none;">
</div>
<!-- 視頻分析器 -->
<div id="video-analyzer" class="video-analyzer">
<div class="data-title">視頻神經網路分析器</div>
<div class="video-container">
<video id="preview-video" controls style="width: 100%; border-radius: 8px;"></video>
<div class="video-overlay"></div>
</div>
<div class="cyber-controls">
<button id="process-video-btn" class="cyber-btn primary">
<i class="fas fa-brain"></i>
開始AI分析
</button>
<button id="clear-video-btn" class="cyber-btn danger">
<i class="fas fa-trash"></i>
清除數據
</button>
</div>
</div>
<!-- 進度分析器 -->
<div id="progress-analyzer" class="progress-analyzer">
<div class="data-title">神經網路處理進度</div>
<div class="progress-display">
<div class="progress-track">
<div class="progress-fill"></div>
</div>
<div id="upload-status" class="progress-text">等待初始化...</div>
</div>
</div>
</div>
<!-- 結果顯示區域 -->
<div class="result-panel">
<div class="result-title">辨識結果</div>
<div id="video-word-sequence-display" class="result-content">尚無辨識結果</div>
</div>
<div class="result-panel">
<div class="result-title">AI翻譯結果</div>
<div id="video-sentence-display" class="result-content">等待神經網路處理...</div>
</div>
<!-- 即時辨識結果 (本地環境) -->
<div id="realtime-results" class="result-panel" style="display: none;">
<div class="result-title">即時單詞序列</div>
<div id="word-sequence-display" class="result-content">尚無偵測結果</div>
</div>
<div id="realtime-sentence" class="result-panel" style="display: none;">
<div class="result-title">AI生成句子</div>
<div id="sentence-display" class="result-content">等待手語輸入完成...</div>
</div>
</div>
</main>
<!-- 右側數據面板 -->
<aside class="data-panel">
<div class="data-section">
<div class="data-title">即時監控</div>
<ul class="system-stats">
<li class="stat-item">
<span class="stat-label">處理幀數</span>
<span class="stat-value" id="frame-count">0</span>
</li>
<li class="stat-item">
<span class="stat-label">手部狀態</span>
<span class="stat-value" id="hand-detection-status">未偵測</span>
</li>
<li class="stat-item">
<span class="stat-label">神經網路</span>
<span class="stat-value" id="neural-status">待機</span>
</li>
</ul>
</div>
<div class="data-section" id="results-display">
<div class="data-title">分析結果</div>
<div class="result-metric">
<div class="metric-label">當前預測</div>
<div class="metric-value" id="result-label">未開始</div>
</div>
<div class="result-metric">
<div class="metric-label">信心度</div>
<div>
<div class="metric-value" id="result-confidence">0%</div>
<div class="confidence-meter">
<div class="confidence-fill"></div>
</div>
</div>
</div>
</div>
<div class="data-section">
<div class="data-title">類別機率</div>
<div id="probabilities-container">
<div class="metric-label" style="text-align: center; color: var(--text-tertiary);">
等待分析開始...
</div>
</div>
</div>
<div class="data-section">
<div class="data-title">模型指標</div>
<ul class="system-stats">
<li class="stat-item">
<span class="stat-label">推理時間</span>
<span class="stat-value" id="inference-time">0ms</span>
</li>
<li class="stat-item">
<span class="stat-label">特徵維度</span>
<span class="stat-value">225</span>
</li>
<li class="stat-item">
<span class="stat-label">模型大小</span>
<span class="stat-value">15.2MB</span>
</li>
</ul>
</div>
</aside>
</div>
<!-- 通知系統 -->
<div id="cyber-notification" class="cyber-notification"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 全域變數
let selectedVideoFile = null;
let isProcessing = false;
let animationId = null;
let socket = null;
// 環境檢測
const isHuggingFace = window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface.co');
const environmentInfo = document.getElementById('environment-info');
const detailedEnvironmentInfo = document.getElementById('detailed-environment-info');
const cameraSection = document.getElementById('camera-section');
const uploadSection = document.getElementById('upload-section');
const realtimeResults = document.getElementById('realtime-results');
const realtimeSentence = document.getElementById('realtime-sentence');
const systemStatus = document.getElementById('system-status');
const aiStatus = document.getElementById('ai-status');
const networkStatus = document.getElementById('network-status');
// DOM 元素
const videoDisplay = document.getElementById('video-display');
const startBtn = document.getElementById('start-btn');
const stopBtn = document.getElementById('stop-btn');
const resultLabel = document.getElementById('result-label');
const resultConfidence = document.getElementById('result-confidence');
const probabilitiesContainer = document.getElementById('probabilities-container');
const wordSequenceDisplay = document.getElementById('word-sequence-display');
const sentenceDisplay = document.getElementById('sentence-display');
const handStatus = document.getElementById('hand-status');
const handDetectionStatus = document.getElementById('hand-detection-status');
const neuralStatus = document.getElementById('neural-status');
const frameCountDisplay = document.getElementById('frame-count');
// 影片上傳相關元素
const uploadTerminal = document.getElementById('upload-terminal');
const videoFile = document.getElementById('video-file');
const videoAnalyzer = document.getElementById('video-analyzer');
const previewVideo = document.getElementById('preview-video');
const processVideoBtn = document.getElementById('process-video-btn');
const clearVideoBtn = document.getElementById('clear-video-btn');
const progressAnalyzer = document.getElementById('progress-analyzer');
const videoWordSequenceDisplay = document.getElementById('video-word-sequence-display');
const videoSentenceDisplay = document.getElementById('video-sentence-display');
const notification = document.getElementById('cyber-notification');
// 初始化
initParticleSystem();
setupEnvironment();
setupEventListeners();
animateEntry();
updateSystemStats();
// 環境設置
function setupEnvironment() {
if (isHuggingFace) {
environmentInfo.innerHTML = '☁️ HuggingFace Spaces';
detailedEnvironmentInfo.innerHTML = '☁️ HuggingFace Spaces (雲端) - 使用影片上傳功能';
cameraSection.style.display = 'none';
uploadSection.style.display = 'block';
realtimeResults.style.display = 'none';
realtimeSentence.style.display = 'none';
// 更新狀態指示器
systemStatus.className = 'status-dot';
aiStatus.className = 'status-dot warning';
networkStatus.className = 'status-dot';
} else {
environmentInfo.innerHTML = '💻 本地環境';
detailedEnvironmentInfo.innerHTML = '💻 本地環境 - 支援即時攝像頭辨識';
cameraSection.style.display = 'block';
uploadSection.style.display = 'none';
realtimeResults.style.display = 'block';
realtimeSentence.style.display = 'block';
// 初始化WebSocket
socket = io();
setupWebSocketEvents();
}
}
// 粒子系統
function initParticleSystem() {
const canvas = document.getElementById('particle-canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
const particleCount = 80;
class Particle {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 0.3;
this.vy = (Math.random() - 0.5) * 0.3;
this.size = Math.random() * 2 + 1;
this.opacity = Math.random() * 0.5 + 0.2;
}
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
}
draw() {
ctx.globalAlpha = this.opacity;
ctx.fillStyle = '#00d4ff';
ctx.shadowBlur = 10;
ctx.shadowColor = '#00d4ff';
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
// 初始化粒子
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle());
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(particle => {
particle.update();
particle.draw();
});
// 繪製連接線
ctx.globalAlpha = 0.1;
ctx.strokeStyle = '#00d4ff';
ctx.lineWidth = 1;
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
animationId = requestAnimationFrame(animate);
}
animate();
// 視窗調整
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});
}
// WebSocket 事件設置
function setupWebSocketEvents() {
if (!socket) return;
socket.on('connect', function() {
console.log('已連接到神經網路服務');
showNotification('神經網路連接已建立', 'success');
networkStatus.className = 'status-dot';
});
socket.on('disconnect', function() {
console.log('神經網路連接已斷開');
showNotification('神經網路連接已斷開', 'error');
networkStatus.className = 'status-dot error';
});
// 接收幀更新
socket.on('update_frame', function(data) {
if (videoDisplay) {
videoDisplay.src = `data:image/jpeg;base64,${data.image}`;
}
updateRealtimeStatus(data.status);
});
}
// 設置事件監聽器
function setupEventListeners() {
// 攝像頭控制 (本地環境)
if (!isHuggingFace && socket) {
startBtn?.addEventListener('click', function() {
socket.emit('start_stream', {}, function(response) {
if (response.status === 'success') {
startBtn.disabled = true;
stopBtn.disabled = false;
resultLabel.textContent = '神經網路分析中...';
resultConfidence.textContent = '信心度: 0%';
neuralStatus.textContent = '分析中';
aiStatus.className = 'status-dot';
showNotification('攝像頭神經網路分析已啟動', 'success');
} else {
showNotification('啟動失敗: ' + (response.message || '未知錯誤'), 'error');
}
});
});
stopBtn?.addEventListener('click', function() {
socket.emit('stop_stream', {}, function(response) {
if (response.status === 'success') {
startBtn.disabled = false;
stopBtn.disabled = true;
resultLabel.textContent = '未開始';
resultConfidence.textContent = '信心度: 0%';
handStatus.textContent = '未偵測';
handStatus.className = 'camera-status inactive';
handDetectionStatus.textContent = '未偵測';
neuralStatus.textContent = '待機';
aiStatus.className = 'status-dot warning';
showNotification('神經網路分析已停止', 'success');
}
});
});
}
// 影片上傳功能 (雲端環境)
if (isHuggingFace) {
// 點擊上傳
uploadTerminal?.addEventListener('click', () => videoFile?.click());
// 檔案選擇
videoFile?.addEventListener('change', function(e) {
if (e.target.files.length > 0) {
handleVideoFile(e.target.files[0]);
}
});
// 拖拽支援
uploadTerminal?.addEventListener('dragover', function(e) {
e.preventDefault();
uploadTerminal.classList.add('dragover');
});
uploadTerminal?.addEventListener('dragleave', function(e) {
e.preventDefault();
uploadTerminal.classList.remove('dragover');
});
uploadTerminal?.addEventListener('drop', function(e) {
e.preventDefault();
uploadTerminal.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleVideoFile(files[0]);
}
});
// 處理影片按鈕
processVideoBtn?.addEventListener('click', function() {
if (!selectedVideoFile) {
showNotification('請先選擇影片檔案!', 'error');
return;
}
uploadVideo(selectedVideoFile);
});
// 清除影片按鈕
clearVideoBtn?.addEventListener('click', function() {
clearVideo();
});
}
}
// 處理影片檔案
function handleVideoFile(file) {
if (!file.type.startsWith('video/')) {
showNotification('請選擇影片檔案!', 'error');
return;
}
if (file.size > 100 * 1024 * 1024) {
showNotification('檔案大小不能超過 100MB', 'error');
return;
}
selectedVideoFile = file;
// 顯示預覽
const url = URL.createObjectURL(file);
if (previewVideo) {
previewVideo.src = url;
}
// 動畫切換到分析器
if (uploadTerminal && videoAnalyzer) {
anime({
targets: uploadTerminal,
opacity: 0,
scale: 0.8,
duration: 500,
complete: () => {
uploadTerminal.style.display = 'none';
videoAnalyzer.style.display = 'block';
anime({
targets: videoAnalyzer,
opacity: [0, 1],
scale: [0.8, 1],
duration: 800,
easing: 'easeOutCubic'
});
}
});
}
showNotification('影片載入成功,神經網路已準備就緒', 'success');
}
// 上傳影片
function uploadVideo(file) {
if (isProcessing) return;
isProcessing = true;
// 顯示進度分析器
if (progressAnalyzer) {
progressAnalyzer.style.display = 'block';
anime({
targets: progressAnalyzer,
opacity: [0, 1],
translateY: [-30, 0],
duration: 600
});
}
// 更新按鈕狀態
if (processVideoBtn) {
processVideoBtn.innerHTML = '<div class="cyber-loader"></div>神經網路運算中...';
processVideoBtn.disabled = true;
}
if (clearVideoBtn) {
clearVideoBtn.disabled = true;
}
// 更新狀態
aiStatus.className = 'status-dot';
neuralStatus.textContent = '深度學習中';
const formData = new FormData();
formData.append('video', file);
// 模擬進度更新
simulateProgress();
fetch('/process_video', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(result => {
displayVideoResult(result);
})
.catch(error => {
console.error('神經網路處理錯誤:', error);
showNotification('神經網路處理失敗: ' + error.message, 'error');
})
.finally(() => {
resetProcessingState();
});
}
// 模擬進度更新
function simulateProgress() {
let progress = 0;
const phases = [
'初始化神經網路...',
'載入預訓練模型...',
'提取關鍵點特徵...',
'深度學習推理...',
'生成預測結果...'
];
let currentPhase = 0;
const interval = setInterval(() => {
if (progress < 90 && isProcessing) {
progress += Math.random() * 15;
if (progress > 90) progress = 90;
// 更新階段
if (progress > (currentPhase + 1) * 18 && currentPhase < phases.length - 1) {
currentPhase++;
}
updateProgress(progress, phases[currentPhase]);
} else {
clearInterval(interval);
}
}, 1200);
}
// 更新進度
function updateProgress(percentage, phase) {
const progressFill = document.querySelector('.progress-fill');
const uploadStatus = document.getElementById('upload-status');
if (progressFill) {
anime({
targets: progressFill,
width: percentage + '%',
duration: 800,
easing: 'easeOutCubic'
});
}
if (uploadStatus) {
uploadStatus.textContent = `${phase} ${Math.round(percentage)}%`;
}
}
// 顯示影片辨識結果
function displayVideoResult(result) {
console.log('收到神經網路分析結果:', result);
if (result.status === 'success') {
// 完成進度
updateProgress(100, '神經網路分析完成!');
// 顯示當前預測 (模型預測的類別)
const predictedLabel = result.predicted_label || '未知';
if (resultLabel) resultLabel.textContent = predictedLabel;
// 顯示信心度
const confidence = result.confidence || 0;
if (resultConfidence) resultConfidence.textContent = `信心度: ${(confidence * 100).toFixed(1)}%`;
// 更新信心度條
const confidenceFill = document.querySelector('.confidence-fill');
if (confidenceFill) {
anime({
targets: confidenceFill,
width: (confidence * 100) + '%',
duration: 1500,
easing: 'easeOutCubic'
});
}
// 更新類別機率顯示
if (result.probabilities && probabilitiesContainer) {
probabilitiesContainer.innerHTML = '';
result.probabilities.forEach(function(item) {
const probContainer = document.createElement('div');
probContainer.className = 'mb-3';
const probLabel = document.createElement('div');
probLabel.className = 'prob-label';
probLabel.innerHTML = `<span>${item.label}</span><span>${(item.probability * 100).toFixed(1)}%</span>`;
const barContainer = document.createElement('div');
barContainer.className = 'prob-bar-container';
const bar = document.createElement('div');
bar.className = 'prob-bar';
bar.style.width = `${item.probability * 100}%`;
barContainer.appendChild(bar);
probContainer.appendChild(probLabel);
probContainer.appendChild(barContainer);
probabilitiesContainer.appendChild(probContainer);
});
}
// 顯示辨識結果 (模型識別的單詞序列)
if (videoWordSequenceDisplay) {
if (result.word_sequence && result.word_sequence.length > 0) {
videoWordSequenceDisplay.textContent = result.word_sequence.join(' ');
} else {
videoWordSequenceDisplay.textContent = '低於辨識閾值';
}
}
// 顯示AI翻譯結果 (GPT生成的句子)
if (videoSentenceDisplay) {
if (result.generated_sentence) {
videoSentenceDisplay.textContent = result.generated_sentence;
} else {
videoSentenceDisplay.textContent = '無法生成翻譯';
}
}
showNotification('神經網路分析完成!', 'success');
} else {
console.error('神經網路分析失敗:', result);
showNotification('神經網路分析失敗: ' + (result.message || result.error || '未知錯誤'), 'error');
// 重置顯示
if (resultLabel) resultLabel.textContent = '分析失敗';
if (resultConfidence) resultConfidence.textContent = '信心度: 0%';
if (videoWordSequenceDisplay) videoWordSequenceDisplay.textContent = '分析失敗';
if (videoSentenceDisplay) videoSentenceDisplay.textContent = '分析失敗';
if (probabilitiesContainer) {
probabilitiesContainer.innerHTML = '<div class="metric-label" style="text-align: center; color: var(--text-tertiary);">分析失敗</div>';
}
}
}
// 重置處理狀態
function resetProcessingState() {
isProcessing = false;
if (processVideoBtn) {
processVideoBtn.innerHTML = '<i class="fas fa-brain"></i> 開始AI分析';
processVideoBtn.disabled = false;
}
if (clearVideoBtn) {
clearVideoBtn.disabled = false;
}
aiStatus.className = 'status-dot warning';
neuralStatus.textContent = '待機';
setTimeout(() => {
if (progressAnalyzer) {
progressAnalyzer.style.display = 'none';
const progressFill = document.querySelector('.progress-fill');
if (progressFill) progressFill.style.width = '0%';
}
}, 3000);
}
// 清除影片
function clearVideo() {
selectedVideoFile = null;
if (videoFile) videoFile.value = '';
if (previewVideo) previewVideo.src = '';
// 隱藏進度和分析器
if (progressAnalyzer) progressAnalyzer.style.display = 'none';
// 動畫切換回上傳終端
if (videoAnalyzer && uploadTerminal) {
anime({
targets: videoAnalyzer,
opacity: 0,
scale: 0.8,
duration: 500,
complete: () => {
videoAnalyzer.style.display = 'none';
uploadTerminal.style.display = 'block';
anime({
targets: uploadTerminal,
opacity: [0, 1],
scale: [0.8, 1],
duration: 800,
easing: 'easeOutCubic'
});
}
});
}
// 重置結果顯示
if (resultLabel) resultLabel.textContent = '未開始';
if (resultConfidence) resultConfidence.textContent = '信心度: 0%';
if (probabilitiesContainer) {
probabilitiesContainer.innerHTML = '<div class="metric-label" style="text-align: center; color: var(--text-tertiary);">等待分析開始...</div>';
}
if (videoWordSequenceDisplay) videoWordSequenceDisplay.textContent = '尚無辨識結果';
if (videoSentenceDisplay) videoSentenceDisplay.textContent = '等待神經網路處理...';
showNotification('數據已清除,神經網路已重置', 'success');
}
// 更新即時狀態 (本地環境)
function updateRealtimeStatus(status) {
if (isHuggingFace) return;
// 更新手部狀態
if (status.hand_present) {
if (handStatus) {
handStatus.textContent = '已偵測到手部';
handStatus.className = 'camera-status active';
}
if (handDetectionStatus) handDetectionStatus.textContent = '已偵測';
} else {
if (handStatus) {
handStatus.textContent = '未偵測到手部';
handStatus.className = 'camera-status inactive';
}
if (handDetectionStatus) handDetectionStatus.textContent = '未偵測';
}
// 更新當前預測結果
if (status.current_prediction) {
if (resultLabel) resultLabel.textContent = status.current_prediction.label;
if (resultConfidence) resultConfidence.textContent = `信心度: ${(status.current_prediction.confidence * 100).toFixed(1)}%`;
// 更新信心度條
const confidenceFill = document.querySelector('.confidence-fill');
if (confidenceFill) {
confidenceFill.style.width = `${status.current_prediction.confidence * 100}%`;
}
}
// 更新機率條
if (status.probabilities && probabilitiesContainer) {
probabilitiesContainer.innerHTML = '';
status.probabilities.forEach(function(item) {
const probContainer = document.createElement('div');
probContainer.className = 'mb-3';
const probLabel = document.createElement('div');
probLabel.className = 'prob-label';
probLabel.innerHTML = `<span>${item.label}</span><span>${(item.probability * 100).toFixed(1)}%</span>`;
const barContainer = document.createElement('div');
barContainer.className = 'prob-bar-container';
const bar = document.createElement('div');
bar.className = 'prob-bar';
bar.style.width = `${item.probability * 100}%`;
barContainer.appendChild(bar);
probContainer.appendChild(probLabel);
probContainer.appendChild(barContainer);
probabilitiesContainer.appendChild(probContainer);
});
}
// 更新單詞序列
if (status.word_sequence && status.word_sequence.length > 0 && wordSequenceDisplay) {
wordSequenceDisplay.textContent = status.word_sequence.join(' ');
} else if (wordSequenceDisplay) {
wordSequenceDisplay.textContent = '尚無偵測結果';
}
// 更新生成的句子
if (status.generated_sentence && status.display_sentence && sentenceDisplay) {
sentenceDisplay.textContent = status.generated_sentence;
} else if (sentenceDisplay && !status.display_sentence) {
sentenceDisplay.textContent = '等待手語輸入完成...';
}
// 更新幀數
if (status.frame_count && frameCountDisplay) {
frameCountDisplay.textContent = status.frame_count;
}
}
// 入場動畫
function animateEntry() {
if (typeof anime === 'undefined') return;
anime.timeline()
.add({
targets: '.header-bar',
opacity: [0, 1],
translateY: [-50, 0],
duration: 1000,
easing: 'easeOutCubic'
})
.add({
targets: '.left-panel',
opacity: [0, 1],
translateX: [-100, 0],
duration: 800,
easing: 'easeOutCubic'
}, '-=500')
.add({
targets: '.main-workspace',
opacity: [0, 1],
scale: [0.9, 1],
duration: 800,
easing: 'easeOutCubic'
}, '-=600')
.add({
targets: '.data-panel',
opacity: [0, 1],
translateX: [100, 0],
duration: 800,
easing: 'easeOutCubic'
}, '-=600');
}
// 更新系統統計
function updateSystemStats() {
setInterval(() => {
if (isProcessing) {
// 處理中的動態數據
const cpuUsage = document.getElementById('cpu-usage');
const memoryUsage = document.getElementById('memory-usage');
const fpsDisplay = document.getElementById('fps-display');
const latencyDisplay = document.getElementById('latency-display');
const inferenceTime = document.getElementById('inference-time');
if (cpuUsage) cpuUsage.textContent = Math.floor(Math.random() * 30 + 40) + '%';
if (memoryUsage) memoryUsage.textContent = (Math.random() * 1.5 + 2.5).toFixed(1) + 'GB';
if (fpsDisplay) fpsDisplay.textContent = (Math.random() * 5 + 25).toFixed(1);
if (latencyDisplay) latencyDisplay.textContent = Math.floor(Math.random() * 20 + 5) + 'ms';
if (inferenceTime) inferenceTime.textContent = Math.floor(Math.random() * 50 + 10) + 'ms';
} else {
// 待機狀態的穩定數據
const cpuUsage = document.getElementById('cpu-usage');
const memoryUsage = document.getElementById('memory-usage');
const fpsDisplay = document.getElementById('fps-display');
const latencyDisplay = document.getElementById('latency-display');
const inferenceTime = document.getElementById('inference-time');
if (cpuUsage) cpuUsage.textContent = Math.floor(Math.random() * 10 + 15) + '%';
if (memoryUsage) memoryUsage.textContent = (Math.random() * 0.5 + 2.0).toFixed(1) + 'GB';
if (fpsDisplay) fpsDisplay.textContent = (Math.random() * 2 + 29).toFixed(1);
if (latencyDisplay) latencyDisplay.textContent = Math.floor(Math.random() * 10 + 8) + 'ms';
if (inferenceTime) inferenceTime.textContent = Math.floor(Math.random() * 20 + 5) + 'ms';
}
}, 2000);
}
// 顯示通知
function showNotification(message, type = 'success', duration = 4000) {
if (!notification) return;
notification.textContent = message;
notification.className = `cyber-notification ${type}`;
notification.style.display = 'block';
if (typeof anime !== 'undefined') {
anime({
targets: notification,
opacity: [0, 1],
translateX: [50, 0],
duration: 500,
easing: 'easeOutCubic'
});
setTimeout(() => {
anime({
targets: notification,
opacity: 0,
translateX: 50,
duration: 500,
complete: () => {
notification.style.display = 'none';
}
});
}, duration);
} else {
setTimeout(() => {
notification.style.display = 'none';
}, duration);
}
}
// 清理資源
window.addEventListener('beforeunload', () => {
if (animationId) {
cancelAnimationFrame(animationId);
}
});
});
</script>
</body>
</html>