Spaces:
Sleeping
Sleeping
| <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> |