XiaoBai1221 commited on
Commit
e731832
·
1 Parent(s): 4958130

Initial commit

Browse files
Files changed (3) hide show
  1. .DS_Store +0 -0
  2. app.py +16 -0
  3. templates/index.html +1581 -398
.DS_Store ADDED
Binary file (6.15 kB). View file
 
app.py CHANGED
@@ -897,10 +897,26 @@ def process_video():
897
  if sender_id != 'unknown':
898
  send_message(sender_id, recognition_result)
899
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
900
  return jsonify({
901
  "status": "success",
902
  "recognition_result": recognition_result,
903
  "confidence": float(confidence),
 
 
904
  "sender_id": sender_id
905
  })
906
  else:
 
897
  if sender_id != 'unknown':
898
  send_message(sender_id, recognition_result)
899
 
900
+ # 解析辨識結果,提供完整的前端所需資訊
901
+ word_sequence = []
902
+ generated_sentence = recognition_result
903
+
904
+ # 嘗試從辨識結果中提取單詞序列(簡單的文字分割)
905
+ if recognition_result and recognition_result != "無法辨識手語內容":
906
+ # 如果結果包含多個詞,可以分割
907
+ potential_words = recognition_result.split()
908
+ if len(potential_words) <= 4: # 假設是單詞序列
909
+ word_sequence = potential_words
910
+ else:
911
+ # 否則視為生成的句子
912
+ word_sequence = [recognition_result.split()[0]] if recognition_result.split() else []
913
+
914
  return jsonify({
915
  "status": "success",
916
  "recognition_result": recognition_result,
917
  "confidence": float(confidence),
918
+ "word_sequence": word_sequence,
919
+ "generated_sentence": generated_sentence,
920
  "sender_id": sender_id
921
  })
922
  else:
templates/index.html CHANGED
@@ -3,307 +3,1060 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>手語辨識系統</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
 
 
 
8
  <script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
 
 
 
 
9
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  body {
11
- font-family: "微軟正黑體", "Microsoft JhengHei", Arial, sans-serif;
12
- background-color: #1a1a1a;
13
- color: #e0e0e0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
- .container {
16
- max-width: 1200px;
17
- padding: 20px;
18
  }
19
- .main-title {
20
- color: #4aa3df;
21
- margin-bottom: 30px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  text-align: center;
23
- font-weight: bold;
24
- font-size: 2.2rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  .video-container {
27
  position: relative;
28
- margin-bottom: 20px;
29
- background-color: #2a2a2a;
30
- border-radius: 10px;
31
  overflow: hidden;
32
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
 
33
  }
 
34
  #video-display {
35
  width: 100%;
36
- border-radius: 10px;
 
37
  display: block;
 
38
  }
39
- .control-panel {
40
- background-color: #2a2a2a;
41
- border-radius: 10px;
42
- padding: 20px;
43
- margin-bottom: 20px;
44
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
 
 
 
 
 
 
 
 
45
  }
46
- .upload-panel {
47
- background-color: #2a2a2a;
48
- border-radius: 10px;
49
- padding: 20px;
50
- margin-bottom: 20px;
51
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
52
  }
53
- .btn-primary {
54
- background-color: #4aa3df;
55
- border: none;
 
 
56
  }
57
- .btn-danger {
58
- background-color: #e74c3c;
59
- border: none;
 
 
 
 
 
 
60
  }
61
- .btn-success {
62
- background-color: #27ae60;
63
- border: none;
 
 
 
 
 
 
 
 
64
  }
65
- .btn-primary:hover {
66
- background-color: #3498db;
 
 
67
  }
68
- .btn-danger:hover {
69
- background-color: #c0392b;
 
 
 
 
 
70
  }
71
- .btn-success:hover {
72
- background-color: #229954;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
- .panel-title {
75
- color: #4aa3df;
76
- border-bottom: 1px solid #3a3a3a;
77
- padding-bottom: 10px;
78
- margin-bottom: 15px;
79
- font-weight: bold;
 
 
 
 
80
  }
81
- .prediction-panel {
82
- background-color: #2a2a2a;
83
- border-radius: 10px;
84
- padding: 20px;
85
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
86
  }
87
- .result-label {
88
- font-size: 1.8rem;
89
- margin-bottom: 10px;
90
- font-weight: bold;
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
- .result-confidence {
93
- font-size: 1.1rem;
94
- color: #aaa;
95
- margin-bottom: 20px;
96
  }
97
- .word-sequence {
98
- background-color: #3a3a3a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  border-radius: 8px;
100
- padding: 15px;
101
- margin-top: 20px;
102
- border-left: 4px solid #4aa3df;
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
- .sentence-result {
105
- background-color: #3a3a3a;
 
106
  border-radius: 8px;
107
- padding: 15px;
108
- margin-top: 20px;
109
- border-left: 4px solid #9b59b6;
110
  }
111
- .status-indicator {
112
- padding: 8px;
113
- border-radius: 5px;
114
- display: inline-block;
115
- margin-bottom: 10px;
116
- font-weight: bold;
 
 
117
  }
118
- .status-active {
119
- background-color: #27ae60;
120
- color: white;
 
 
 
 
 
121
  }
122
- .status-inactive {
123
- background-color: #7f8c8d;
124
- color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  .prob-bar-container {
127
  height: 25px;
128
- background-color: #3a3a3a;
129
  border-radius: 5px;
130
  margin-bottom: 10px;
131
  overflow: hidden;
 
132
  }
 
133
  .prob-bar {
134
  height: 100%;
135
- background-color: #4aa3df;
136
  border-radius: 5px;
137
  transition: width 0.3s ease;
 
138
  }
 
139
  .prob-label {
140
  display: flex;
141
  justify-content: space-between;
142
  margin-bottom: 5px;
 
143
  }
144
- .camera-status {
145
- position: absolute;
146
- top: 15px;
147
- right: 15px;
148
- padding: 5px 10px;
149
- border-radius: 5px;
150
- color: white;
151
- font-weight: bold;
152
- z-index: 100;
153
- }
154
- .footer {
155
- text-align: center;
156
- margin-top: 30px;
157
- color: #7f8c8d;
 
 
158
  font-size: 0.9rem;
159
  }
160
- .environment-indicator {
161
- background-color: #3a3a3a;
162
- border-radius: 8px;
163
- padding: 10px;
164
- margin-bottom: 20px;
165
- text-align: center;
166
- border-left: 4px solid #f39c12;
167
  }
168
- .upload-area {
169
- border: 2px dashed #4aa3df;
170
- border-radius: 10px;
171
- padding: 30px;
172
- text-align: center;
173
- margin-bottom: 20px;
174
- transition: all 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  }
176
- .upload-area:hover {
177
- border-color: #3498db;
178
- background-color: #333;
 
 
 
 
 
 
 
179
  }
180
- .upload-area.dragover {
181
- border-color: #27ae60;
182
- background-color: #2a4d3a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  }
184
- .progress {
185
- background-color: #3a3a3a;
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  }
187
- .progress-bar {
188
- background-color: #4aa3df;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  }
190
  </style>
191
  </head>
192
  <body>
193
- <div class="container">
194
- <h1 class="main-title">🤟 手語辨識系統</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
- <!-- 環境指示器 -->
197
- <div class="environment-indicator">
198
- <strong>🌐 執行環境:</strong><span id="environment-info">檢測中...</span>
199
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
- <div class="row">
202
- <div class="col-lg-8">
 
 
 
 
 
 
203
  <!-- 即時攝像頭區域 (本地環境) -->
204
  <div id="camera-section" style="display: none;">
205
- <div class="video-container">
206
- <div id="hand-status" class="camera-status bg-secondary">未偵測</div>
207
- <img id="video-display" src="" alt="即時視頻畫面">
208
- </div>
209
-
210
- <div class="control-panel">
211
- <h4 class="panel-title">📹 即時攝像頭辨識</h4>
212
- <div class="d-flex justify-content-between">
213
- <button id="start-btn" class="btn btn-primary">開始辨識</button>
214
- <button id="stop-btn" class="btn btn-danger" disabled>停止辨識</button>
 
 
 
 
 
 
 
215
  </div>
216
- </div>
217
-
218
- <div class="word-sequence">
219
- <h4 class="panel-title">單詞序列</h4>
220
- <div id="word-sequence-display" class="fs-5">尚無偵測結果</div>
221
- </div>
222
-
223
- <div class="sentence-result">
224
- <h4 class="panel-title">翻譯結果</h4>
225
- <div id="sentence-display" class="fs-5">等待手語輸入完成...</div>
226
  </div>
227
  </div>
228
 
229
  <!-- 影片上傳區域 (雲端環境) -->
230
  <div id="upload-section">
231
- <div class="upload-panel">
232
- <h4 class="panel-title">📁 影片上傳辨識</h4>
233
- <div class="upload-area" id="upload-area">
234
- <div id="upload-content">
235
- <i class="fas fa-cloud-upload-alt" style="font-size: 3rem; color: #4aa3df; margin-bottom: 15px;"></i>
236
- <p class="mb-3">拖拽影片檔案到此處,或點擊選擇檔案</p>
237
- <input type="file" id="video-file" accept="video/*" style="display: none;">
238
- <button class="btn btn-primary" onclick="document.getElementById('video-file').click()">選擇影片檔案</button>
239
- <p class="mt-2 text-muted">支援格式:MP4, AVI, MOV, WMV</p>
240
- </div>
241
- <div id="upload-progress" style="display: none;">
242
- <div class="progress mb-3">
243
- <div class="progress-bar" role="progressbar" style="width: 0%"></div>
244
- </div>
245
- <p id="upload-status">上傳中...</p>
246
- </div>
247
  </div>
248
 
249
- <div id="video-preview" style="display: none;">
250
- <video id="preview-video" controls style="width: 100%; border-radius: 10px; margin-bottom: 15px;"></video>
251
- <div class="d-flex justify-content-between">
252
- <button id="process-video-btn" class="btn btn-success">🚀 開始辨識</button>
253
- <button id="clear-video-btn" class="btn btn-danger">🗑️ 清除影片</button>
254
- </div>
 
 
 
255
  </div>
256
  </div>
257
 
258
- <!-- 結果顯示區域 (雲端環境) -->
259
- <div class="word-sequence">
260
- <h4 class="panel-title">辨識結果</h4>
261
- <div id="word-sequence-display" class="fs-5">尚無辨識結果</div>
 
 
 
 
 
262
  </div>
263
-
264
- <div class="sentence-result">
265
- <h4 class="panel-title">翻譯結果</h4>
266
- <div id="sentence-display" class="fs-5">等待影片上傳...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
  </div>
269
  </div>
270
 
271
- <div class="col-lg-4">
272
- <div class="prediction-panel">
273
- <h4 class="panel-title">目前預測結果</h4>
274
- <div id="result-label" class="result-label text-center">未開始</div>
275
- <div id="result-confidence" class="result-confidence text-center">信心度: 0%</div>
276
-
277
- <h5 class="mt-4 mb-3">所有類別機率</h5>
278
- <div id="probabilities-container"></div>
279
  </div>
280
  </div>
281
- </div>
282
-
283
- <div class="footer">
284
- <p>© 2023 手語辨識系統 | 使用 Flask + WebSocket + OpenAI GPT</p>
285
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  </div>
287
-
 
 
 
288
  <script>
289
  document.addEventListener('DOMContentLoaded', function() {
 
 
 
 
 
 
290
  // 環境檢測
291
  const isHuggingFace = window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface.co');
292
  const environmentInfo = document.getElementById('environment-info');
 
293
  const cameraSection = document.getElementById('camera-section');
294
  const uploadSection = document.getElementById('upload-section');
 
 
 
 
 
295
 
296
- if (isHuggingFace) {
297
- environmentInfo.innerHTML = '☁️ HuggingFace Spaces (雲端) - 使用影片上傳功能';
298
- cameraSection.style.display = 'none';
299
- uploadSection.style.display = 'block';
300
- } else {
301
- environmentInfo.innerHTML = '💻 本地環境 - 支���即時攝像頭辨識';
302
- cameraSection.style.display = 'block';
303
- uploadSection.style.display = 'none';
304
- }
305
-
306
- // 獲取DOM元素
307
  const videoDisplay = document.getElementById('video-display');
308
  const startBtn = document.getElementById('start-btn');
309
  const stopBtn = document.getElementById('stop-btn');
@@ -313,252 +1066,567 @@
313
  const wordSequenceDisplay = document.getElementById('word-sequence-display');
314
  const sentenceDisplay = document.getElementById('sentence-display');
315
  const handStatus = document.getElementById('hand-status');
 
 
 
316
 
317
  // 影片上傳相關元素
318
- const uploadArea = document.getElementById('upload-area');
319
  const videoFile = document.getElementById('video-file');
320
- const uploadContent = document.getElementById('upload-content');
321
- const uploadProgress = document.getElementById('upload-progress');
322
- const videoPreview = document.getElementById('video-preview');
323
  const previewVideo = document.getElementById('preview-video');
324
  const processVideoBtn = document.getElementById('process-video-btn');
325
  const clearVideoBtn = document.getElementById('clear-video-btn');
 
 
 
 
326
 
327
- // 連接Socket.IO (僅本地環境)
328
- let socket = null;
329
- if (!isHuggingFace) {
330
- socket = io();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  }
332
 
333
- // Socket.IO 連接事件 (僅本地環境)
334
- if (socket) {
335
- socket.on('connect', function() {
336
- console.log('已連接到伺服器');
337
- });
338
 
339
- // 接收幀更新
340
- socket.on('update_frame', function(data) {
341
- // 更新視頻顯示
342
- videoDisplay.src = `data:image/jpeg;base64,${data.image}`;
 
 
 
 
 
 
 
 
 
 
 
343
 
344
- // 更新狀態
345
- updateStatus(data.status);
346
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
- // 開始按鈕點擊事件
349
- startBtn.addEventListener('click', function() {
350
- socket.emit('start_stream', {}, function(response) {
351
- if (response.status === 'success') {
352
- startBtn.disabled = true;
353
- stopBtn.disabled = false;
354
- resultLabel.textContent = '等待偵測...';
355
- resultConfidence.textContent = '信心度: 0%';
356
- } else {
357
- alert('啟動失敗: ' + (response.message || '未知錯誤'));
358
- }
359
- });
360
- });
361
 
362
- // 停止按鈕點擊事件
363
- stopBtn.addEventListener('click', function() {
364
- socket.emit('stop_stream', {}, function(response) {
365
- if (response.status === 'success') {
366
- startBtn.disabled = false;
367
- stopBtn.disabled = true;
368
- resultLabel.textContent = '未開始';
369
- resultConfidence.textContent = '信心度: 0%';
370
- handStatus.textContent = '未偵測';
371
- handStatus.className = 'camera-status bg-secondary';
372
- }
373
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  });
375
  }
376
 
377
- // 影片上傳功能 (雲端環境)
378
- if (isHuggingFace) {
379
- // 拖拽上傳
380
- uploadArea.addEventListener('dragover', function(e) {
381
- e.preventDefault();
382
- uploadArea.classList.add('dragover');
383
- });
384
 
385
- uploadArea.addEventListener('dragleave', function(e) {
386
- e.preventDefault();
387
- uploadArea.classList.remove('dragover');
 
388
  });
389
 
390
- uploadArea.addEventListener('drop', function(e) {
391
- e.preventDefault();
392
- uploadArea.classList.remove('dragover');
393
- const files = e.dataTransfer.files;
394
- if (files.length > 0) {
395
- handleVideoFile(files[0]);
396
- }
397
  });
398
 
399
- // 檔案選擇
400
- videoFile.addEventListener('change', function(e) {
401
- if (e.target.files.length > 0) {
402
- handleVideoFile(e.target.files[0]);
403
  }
 
404
  });
405
-
406
- // 處理影片檔案
407
- function handleVideoFile(file) {
408
- if (!file.type.startsWith('video/')) {
409
- alert('請選擇影片檔案!');
410
- return;
411
- }
412
-
413
- // 顯示預覽
414
- const url = URL.createObjectURL(file);
415
- previewVideo.src = url;
416
- uploadContent.style.display = 'none';
417
- videoPreview.style.display = 'block';
 
 
 
 
 
 
 
 
418
 
419
- // 儲存檔案供後續處理
420
- window.selectedVideoFile = file;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  }
422
 
423
- // 處理影片按鈕
424
- processVideoBtn.addEventListener('click', function() {
425
- if (!window.selectedVideoFile) {
426
- alert('請先選擇影片檔案!');
427
- return;
428
- }
429
-
430
- uploadVideo(window.selectedVideoFile);
431
- });
432
-
433
- // 清除影片按鈕
434
- clearVideoBtn.addEventListener('click', function() {
435
- previewVideo.src = '';
436
- uploadContent.style.display = 'block';
437
- videoPreview.style.display = 'none';
438
- videoFile.value = '';
439
- window.selectedVideoFile = null;
440
 
441
- // 重置結果顯示
442
- resultLabel.textContent = '未開始';
443
- resultConfidence.textContent = '信心度: 0%';
444
- probabilitiesContainer.innerHTML = '';
445
- });
446
-
447
- // 上傳影片函數
448
- function uploadVideo(file) {
449
- const formData = new FormData();
450
- formData.append('video', file);
451
 
452
- // 顯示進度
453
- uploadProgress.style.display = 'block';
454
- processVideoBtn.disabled = true;
 
 
455
 
456
- const xhr = new XMLHttpRequest();
 
 
 
457
 
458
- xhr.upload.addEventListener('progress', function(e) {
459
- if (e.lengthComputable) {
460
- const percentComplete = (e.loaded / e.total) * 100;
461
- document.querySelector('.progress-bar').style.width = percentComplete + '%';
462
- document.getElementById('upload-status').textContent = `上傳中... ${percentComplete.toFixed(1)}%`;
 
463
  }
464
  });
465
 
466
- xhr.addEventListener('load', function() {
467
- if (xhr.status === 200) {
468
- try {
469
- const response = JSON.parse(xhr.responseText);
470
- displayVideoResult(response);
471
- } catch (e) {
472
- console.error('解析回應失敗:', e);
473
- alert('處理回應時發生錯誤!');
474
- }
475
- } else {
476
- try {
477
- const errorResponse = JSON.parse(xhr.responseText);
478
- alert('影片處理失敗: ' + (errorResponse.message || '未知錯誤'));
479
- } catch (e) {
480
- alert('影片處理失敗!HTTP狀態: ' + xhr.status);
481
- }
482
  }
483
- uploadProgress.style.display = 'none';
484
- processVideoBtn.disabled = false;
485
  });
486
 
487
- xhr.addEventListener('error', function() {
488
- console.error('網路錯誤');
489
- alert('網路連接失敗,請檢查網路連接後重試!');
490
- document.getElementById('upload-status').textContent = '網路錯誤';
491
- uploadProgress.style.display = 'none';
492
- processVideoBtn.disabled = false;
493
  });
494
-
495
- xhr.addEventListener('timeout', function() {
496
- console.error('請求超時');
497
- alert('請求超時,請重試!影片可能太大或處理時間過長。');
498
- document.getElementById('upload-status').textContent = '請求超時';
499
- uploadProgress.style.display = 'none';
500
- processVideoBtn.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  });
502
-
503
- xhr.open('POST', '/process_video');
504
- xhr.timeout = 120000; // 設定 2 分鐘超時
505
- xhr.send(formData);
506
  }
507
 
508
- // 顯示影片辨識結果
509
- function displayVideoResult(result) {
510
- console.log('收到辨識結果:', result);
511
-
512
- if (result.status === 'success') {
513
- // 使用後端實際回傳的欄位名稱
514
- resultLabel.textContent = result.recognition_result || '辨識完成';
515
- resultConfidence.textContent = `信心度: ${(result.confidence * 100).toFixed(1)}%`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
 
517
- // 顯示單詞序列
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  if (result.word_sequence && result.word_sequence.length > 0) {
519
- wordSequenceDisplay.textContent = result.word_sequence.join(' ');
 
 
520
  } else {
521
- wordSequenceDisplay.textContent = result.recognition_result || '無單詞序列';
522
  }
523
-
524
- // 顯示生成的句子
 
 
525
  if (result.generated_sentence) {
526
- sentenceDisplay.textContent = result.generated_sentence;
 
 
527
  } else {
528
- sentenceDisplay.textContent = result.recognition_result || '無生成句子';
529
  }
530
-
531
- // 更新狀態顯示
532
- document.getElementById('upload-status').textContent = '辨識完成!';
533
- document.querySelector('.progress-bar').style.width = '100%';
534
-
535
- } else {
536
- console.error('辨識失敗:', result);
537
- alert('影片辨識失敗: ' + (result.message || result.error || '未知錯誤'));
538
- document.getElementById('upload-status').textContent = '辨識失敗';
539
  }
 
 
 
 
 
 
 
 
 
 
 
 
540
  }
541
  }
542
 
543
- // 更新所有狀態顯示
544
- function updateStatus(status) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
545
  // 更新手部狀態
546
  if (status.hand_present) {
547
- handStatus.textContent = '已偵測到手部';
548
- handStatus.className = 'camera-status bg-success';
 
 
 
549
  } else {
550
- handStatus.textContent = '未偵測到手部';
551
- handStatus.className = 'camera-status bg-secondary';
 
 
 
552
  }
553
 
554
  // 更新當前預測結果
555
  if (status.current_prediction) {
556
- resultLabel.textContent = status.current_prediction.label;
557
- resultConfidence.textContent = `信心度: ${(status.current_prediction.confidence * 100).toFixed(1)}%`;
 
 
 
 
 
 
558
  }
559
 
560
  // 更新機率條
561
- if (status.probabilities) {
562
  probabilitiesContainer.innerHTML = '';
563
  status.probabilities.forEach(function(item) {
564
  const probContainer = document.createElement('div');
@@ -583,20 +1651,135 @@
583
  }
584
 
585
  // 更新單詞序列
586
- if (status.word_sequence && status.word_sequence.length > 0) {
587
  wordSequenceDisplay.textContent = status.word_sequence.join(' ');
588
- } else {
589
  wordSequenceDisplay.textContent = '尚無偵測結果';
590
  }
591
 
592
  // 更新生成的句子
593
- if (status.generated_sentence && status.display_sentence) {
594
  sentenceDisplay.textContent = status.generated_sentence;
595
- } else if (!status.display_sentence) {
596
  sentenceDisplay.textContent = '等待手語輸入完成...';
597
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  }
 
 
 
 
 
 
 
599
  });
600
  </script>
601
  </body>
602
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SignView 2.0 - Neural Sign Recognition Lab</title>
7
+
8
+ <!-- 字體和圖標 -->
9
+ <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">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
  <script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
12
+
13
+ <!-- 動畫庫 -->
14
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
15
+
16
  <style>
17
+ :root {
18
+ /* 賽博朋克配色系統 */
19
+ --neon-blue: #00d4ff;
20
+ --neon-cyan: #00ffff;
21
+ --neon-green: #00ff88;
22
+ --neon-purple: #d400ff;
23
+ --neon-pink: #ff0084;
24
+ --neon-yellow: #ffdc00;
25
+ --neon-red: #ff0055;
26
+
27
+ /* 背景色彩 */
28
+ --bg-primary: #0a0a0a;
29
+ --bg-secondary: #1a1a1a;
30
+ --bg-tertiary: #2a2a2a;
31
+ --bg-glass: rgba(0, 212, 255, 0.05);
32
+ --bg-glow: rgba(0, 212, 255, 0.1);
33
+
34
+ /* 文字顏色 */
35
+ --text-primary: #ffffff;
36
+ --text-secondary: #b0b0b0;
37
+ --text-tertiary: #808080;
38
+ --text-neon: var(--neon-cyan);
39
+
40
+ /* 邊框和陰影 */
41
+ --border-neon: 1px solid var(--neon-blue);
42
+ --border-glow: 0 0 20px rgba(0, 212, 255, 0.5);
43
+ --shadow-neon: 0 0 30px rgba(0, 212, 255, 0.3);
44
+ --shadow-deep: 0 10px 50px rgba(0, 0, 0, 0.8);
45
+
46
+ /* 漸變 */
47
+ --gradient-neon: linear-gradient(135deg, var(--neon-blue), var(--neon-purple));
48
+ --gradient-bg: linear-gradient(45deg, #0a0a0a, #1a1a1a, #0a0a0a);
49
+ --gradient-glass: linear-gradient(135deg, rgba(255,255,255,0.1), rgba(255,255,255,0.02));
50
+ }
51
+
52
+ * {
53
+ margin: 0;
54
+ padding: 0;
55
+ box-sizing: border-box;
56
+ }
57
+
58
  body {
59
+ font-family: 'Rajdhani', monospace;
60
+ background: var(--bg-primary);
61
+ color: var(--text-primary);
62
+ overflow-x: hidden;
63
+ height: 100vh;
64
+ position: relative;
65
+ }
66
+
67
+ /* 粒子背景動畫 */
68
+ #particle-canvas {
69
+ position: fixed;
70
+ top: 0;
71
+ left: 0;
72
+ width: 100%;
73
+ height: 100%;
74
+ z-index: -1;
75
+ opacity: 0.6;
76
+ }
77
+
78
+ /* 掃描線效果 */
79
+ .scan-line {
80
+ position: fixed;
81
+ top: 0;
82
+ left: 0;
83
+ width: 100%;
84
+ height: 2px;
85
+ background: linear-gradient(90deg, transparent, var(--neon-cyan), transparent);
86
+ z-index: 1000;
87
+ animation: scan 3s linear infinite;
88
+ }
89
+
90
+ @keyframes scan {
91
+ 0% { top: 0; opacity: 1; }
92
+ 50% { opacity: 0.3; }
93
+ 100% { top: 100vh; opacity: 1; }
94
+ }
95
+
96
+ /* 主容器 */
97
+ .cyber-container {
98
+ display: grid;
99
+ grid-template-columns: 300px 1fr 350px;
100
+ grid-template-rows: 80px 1fr;
101
+ height: 100vh;
102
+ gap: 2px;
103
+ background: var(--gradient-bg);
104
+ }
105
+
106
+ /* 頂部標題欄 */
107
+ .header-bar {
108
+ grid-column: 1 / -1;
109
+ background: var(--bg-secondary);
110
+ border-bottom: var(--border-neon);
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: space-between;
114
+ padding: 0 2rem;
115
+ position: relative;
116
+ overflow: hidden;
117
+ }
118
+
119
+ .header-bar::before {
120
+ content: '';
121
+ position: absolute;
122
+ top: 0;
123
+ left: -100%;
124
+ width: 100%;
125
+ height: 100%;
126
+ background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.1), transparent);
127
+ animation: sweep 4s ease-in-out infinite;
128
+ }
129
+
130
+ @keyframes sweep {
131
+ 0% { left: -100%; }
132
+ 50% { left: 100%; }
133
+ 100% { left: -100%; }
134
+ }
135
+
136
+ .logo-section {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 1rem;
140
+ }
141
+
142
+ .logo-icon {
143
+ font-size: 2rem;
144
+ color: var(--neon-cyan);
145
+ filter: drop-shadow(0 0 10px var(--neon-cyan));
146
+ animation: pulse-glow 2s ease-in-out infinite alternate;
147
+ }
148
+
149
+ @keyframes pulse-glow {
150
+ from { filter: drop-shadow(0 0 10px var(--neon-cyan)); }
151
+ to { filter: drop-shadow(0 0 20px var(--neon-cyan)); }
152
+ }
153
+
154
+ .logo-text {
155
+ font-family: 'Orbitron', monospace;
156
+ font-size: 1.5rem;
157
+ font-weight: 700;
158
+ color: var(--text-primary);
159
+ text-shadow: 0 0 15px var(--neon-blue);
160
+ }
161
+
162
+ .status-indicators {
163
+ display: flex;
164
+ gap: 1rem;
165
+ align-items: center;
166
+ }
167
+
168
+ .status-dot {
169
+ width: 12px;
170
+ height: 12px;
171
+ border-radius: 50%;
172
+ background: var(--neon-green);
173
+ box-shadow: 0 0 15px var(--neon-green);
174
+ animation: blink 1.5s ease-in-out infinite;
175
+ }
176
+
177
+ .status-dot.warning {
178
+ background: var(--neon-yellow);
179
+ box-shadow: 0 0 15px var(--neon-yellow);
180
+ }
181
+
182
+ .status-dot.error {
183
+ background: var(--neon-red);
184
+ box-shadow: 0 0 15px var(--neon-red);
185
+ }
186
+
187
+ @keyframes blink {
188
+ 0%, 100% { opacity: 1; }
189
+ 50% { opacity: 0.3; }
190
+ }
191
+
192
+ /* 左側面板 */
193
+ .left-panel {
194
+ background: var(--bg-secondary);
195
+ border-right: var(--border-neon);
196
+ padding: 2rem;
197
+ overflow-y: auto;
198
+ }
199
+
200
+ .panel-section {
201
+ margin-bottom: 2rem;
202
+ padding: 1rem;
203
+ background: var(--bg-glass);
204
+ border: 1px solid rgba(0, 212, 255, 0.2);
205
+ border-radius: 8px;
206
+ backdrop-filter: blur(10px);
207
+ }
208
+
209
+ .panel-title {
210
+ font-family: 'Orbitron', monospace;
211
+ font-size: 0.9rem;
212
+ color: var(--neon-cyan);
213
+ margin-bottom: 1rem;
214
+ text-transform: uppercase;
215
+ letter-spacing: 2px;
216
+ }
217
+
218
+ .neural-activity {
219
+ height: 100px;
220
+ background: var(--bg-primary);
221
+ border: 1px solid var(--neon-blue);
222
+ border-radius: 4px;
223
+ position: relative;
224
+ overflow: hidden;
225
+ }
226
+
227
+ .wave {
228
+ position: absolute;
229
+ width: 100%;
230
+ height: 2px;
231
+ background: var(--neon-cyan);
232
+ box-shadow: 0 0 10px var(--neon-cyan);
233
+ animation: wave 2s ease-in-out infinite;
234
+ }
235
+
236
+ .wave:nth-child(2) { animation-delay: 0.5s; }
237
+ .wave:nth-child(3) { animation-delay: 1s; }
238
+
239
+ @keyframes wave {
240
+ 0%, 100% { transform: translateX(-100%); }
241
+ 50% { transform: translateX(100%); }
242
  }
243
+
244
+ .system-stats {
245
+ list-style: none;
246
  }
247
+
248
+ .stat-item {
249
+ display: flex;
250
+ justify-content: space-between;
251
+ margin-bottom: 0.5rem;
252
+ font-size: 0.9rem;
253
+ }
254
+
255
+ .stat-label {
256
+ color: var(--text-secondary);
257
+ }
258
+
259
+ .stat-value {
260
+ color: var(--neon-green);
261
+ font-weight: 600;
262
+ }
263
+
264
+ /* 主要工作區域 */
265
+ .main-workspace {
266
+ background: var(--bg-primary);
267
+ padding: 2rem;
268
+ display: flex;
269
+ flex-direction: column;
270
+ position: relative;
271
+ }
272
+
273
+ .workspace-grid {
274
+ display: grid;
275
+ grid-template-rows: auto 1fr auto;
276
+ height: 100%;
277
+ gap: 2rem;
278
+ }
279
+
280
+ /* 環境指示器 */
281
+ .environment-indicator {
282
+ background: var(--bg-glass);
283
+ border: 1px solid rgba(255, 220, 0, 0.3);
284
+ border-radius: 8px;
285
+ padding: 1rem;
286
+ margin-bottom: 1rem;
287
  text-align: center;
288
+ border-left: 4px solid var(--neon-yellow);
289
+ backdrop-filter: blur(10px);
290
+ }
291
+
292
+ /* 上傳區域 */
293
+ .upload-terminal {
294
+ background: var(--bg-secondary);
295
+ border: 2px dashed var(--neon-blue);
296
+ border-radius: 12px;
297
+ padding: 3rem;
298
+ text-align: center;
299
+ position: relative;
300
+ cursor: pointer;
301
+ transition: all 0.3s ease;
302
+ overflow: hidden;
303
+ }
304
+
305
+ .upload-terminal::before {
306
+ content: '';
307
+ position: absolute;
308
+ top: -2px;
309
+ left: -2px;
310
+ right: -2px;
311
+ bottom: -2px;
312
+ background: var(--gradient-neon);
313
+ border-radius: 12px;
314
+ z-index: -1;
315
+ opacity: 0;
316
+ transition: opacity 0.3s ease;
317
+ }
318
+
319
+ .upload-terminal:hover::before {
320
+ opacity: 1;
321
+ animation: border-flow 2s linear infinite;
322
+ }
323
+
324
+ @keyframes border-flow {
325
+ 0% { background-position: 0% 50%; }
326
+ 100% { background-position: 100% 50%; }
327
  }
328
+
329
+ .upload-terminal.dragover {
330
+ border-color: var(--neon-green);
331
+ box-shadow: var(--border-glow);
332
+ }
333
+
334
+ .upload-icon {
335
+ font-size: 4rem;
336
+ color: var(--neon-cyan);
337
+ margin-bottom: 1rem;
338
+ filter: drop-shadow(0 0 20px var(--neon-cyan));
339
+ }
340
+
341
+ .upload-text {
342
+ font-family: 'Orbitron', monospace;
343
+ font-size: 1.2rem;
344
+ color: var(--text-primary);
345
+ margin-bottom: 0.5rem;
346
+ }
347
+
348
+ .upload-hint {
349
+ color: var(--text-secondary);
350
+ font-size: 0.9rem;
351
+ }
352
+
353
+ /* 攝像頭區域 */
354
+ .camera-terminal {
355
+ background: var(--bg-secondary);
356
+ border: var(--border-neon);
357
+ border-radius: 12px;
358
+ padding: 2rem;
359
+ position: relative;
360
+ }
361
+
362
  .video-container {
363
  position: relative;
364
+ border-radius: 8px;
 
 
365
  overflow: hidden;
366
+ margin-bottom: 2rem;
367
+ background: #000;
368
  }
369
+
370
  #video-display {
371
  width: 100%;
372
+ height: auto;
373
+ min-height: 300px;
374
  display: block;
375
+ border-radius: 8px;
376
  }
377
+
378
+ .camera-status {
379
+ position: absolute;
380
+ top: 15px;
381
+ right: 15px;
382
+ padding: 8px 15px;
383
+ border-radius: 20px;
384
+ color: white;
385
+ font-weight: bold;
386
+ font-size: 0.8rem;
387
+ font-family: 'Orbitron', monospace;
388
+ z-index: 100;
389
+ backdrop-filter: blur(10px);
390
+ border: 1px solid rgba(255, 255, 255, 0.2);
391
  }
392
+
393
+ .camera-status.active {
394
+ background: rgba(0, 255, 136, 0.2);
395
+ border-color: var(--neon-green);
396
+ color: var(--neon-green);
397
+ box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
398
  }
399
+
400
+ .camera-status.inactive {
401
+ background: rgba(128, 128, 128, 0.2);
402
+ border-color: var(--text-tertiary);
403
+ color: var(--text-tertiary);
404
  }
405
+
406
+ /* 視頻預覽區域 */
407
+ .video-analyzer {
408
+ display: none;
409
+ background: var(--bg-secondary);
410
+ border: var(--border-neon);
411
+ border-radius: 12px;
412
+ padding: 2rem;
413
+ position: relative;
414
  }
415
+
416
+ .video-overlay {
417
+ position: absolute;
418
+ top: 0;
419
+ left: 0;
420
+ right: 0;
421
+ bottom: 0;
422
+ background: linear-gradient(45deg, transparent 48%, var(--neon-cyan) 49%, var(--neon-cyan) 51%, transparent 52%);
423
+ opacity: 0;
424
+ pointer-events: none;
425
+ animation: scan-overlay 3s ease-in-out infinite;
426
  }
427
+
428
+ @keyframes scan-overlay {
429
+ 0%, 90%, 100% { opacity: 0; }
430
+ 5%, 85% { opacity: 0.1; }
431
  }
432
+
433
+ /* 控制按鈕 */
434
+ .cyber-controls {
435
+ display: flex;
436
+ gap: 1rem;
437
+ justify-content: center;
438
+ margin-bottom: 2rem;
439
  }
440
+
441
+ .cyber-btn {
442
+ background: var(--bg-secondary);
443
+ border: 1px solid var(--neon-blue);
444
+ color: var(--text-primary);
445
+ padding: 1rem 2rem;
446
+ border-radius: 8px;
447
+ font-family: 'Orbitron', monospace;
448
+ font-weight: 600;
449
+ cursor: pointer;
450
+ position: relative;
451
+ overflow: hidden;
452
+ transition: all 0.3s ease;
453
+ text-transform: uppercase;
454
+ letter-spacing: 1px;
455
+ font-size: 0.9rem;
456
  }
457
+
458
+ .cyber-btn::before {
459
+ content: '';
460
+ position: absolute;
461
+ top: 0;
462
+ left: -100%;
463
+ width: 100%;
464
+ height: 100%;
465
+ background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.4), transparent);
466
+ transition: left 0.5s ease;
467
  }
468
+
469
+ .cyber-btn:hover::before {
470
+ left: 100%;
 
 
471
  }
472
+
473
+ .cyber-btn:hover {
474
+ border-color: var(--neon-cyan);
475
+ box-shadow: 0 0 25px rgba(0, 212, 255, 0.5);
476
+ transform: translateY(-2px);
477
+ }
478
+
479
+ .cyber-btn.primary {
480
+ background: var(--gradient-neon);
481
+ border-color: var(--neon-cyan);
482
+ box-shadow: 0 0 20px rgba(0, 212, 255, 0.3);
483
+ }
484
+
485
+ .cyber-btn.danger {
486
+ border-color: var(--neon-red);
487
+ color: var(--neon-red);
488
  }
489
+
490
+ .cyber-btn.danger:hover {
491
+ border-color: var(--neon-red);
492
+ box-shadow: 0 0 25px rgba(255, 0, 85, 0.5);
493
  }
494
+
495
+ .cyber-btn:disabled {
496
+ opacity: 0.5;
497
+ cursor: not-allowed;
498
+ }
499
+
500
+ /* 結果顯示區域 */
501
+ .result-panel {
502
+ background: var(--bg-glass);
503
+ border: 1px solid rgba(0, 212, 255, 0.2);
504
+ border-radius: 12px;
505
+ padding: 2rem;
506
+ margin-top: 1rem;
507
+ backdrop-filter: blur(10px);
508
+ }
509
+
510
+ .result-title {
511
+ font-family: 'Orbitron', monospace;
512
+ color: var(--neon-cyan);
513
+ font-size: 1rem;
514
+ margin-bottom: 1rem;
515
+ text-transform: uppercase;
516
+ letter-spacing: 2px;
517
+ }
518
+
519
+ .result-content {
520
+ font-size: 1.2rem;
521
+ color: var(--text-primary);
522
+ padding: 1rem;
523
+ background: var(--bg-primary);
524
+ border: 1px solid rgba(0, 212, 255, 0.1);
525
  border-radius: 8px;
526
+ min-height: 60px;
527
+ display: flex;
528
+ align-items: center;
529
+ justify-content: center;
530
+ }
531
+
532
+ /* 進度分析器 */
533
+ .progress-analyzer {
534
+ display: none;
535
+ background: var(--bg-secondary);
536
+ border: var(--border-neon);
537
+ border-radius: 12px;
538
+ padding: 2rem;
539
+ margin-top: 1rem;
540
  }
541
+
542
+ .progress-display {
543
+ background: var(--bg-primary);
544
  border-radius: 8px;
545
+ padding: 1rem;
546
+ margin-bottom: 1rem;
 
547
  }
548
+
549
+ .progress-track {
550
+ height: 6px;
551
+ background: var(--bg-tertiary);
552
+ border-radius: 3px;
553
+ overflow: hidden;
554
+ position: relative;
555
+ margin-bottom: 1rem;
556
  }
557
+
558
+ .progress-fill {
559
+ height: 100%;
560
+ background: var(--gradient-neon);
561
+ border-radius: 3px;
562
+ width: 0%;
563
+ transition: width 0.5s ease;
564
+ box-shadow: 0 0 15px var(--neon-cyan);
565
  }
566
+
567
+ .progress-text {
568
+ text-align: center;
569
+ color: var(--neon-cyan);
570
+ font-family: 'Orbitron', monospace;
571
+ font-size: 0.9rem;
572
+ }
573
+
574
+ /* 右側數據面板 */
575
+ .data-panel {
576
+ background: var(--bg-secondary);
577
+ border-left: var(--border-neon);
578
+ padding: 2rem;
579
+ overflow-y: auto;
580
+ }
581
+
582
+ .data-section {
583
+ background: var(--bg-glass);
584
+ border: 1px solid rgba(0, 212, 255, 0.2);
585
+ border-radius: 8px;
586
+ padding: 1.5rem;
587
+ margin-bottom: 2rem;
588
+ backdrop-filter: blur(10px);
589
  }
590
+
591
+ .data-title {
592
+ font-family: 'Orbitron', monospace;
593
+ color: var(--neon-cyan);
594
+ font-size: 1rem;
595
+ margin-bottom: 1rem;
596
+ text-transform: uppercase;
597
+ letter-spacing: 2px;
598
+ }
599
+
600
+ .result-metric {
601
+ display: flex;
602
+ justify-content: space-between;
603
+ align-items: center;
604
+ padding: 0.75rem;
605
+ background: var(--bg-primary);
606
+ border: 1px solid rgba(0, 212, 255, 0.1);
607
+ border-radius: 6px;
608
+ margin-bottom: 0.75rem;
609
+ }
610
+
611
+ .metric-label {
612
+ color: var(--text-secondary);
613
+ font-size: 0.9rem;
614
+ }
615
+
616
+ .metric-value {
617
+ color: var(--neon-green);
618
+ font-family: 'Orbitron', monospace;
619
+ font-weight: 600;
620
+ }
621
+
622
+ .confidence-meter {
623
+ height: 4px;
624
+ background: var(--bg-tertiary);
625
+ border-radius: 2px;
626
+ overflow: hidden;
627
+ margin-top: 0.5rem;
628
+ }
629
+
630
+ .confidence-fill {
631
+ height: 100%;
632
+ background: linear-gradient(90deg, var(--neon-red), var(--neon-yellow), var(--neon-green));
633
+ border-radius: 2px;
634
+ width: 0%;
635
+ transition: width 1s ease;
636
+ box-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
637
+ }
638
+
639
+ /* 機率條 */
640
  .prob-bar-container {
641
  height: 25px;
642
+ background-color: var(--bg-tertiary);
643
  border-radius: 5px;
644
  margin-bottom: 10px;
645
  overflow: hidden;
646
+ position: relative;
647
  }
648
+
649
  .prob-bar {
650
  height: 100%;
651
+ background: var(--gradient-neon);
652
  border-radius: 5px;
653
  transition: width 0.3s ease;
654
+ box-shadow: 0 0 15px rgba(0, 212, 255, 0.3);
655
  }
656
+
657
  .prob-label {
658
  display: flex;
659
  justify-content: space-between;
660
  margin-bottom: 5px;
661
+ font-size: 0.8rem;
662
  }
663
+
664
+ /* 通知系統 */
665
+ .cyber-notification {
666
+ position: fixed;
667
+ top: 2rem;
668
+ right: 2rem;
669
+ background: var(--bg-secondary);
670
+ border: var(--border-neon);
671
+ border-radius: 8px;
672
+ padding: 1rem 1.5rem;
673
+ color: var(--text-primary);
674
+ box-shadow: var(--shadow-neon);
675
+ z-index: 1000;
676
+ display: none;
677
+ backdrop-filter: blur(10px);
678
+ font-family: 'Orbitron', monospace;
679
  font-size: 0.9rem;
680
  }
681
+
682
+ .cyber-notification.success {
683
+ border-color: var(--neon-green);
684
+ box-shadow: 0 0 30px rgba(0, 255, 136, 0.3);
 
 
 
685
  }
686
+
687
+ .cyber-notification.error {
688
+ border-color: var(--neon-red);
689
+ box-shadow: 0 0 30px rgba(255, 0, 85, 0.3);
690
+ }
691
+
692
+ /* 加載動畫 */
693
+ .cyber-loader {
694
+ display: inline-block;
695
+ width: 20px;
696
+ height: 20px;
697
+ border: 2px solid var(--bg-tertiary);
698
+ border-top: 2px solid var(--neon-cyan);
699
+ border-radius: 50%;
700
+ animation: cyber-spin 1s linear infinite;
701
+ margin-right: 0.5rem;
702
+ }
703
+
704
+ @keyframes cyber-spin {
705
+ 0% { transform: rotate(0deg); }
706
+ 100% { transform: rotate(360deg); }
707
  }
708
+
709
+ /* 數據流動畫 */
710
+ .data-stream {
711
+ position: fixed;
712
+ top: 0;
713
+ right: 0;
714
+ width: 200px;
715
+ height: 100vh;
716
+ pointer-events: none;
717
+ z-index: -1;
718
  }
719
+
720
+ .stream-line {
721
+ position: absolute;
722
+ width: 1px;
723
+ height: 100px;
724
+ background: linear-gradient(to bottom, transparent, var(--neon-cyan), transparent);
725
+ animation: stream 4s linear infinite;
726
+ }
727
+
728
+ .stream-line:nth-child(2) { left: 50px; animation-delay: 1s; }
729
+ .stream-line:nth-child(3) { left: 100px; animation-delay: 2s; }
730
+ .stream-line:nth-child(4) { left: 150px; animation-delay: 3s; }
731
+
732
+ @keyframes stream {
733
+ 0% { top: -100px; opacity: 0; }
734
+ 20% { opacity: 1; }
735
+ 80% { opacity: 1; }
736
+ 100% { top: 100vh; opacity: 0; }
737
+ }
738
+
739
+ /* 響應式設計 */
740
+ @media (max-width: 1200px) {
741
+ .cyber-container {
742
+ grid-template-columns: 250px 1fr 300px;
743
+ }
744
  }
745
+
746
+ @media (max-width: 1024px) {
747
+ .cyber-container {
748
+ grid-template-columns: 1fr;
749
+ grid-template-rows: 80px 1fr;
750
+ }
751
+
752
+ .left-panel,
753
+ .data-panel {
754
+ display: none;
755
+ }
756
+
757
+ .main-workspace {
758
+ padding: 1rem;
759
+ }
760
  }
761
+
762
+ @media (max-width: 768px) {
763
+ .header-bar {
764
+ padding: 0 1rem;
765
+ }
766
+
767
+ .logo-text {
768
+ font-size: 1.2rem;
769
+ }
770
+
771
+ .cyber-controls {
772
+ flex-direction: column;
773
+ gap: 0.5rem;
774
+ }
775
+
776
+ .cyber-btn {
777
+ padding: 0.8rem 1.5rem;
778
+ }
779
  }
780
  </style>
781
  </head>
782
  <body>
783
+ <!-- 粒子背景 -->
784
+ <canvas id="particle-canvas"></canvas>
785
+
786
+ <!-- 掃描線 -->
787
+ <div class="scan-line"></div>
788
+
789
+ <!-- 數據流 -->
790
+ <div class="data-stream">
791
+ <div class="stream-line"></div>
792
+ <div class="stream-line"></div>
793
+ <div class="stream-line"></div>
794
+ <div class="stream-line"></div>
795
+ </div>
796
+
797
+ <!-- 主容器 -->
798
+ <div class="cyber-container">
799
+ <!-- 頂部標題欄 -->
800
+ <header class="header-bar">
801
+ <div class="logo-section">
802
+ <i class="fas fa-brain logo-icon"></i>
803
+ <div class="logo-text">SignView 2.0</div>
804
+ <div style="color: var(--text-secondary); font-size: 0.8rem; margin-left: 1rem;">
805
+ Neural Sign Recognition Lab
806
+ </div>
807
+ </div>
808
+
809
+ <div class="status-indicators">
810
+ <div class="status-dot" id="system-status" title="系統狀態"></div>
811
+ <div class="status-dot warning" id="ai-status" title="AI模型狀態"></div>
812
+ <div class="status-dot" id="network-status" title="網路連接"></div>
813
+ <span id="environment-info" style="color: var(--text-secondary); font-size: 0.8rem; margin-left: 1rem;">
814
+ 檢測中...
815
+ </span>
816
+ </div>
817
+ </header>
818
 
819
+ <!-- 左側控制面板 -->
820
+ <aside class="left-panel">
821
+ <div class="panel-section">
822
+ <div class="panel-title">神經網路活動</div>
823
+ <div class="neural-activity">
824
+ <div class="wave"></div>
825
+ <div class="wave"></div>
826
+ <div class="wave"></div>
827
+ </div>
828
+ </div>
829
+
830
+ <div class="panel-section">
831
+ <div class="panel-title">系統狀態</div>
832
+ <ul class="system-stats">
833
+ <li class="stat-item">
834
+ <span class="stat-label">處理器使用率</span>
835
+ <span class="stat-value" id="cpu-usage">23%</span>
836
+ </li>
837
+ <li class="stat-item">
838
+ <span class="stat-label">記憶體使用</span>
839
+ <span class="stat-value" id="memory-usage">2.1GB</span>
840
+ </li>
841
+ <li class="stat-item">
842
+ <span class="stat-label">幀率</span>
843
+ <span class="stat-value" id="fps-display">30.0</span>
844
+ </li>
845
+ <li class="stat-item">
846
+ <span class="stat-label">延遲</span>
847
+ <span class="stat-value" id="latency-display">12ms</span>
848
+ </li>
849
+ </ul>
850
+ </div>
851
+
852
+ <div class="panel-section">
853
+ <div class="panel-title">AI 模型狀態</div>
854
+ <ul class="system-stats">
855
+ <li class="stat-item">
856
+ <span class="stat-label">模型版本</span>
857
+ <span class="stat-value">v3.4.1</span>
858
+ </li>
859
+ <li class="stat-item">
860
+ <span class="stat-label">辨識類別</span>
861
+ <span class="stat-value" id="num-classes">4</span>
862
+ </li>
863
+ <li class="stat-item">
864
+ <span class="stat-label">準確率</span>
865
+ <span class="stat-value">94.36%</span>
866
+ </li>
867
+ </ul>
868
+ </div>
869
+ </aside>
870
 
871
+ <!-- 主要工作區域 -->
872
+ <main class="main-workspace">
873
+ <div class="workspace-grid">
874
+ <!-- 環境指示器 -->
875
+ <div class="environment-indicator">
876
+ <strong>🌐 執行環境:</strong><span id="detailed-environment-info">檢測中...</span>
877
+ </div>
878
+
879
  <!-- 即時攝像頭區域 (本地環境) -->
880
  <div id="camera-section" style="display: none;">
881
+ <div class="camera-terminal">
882
+ <div class="data-title">即時神經網路分析</div>
883
+ <div class="video-container">
884
+ <div id="hand-status" class="camera-status inactive">未偵測</div>
885
+ <img id="video-display" src="" alt="即時視頻畫面">
886
+ <div class="video-overlay"></div>
887
+ </div>
888
+
889
+ <div class="cyber-controls">
890
+ <button id="start-btn" class="cyber-btn primary">
891
+ <i class="fas fa-play"></i>
892
+ 啟動分析
893
+ </button>
894
+ <button id="stop-btn" class="cyber-btn danger" disabled>
895
+ <i class="fas fa-stop"></i>
896
+ 停止分析
897
+ </button>
898
  </div>
 
 
 
 
 
 
 
 
 
 
899
  </div>
900
  </div>
901
 
902
  <!-- 影片上傳區域 (雲端環境) -->
903
  <div id="upload-section">
904
+ <!-- 上傳終端 -->
905
+ <div id="upload-terminal" class="upload-terminal">
906
+ <i class="fas fa-upload upload-icon"></i>
907
+ <div class="upload-text">拖拽或點擊上傳影片</div>
908
+ <div class="upload-hint">支援 MP4, AVI, MOV, WMV | 最大 100MB</div>
909
+ <input type="file" id="video-file" accept="video/*" style="display: none;">
910
+ </div>
911
+
912
+ <!-- 視頻分析器 -->
913
+ <div id="video-analyzer" class="video-analyzer">
914
+ <div class="data-title">視頻神經網路分析器</div>
915
+ <div class="video-container">
916
+ <video id="preview-video" controls style="width: 100%; border-radius: 8px;"></video>
917
+ <div class="video-overlay"></div>
 
 
918
  </div>
919
 
920
+ <div class="cyber-controls">
921
+ <button id="process-video-btn" class="cyber-btn primary">
922
+ <i class="fas fa-brain"></i>
923
+ 開始AI分析
924
+ </button>
925
+ <button id="clear-video-btn" class="cyber-btn danger">
926
+ <i class="fas fa-trash"></i>
927
+ 清除數據
928
+ </button>
929
  </div>
930
  </div>
931
 
932
+ <!-- 進度分析器 -->
933
+ <div id="progress-analyzer" class="progress-analyzer">
934
+ <div class="data-title">神經網路處理進度</div>
935
+ <div class="progress-display">
936
+ <div class="progress-track">
937
+ <div class="progress-fill"></div>
938
+ </div>
939
+ <div id="upload-status" class="progress-text">等待初始化...</div>
940
+ </div>
941
  </div>
942
+ </div>
943
+
944
+ <!-- 結果顯示區域 -->
945
+ <div class="result-panel">
946
+ <div class="result-title">辨識結果</div>
947
+ <div id="video-word-sequence-display" class="result-content">尚無辨識結果</div>
948
+ </div>
949
+
950
+ <div class="result-panel">
951
+ <div class="result-title">AI翻譯結果</div>
952
+ <div id="video-sentence-display" class="result-content">等待神經網路處理...</div>
953
+ </div>
954
+
955
+ <!-- 即時辨識結果 (本地環境) -->
956
+ <div id="realtime-results" class="result-panel" style="display: none;">
957
+ <div class="result-title">即時單詞序列</div>
958
+ <div id="word-sequence-display" class="result-content">尚無偵測結果</div>
959
+ </div>
960
+
961
+ <div id="realtime-sentence" class="result-panel" style="display: none;">
962
+ <div class="result-title">AI生成句子</div>
963
+ <div id="sentence-display" class="result-content">等待手語輸入完成...</div>
964
+ </div>
965
+ </div>
966
+ </main>
967
+
968
+ <!-- 右側數據面板 -->
969
+ <aside class="data-panel">
970
+ <div class="data-section">
971
+ <div class="data-title">即時監控</div>
972
+ <ul class="system-stats">
973
+ <li class="stat-item">
974
+ <span class="stat-label">處理幀數</span>
975
+ <span class="stat-value" id="frame-count">0</span>
976
+ </li>
977
+ <li class="stat-item">
978
+ <span class="stat-label">手部狀態</span>
979
+ <span class="stat-value" id="hand-detection-status">未偵測</span>
980
+ </li>
981
+ <li class="stat-item">
982
+ <span class="stat-label">神經網路</span>
983
+ <span class="stat-value" id="neural-status">待機</span>
984
+ </li>
985
+ </ul>
986
+ </div>
987
+
988
+ <div class="data-section" id="results-display">
989
+ <div class="data-title">分析結果</div>
990
+
991
+ <div class="result-metric">
992
+ <div class="metric-label">當前預測</div>
993
+ <div class="metric-value" id="result-label">未開始</div>
994
+ </div>
995
+
996
+ <div class="result-metric">
997
+ <div class="metric-label">信心度</div>
998
+ <div>
999
+ <div class="metric-value" id="result-confidence">0%</div>
1000
+ <div class="confidence-meter">
1001
+ <div class="confidence-fill"></div>
1002
+ </div>
1003
  </div>
1004
  </div>
1005
  </div>
1006
 
1007
+ <div class="data-section">
1008
+ <div class="data-title">類別機率</div>
1009
+ <div id="probabilities-container">
1010
+ <div class="metric-label" style="text-align: center; color: var(--text-tertiary);">
1011
+ 等待分析開始...
1012
+ </div>
 
 
1013
  </div>
1014
  </div>
1015
+
1016
+ <div class="data-section">
1017
+ <div class="data-title">模型指標</div>
1018
+ <ul class="system-stats">
1019
+ <li class="stat-item">
1020
+ <span class="stat-label">推理時間</span>
1021
+ <span class="stat-value" id="inference-time">0ms</span>
1022
+ </li>
1023
+ <li class="stat-item">
1024
+ <span class="stat-label">特徵維度</span>
1025
+ <span class="stat-value">225</span>
1026
+ </li>
1027
+ <li class="stat-item">
1028
+ <span class="stat-label">模型大小</span>
1029
+ <span class="stat-value">15.2MB</span>
1030
+ </li>
1031
+ </ul>
1032
+ </div>
1033
+ </aside>
1034
  </div>
1035
+
1036
+ <!-- 通知系統 -->
1037
+ <div id="cyber-notification" class="cyber-notification"></div>
1038
+
1039
  <script>
1040
  document.addEventListener('DOMContentLoaded', function() {
1041
+ // 全域變數
1042
+ let selectedVideoFile = null;
1043
+ let isProcessing = false;
1044
+ let animationId = null;
1045
+ let socket = null;
1046
+
1047
  // 環境檢測
1048
  const isHuggingFace = window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface.co');
1049
  const environmentInfo = document.getElementById('environment-info');
1050
+ const detailedEnvironmentInfo = document.getElementById('detailed-environment-info');
1051
  const cameraSection = document.getElementById('camera-section');
1052
  const uploadSection = document.getElementById('upload-section');
1053
+ const realtimeResults = document.getElementById('realtime-results');
1054
+ const realtimeSentence = document.getElementById('realtime-sentence');
1055
+ const systemStatus = document.getElementById('system-status');
1056
+ const aiStatus = document.getElementById('ai-status');
1057
+ const networkStatus = document.getElementById('network-status');
1058
 
1059
+ // DOM 元素
 
 
 
 
 
 
 
 
 
 
1060
  const videoDisplay = document.getElementById('video-display');
1061
  const startBtn = document.getElementById('start-btn');
1062
  const stopBtn = document.getElementById('stop-btn');
 
1066
  const wordSequenceDisplay = document.getElementById('word-sequence-display');
1067
  const sentenceDisplay = document.getElementById('sentence-display');
1068
  const handStatus = document.getElementById('hand-status');
1069
+ const handDetectionStatus = document.getElementById('hand-detection-status');
1070
+ const neuralStatus = document.getElementById('neural-status');
1071
+ const frameCountDisplay = document.getElementById('frame-count');
1072
 
1073
  // 影片上傳相關元素
1074
+ const uploadTerminal = document.getElementById('upload-terminal');
1075
  const videoFile = document.getElementById('video-file');
1076
+ const videoAnalyzer = document.getElementById('video-analyzer');
 
 
1077
  const previewVideo = document.getElementById('preview-video');
1078
  const processVideoBtn = document.getElementById('process-video-btn');
1079
  const clearVideoBtn = document.getElementById('clear-video-btn');
1080
+ const progressAnalyzer = document.getElementById('progress-analyzer');
1081
+ const videoWordSequenceDisplay = document.getElementById('video-word-sequence-display');
1082
+ const videoSentenceDisplay = document.getElementById('video-sentence-display');
1083
+ const notification = document.getElementById('cyber-notification');
1084
 
1085
+ // 初始化
1086
+ initParticleSystem();
1087
+ setupEnvironment();
1088
+ setupEventListeners();
1089
+ animateEntry();
1090
+ updateSystemStats();
1091
+
1092
+ // 環境設置
1093
+ function setupEnvironment() {
1094
+ if (isHuggingFace) {
1095
+ environmentInfo.innerHTML = '☁️ HuggingFace Spaces';
1096
+ detailedEnvironmentInfo.innerHTML = '☁️ HuggingFace Spaces (雲端) - 使用影片上傳功能';
1097
+ cameraSection.style.display = 'none';
1098
+ uploadSection.style.display = 'block';
1099
+ realtimeResults.style.display = 'none';
1100
+ realtimeSentence.style.display = 'none';
1101
+
1102
+ // 更新狀態指示器
1103
+ systemStatus.className = 'status-dot';
1104
+ aiStatus.className = 'status-dot warning';
1105
+ networkStatus.className = 'status-dot';
1106
+ } else {
1107
+ environmentInfo.innerHTML = '💻 本地環境';
1108
+ detailedEnvironmentInfo.innerHTML = '💻 本地環境 - 支援即時攝像頭辨識';
1109
+ cameraSection.style.display = 'block';
1110
+ uploadSection.style.display = 'none';
1111
+ realtimeResults.style.display = 'block';
1112
+ realtimeSentence.style.display = 'block';
1113
+
1114
+ // 初始化WebSocket
1115
+ socket = io();
1116
+ setupWebSocketEvents();
1117
+ }
1118
  }
1119
 
1120
+ // 粒子系統
1121
+ function initParticleSystem() {
1122
+ const canvas = document.getElementById('particle-canvas');
1123
+ const ctx = canvas.getContext('2d');
 
1124
 
1125
+ canvas.width = window.innerWidth;
1126
+ canvas.height = window.innerHeight;
1127
+
1128
+ const particles = [];
1129
+ const particleCount = 80;
1130
+
1131
+ class Particle {
1132
+ constructor() {
1133
+ this.x = Math.random() * canvas.width;
1134
+ this.y = Math.random() * canvas.height;
1135
+ this.vx = (Math.random() - 0.5) * 0.3;
1136
+ this.vy = (Math.random() - 0.5) * 0.3;
1137
+ this.size = Math.random() * 2 + 1;
1138
+ this.opacity = Math.random() * 0.5 + 0.2;
1139
+ }
1140
 
1141
+ update() {
1142
+ this.x += this.vx;
1143
+ this.y += this.vy;
1144
+
1145
+ if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
1146
+ if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
1147
+ }
1148
+
1149
+ draw() {
1150
+ ctx.globalAlpha = this.opacity;
1151
+ ctx.fillStyle = '#00d4ff';
1152
+ ctx.shadowBlur = 10;
1153
+ ctx.shadowColor = '#00d4ff';
1154
+ ctx.beginPath();
1155
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
1156
+ ctx.fill();
1157
+ }
1158
+ }
1159
 
1160
+ // 初始化粒子
1161
+ for (let i = 0; i < particleCount; i++) {
1162
+ particles.push(new Particle());
1163
+ }
 
 
 
 
 
 
 
 
 
1164
 
1165
+ function animate() {
1166
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1167
+
1168
+ particles.forEach(particle => {
1169
+ particle.update();
1170
+ particle.draw();
 
 
 
 
 
1171
  });
1172
+
1173
+ // 繪製連接線
1174
+ ctx.globalAlpha = 0.1;
1175
+ ctx.strokeStyle = '#00d4ff';
1176
+ ctx.lineWidth = 1;
1177
+
1178
+ for (let i = 0; i < particles.length; i++) {
1179
+ for (let j = i + 1; j < particles.length; j++) {
1180
+ const dx = particles[i].x - particles[j].x;
1181
+ const dy = particles[i].y - particles[j].y;
1182
+ const distance = Math.sqrt(dx * dx + dy * dy);
1183
+
1184
+ if (distance < 100) {
1185
+ ctx.beginPath();
1186
+ ctx.moveTo(particles[i].x, particles[i].y);
1187
+ ctx.lineTo(particles[j].x, particles[j].y);
1188
+ ctx.stroke();
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ animationId = requestAnimationFrame(animate);
1194
+ }
1195
+
1196
+ animate();
1197
+
1198
+ // 視窗調整
1199
+ window.addEventListener('resize', () => {
1200
+ canvas.width = window.innerWidth;
1201
+ canvas.height = window.innerHeight;
1202
  });
1203
  }
1204
 
1205
+ // WebSocket 事件設置
1206
+ function setupWebSocketEvents() {
1207
+ if (!socket) return;
 
 
 
 
1208
 
1209
+ socket.on('connect', function() {
1210
+ console.log('已連接到神經網路服務');
1211
+ showNotification('神經網路連接已建立', 'success');
1212
+ networkStatus.className = 'status-dot';
1213
  });
1214
 
1215
+ socket.on('disconnect', function() {
1216
+ console.log('神經網路連接已斷開');
1217
+ showNotification('神經網路連接已斷開', 'error');
1218
+ networkStatus.className = 'status-dot error';
 
 
 
1219
  });
1220
 
1221
+ // 接收幀更新
1222
+ socket.on('update_frame', function(data) {
1223
+ if (videoDisplay) {
1224
+ videoDisplay.src = `data:image/jpeg;base64,${data.image}`;
1225
  }
1226
+ updateRealtimeStatus(data.status);
1227
  });
1228
+ }
1229
+
1230
+ // 設置事件監聽器
1231
+ function setupEventListeners() {
1232
+ // 攝像頭控制 (本地環境)
1233
+ if (!isHuggingFace && socket) {
1234
+ startBtn?.addEventListener('click', function() {
1235
+ socket.emit('start_stream', {}, function(response) {
1236
+ if (response.status === 'success') {
1237
+ startBtn.disabled = true;
1238
+ stopBtn.disabled = false;
1239
+ resultLabel.textContent = '神經網路分析中...';
1240
+ resultConfidence.textContent = '信心度: 0%';
1241
+ neuralStatus.textContent = '分析中';
1242
+ aiStatus.className = 'status-dot';
1243
+ showNotification('攝像頭神經網路分析已啟動', 'success');
1244
+ } else {
1245
+ showNotification('啟動失敗: ' + (response.message || '未知錯誤'), 'error');
1246
+ }
1247
+ });
1248
+ });
1249
 
1250
+ stopBtn?.addEventListener('click', function() {
1251
+ socket.emit('stop_stream', {}, function(response) {
1252
+ if (response.status === 'success') {
1253
+ startBtn.disabled = false;
1254
+ stopBtn.disabled = true;
1255
+ resultLabel.textContent = '未開始';
1256
+ resultConfidence.textContent = '信心度: 0%';
1257
+ handStatus.textContent = '未偵測';
1258
+ handStatus.className = 'camera-status inactive';
1259
+ handDetectionStatus.textContent = '未偵測';
1260
+ neuralStatus.textContent = '待機';
1261
+ aiStatus.className = 'status-dot warning';
1262
+ showNotification('神經網路分析已停止', 'success');
1263
+ }
1264
+ });
1265
+ });
1266
  }
1267
 
1268
+ // 影片上傳功能 (雲端環境)
1269
+ if (isHuggingFace) {
1270
+ // 點擊上傳
1271
+ uploadTerminal?.addEventListener('click', () => videoFile?.click());
 
 
 
 
 
 
 
 
 
 
 
 
 
1272
 
1273
+ // 檔案選擇
1274
+ videoFile?.addEventListener('change', function(e) {
1275
+ if (e.target.files.length > 0) {
1276
+ handleVideoFile(e.target.files[0]);
1277
+ }
1278
+ });
 
 
 
 
1279
 
1280
+ // 拖拽支援
1281
+ uploadTerminal?.addEventListener('dragover', function(e) {
1282
+ e.preventDefault();
1283
+ uploadTerminal.classList.add('dragover');
1284
+ });
1285
 
1286
+ uploadTerminal?.addEventListener('dragleave', function(e) {
1287
+ e.preventDefault();
1288
+ uploadTerminal.classList.remove('dragover');
1289
+ });
1290
 
1291
+ uploadTerminal?.addEventListener('drop', function(e) {
1292
+ e.preventDefault();
1293
+ uploadTerminal.classList.remove('dragover');
1294
+ const files = e.dataTransfer.files;
1295
+ if (files.length > 0) {
1296
+ handleVideoFile(files[0]);
1297
  }
1298
  });
1299
 
1300
+ // 處理影片按鈕
1301
+ processVideoBtn?.addEventListener('click', function() {
1302
+ if (!selectedVideoFile) {
1303
+ showNotification('請先選擇影片檔案!', 'error');
1304
+ return;
 
 
 
 
 
 
 
 
 
 
 
1305
  }
1306
+ uploadVideo(selectedVideoFile);
 
1307
  });
1308
 
1309
+ // 清除影片按鈕
1310
+ clearVideoBtn?.addEventListener('click', function() {
1311
+ clearVideo();
 
 
 
1312
  });
1313
+ }
1314
+ }
1315
+
1316
+ // 處理影片檔案
1317
+ function handleVideoFile(file) {
1318
+ if (!file.type.startsWith('video/')) {
1319
+ showNotification('請選擇影片檔案!', 'error');
1320
+ return;
1321
+ }
1322
+
1323
+ if (file.size > 100 * 1024 * 1024) {
1324
+ showNotification('檔案大小不能超過 100MB', 'error');
1325
+ return;
1326
+ }
1327
+
1328
+ selectedVideoFile = file;
1329
+
1330
+ // 顯示預覽
1331
+ const url = URL.createObjectURL(file);
1332
+ if (previewVideo) {
1333
+ previewVideo.src = url;
1334
+ }
1335
+
1336
+ // 動畫切換到分析器
1337
+ if (uploadTerminal && videoAnalyzer) {
1338
+ anime({
1339
+ targets: uploadTerminal,
1340
+ opacity: 0,
1341
+ scale: 0.8,
1342
+ duration: 500,
1343
+ complete: () => {
1344
+ uploadTerminal.style.display = 'none';
1345
+ videoAnalyzer.style.display = 'block';
1346
+
1347
+ anime({
1348
+ targets: videoAnalyzer,
1349
+ opacity: [0, 1],
1350
+ scale: [0.8, 1],
1351
+ duration: 800,
1352
+ easing: 'easeOutCubic'
1353
+ });
1354
+ }
1355
  });
 
 
 
 
1356
  }
1357
 
1358
+ showNotification('影片載入成功,神經網路已準備就緒', 'success');
1359
+ }
1360
+
1361
+ // 上傳影片
1362
+ function uploadVideo(file) {
1363
+ if (isProcessing) return;
1364
+
1365
+ isProcessing = true;
1366
+
1367
+ // 顯示進度分析器
1368
+ if (progressAnalyzer) {
1369
+ progressAnalyzer.style.display = 'block';
1370
+ anime({
1371
+ targets: progressAnalyzer,
1372
+ opacity: [0, 1],
1373
+ translateY: [-30, 0],
1374
+ duration: 600
1375
+ });
1376
+ }
1377
+
1378
+ // 更新按鈕狀態
1379
+ if (processVideoBtn) {
1380
+ processVideoBtn.innerHTML = '<div class="cyber-loader"></div>神經網路運算中...';
1381
+ processVideoBtn.disabled = true;
1382
+ }
1383
+ if (clearVideoBtn) {
1384
+ clearVideoBtn.disabled = true;
1385
+ }
1386
+
1387
+ // 更新狀態
1388
+ aiStatus.className = 'status-dot';
1389
+ neuralStatus.textContent = '深度學習中';
1390
+
1391
+ const formData = new FormData();
1392
+ formData.append('video', file);
1393
+
1394
+ // 模擬進度更新
1395
+ simulateProgress();
1396
+
1397
+ fetch('/process_video', {
1398
+ method: 'POST',
1399
+ body: formData
1400
+ })
1401
+ .then(response => {
1402
+ if (!response.ok) {
1403
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1404
+ }
1405
+ return response.json();
1406
+ })
1407
+ .then(result => {
1408
+ displayVideoResult(result);
1409
+ })
1410
+ .catch(error => {
1411
+ console.error('神經網路處理錯誤:', error);
1412
+ showNotification('神經網路處理失敗: ' + error.message, 'error');
1413
+ })
1414
+ .finally(() => {
1415
+ resetProcessingState();
1416
+ });
1417
+ }
1418
+
1419
+ // 模擬進度更新
1420
+ function simulateProgress() {
1421
+ let progress = 0;
1422
+ const phases = [
1423
+ '初始化神經網路...',
1424
+ '載入預訓練模型...',
1425
+ '提取關鍵點特徵...',
1426
+ '深度學習推理...',
1427
+ '生成預測結果...'
1428
+ ];
1429
+ let currentPhase = 0;
1430
+
1431
+ const interval = setInterval(() => {
1432
+ if (progress < 90 && isProcessing) {
1433
+ progress += Math.random() * 15;
1434
+ if (progress > 90) progress = 90;
1435
+
1436
+ // 更新階段
1437
+ if (progress > (currentPhase + 1) * 18 && currentPhase < phases.length - 1) {
1438
+ currentPhase++;
1439
+ }
1440
 
1441
+ updateProgress(progress, phases[currentPhase]);
1442
+ } else {
1443
+ clearInterval(interval);
1444
+ }
1445
+ }, 1200);
1446
+ }
1447
+
1448
+ // 更新進度
1449
+ function updateProgress(percentage, phase) {
1450
+ const progressFill = document.querySelector('.progress-fill');
1451
+ const uploadStatus = document.getElementById('upload-status');
1452
+
1453
+ if (progressFill) {
1454
+ anime({
1455
+ targets: progressFill,
1456
+ width: percentage + '%',
1457
+ duration: 800,
1458
+ easing: 'easeOutCubic'
1459
+ });
1460
+ }
1461
+
1462
+ if (uploadStatus) {
1463
+ uploadStatus.textContent = `${phase} ${Math.round(percentage)}%`;
1464
+ }
1465
+ }
1466
+
1467
+ // 顯示影片辨識結果
1468
+ function displayVideoResult(result) {
1469
+ console.log('收到神經網路分析結果:', result);
1470
+
1471
+ if (result.status === 'success') {
1472
+ // 完成進度
1473
+ updateProgress(100, '神經網路分析完成!');
1474
+
1475
+ // 顯示辨識結果標籤
1476
+ const recognitionText = result.recognition_result || '神經網路分析完成';
1477
+ if (resultLabel) resultLabel.textContent = recognitionText;
1478
+
1479
+ // 顯示信心度
1480
+ const confidence = result.confidence || 0;
1481
+ if (resultConfidence) resultConfidence.textContent = `信心度: ${(confidence * 100).toFixed(1)}%`;
1482
+
1483
+ // 更新信心度條
1484
+ const confidenceFill = document.querySelector('.confidence-fill');
1485
+ if (confidenceFill) {
1486
+ anime({
1487
+ targets: confidenceFill,
1488
+ width: (confidence * 100) + '%',
1489
+ duration: 1500,
1490
+ easing: 'easeOutCubic'
1491
+ });
1492
+ }
1493
+
1494
+ // 顯示單詞序列
1495
+ if (videoWordSequenceDisplay) {
1496
  if (result.word_sequence && result.word_sequence.length > 0) {
1497
+ videoWordSequenceDisplay.textContent = result.word_sequence.join(' ');
1498
+ } else if (result.recognition_result) {
1499
+ videoWordSequenceDisplay.textContent = result.recognition_result;
1500
  } else {
1501
+ videoWordSequenceDisplay.textContent = '無辨識結果';
1502
  }
1503
+ }
1504
+
1505
+ // 顯示翻譯句子
1506
+ if (videoSentenceDisplay) {
1507
  if (result.generated_sentence) {
1508
+ videoSentenceDisplay.textContent = result.generated_sentence;
1509
+ } else if (result.recognition_result) {
1510
+ videoSentenceDisplay.textContent = result.recognition_result;
1511
  } else {
1512
+ videoSentenceDisplay.textContent = '無翻譯結果';
1513
  }
 
 
 
 
 
 
 
 
 
1514
  }
1515
+
1516
+ showNotification('神經網路分析完成!', 'success');
1517
+
1518
+ } else {
1519
+ console.error('神經網路分析失敗:', result);
1520
+ showNotification('神經網路分析失敗: ' + (result.message || result.error || '未知錯誤'), 'error');
1521
+
1522
+ // 重置顯示
1523
+ if (resultLabel) resultLabel.textContent = '分析失敗';
1524
+ if (resultConfidence) resultConfidence.textContent = '信心度: 0%';
1525
+ if (videoWordSequenceDisplay) videoWordSequenceDisplay.textContent = '分析失敗';
1526
+ if (videoSentenceDisplay) videoSentenceDisplay.textContent = '分析失敗';
1527
  }
1528
  }
1529
 
1530
+ // 重置處理狀態
1531
+ function resetProcessingState() {
1532
+ isProcessing = false;
1533
+
1534
+ if (processVideoBtn) {
1535
+ processVideoBtn.innerHTML = '<i class="fas fa-brain"></i> 開始AI分析';
1536
+ processVideoBtn.disabled = false;
1537
+ }
1538
+ if (clearVideoBtn) {
1539
+ clearVideoBtn.disabled = false;
1540
+ }
1541
+
1542
+ aiStatus.className = 'status-dot warning';
1543
+ neuralStatus.textContent = '待機';
1544
+
1545
+ setTimeout(() => {
1546
+ if (progressAnalyzer) {
1547
+ progressAnalyzer.style.display = 'none';
1548
+ const progressFill = document.querySelector('.progress-fill');
1549
+ if (progressFill) progressFill.style.width = '0%';
1550
+ }
1551
+ }, 3000);
1552
+ }
1553
+
1554
+ // 清除影片
1555
+ function clearVideo() {
1556
+ selectedVideoFile = null;
1557
+ if (videoFile) videoFile.value = '';
1558
+ if (previewVideo) previewVideo.src = '';
1559
+
1560
+ // 隱藏進度和分析器
1561
+ if (progressAnalyzer) progressAnalyzer.style.display = 'none';
1562
+
1563
+ // 動畫切換回上傳終端
1564
+ if (videoAnalyzer && uploadTerminal) {
1565
+ anime({
1566
+ targets: videoAnalyzer,
1567
+ opacity: 0,
1568
+ scale: 0.8,
1569
+ duration: 500,
1570
+ complete: () => {
1571
+ videoAnalyzer.style.display = 'none';
1572
+ uploadTerminal.style.display = 'block';
1573
+
1574
+ anime({
1575
+ targets: uploadTerminal,
1576
+ opacity: [0, 1],
1577
+ scale: [0.8, 1],
1578
+ duration: 800,
1579
+ easing: 'easeOutCubic'
1580
+ });
1581
+ }
1582
+ });
1583
+ }
1584
+
1585
+ // 重置結果顯示
1586
+ if (resultLabel) resultLabel.textContent = '未開始';
1587
+ if (resultConfidence) resultConfidence.textContent = '信心度: 0%';
1588
+ if (probabilitiesContainer) {
1589
+ probabilitiesContainer.innerHTML = '<div class="metric-label" style="text-align: center; color: var(--text-tertiary);">等待分析開始...</div>';
1590
+ }
1591
+ if (videoWordSequenceDisplay) videoWordSequenceDisplay.textContent = '尚無辨識結果';
1592
+ if (videoSentenceDisplay) videoSentenceDisplay.textContent = '等待神經網路處理...';
1593
+
1594
+ showNotification('數據已清除,神經網路已重置', 'success');
1595
+ }
1596
+
1597
+ // 更新即時狀態 (本地環境)
1598
+ function updateRealtimeStatus(status) {
1599
+ if (isHuggingFace) return;
1600
+
1601
  // 更新手部狀態
1602
  if (status.hand_present) {
1603
+ if (handStatus) {
1604
+ handStatus.textContent = '已偵測到手部';
1605
+ handStatus.className = 'camera-status active';
1606
+ }
1607
+ if (handDetectionStatus) handDetectionStatus.textContent = '已偵測';
1608
  } else {
1609
+ if (handStatus) {
1610
+ handStatus.textContent = '未偵測到手部';
1611
+ handStatus.className = 'camera-status inactive';
1612
+ }
1613
+ if (handDetectionStatus) handDetectionStatus.textContent = '未偵測';
1614
  }
1615
 
1616
  // 更新當前預測結果
1617
  if (status.current_prediction) {
1618
+ if (resultLabel) resultLabel.textContent = status.current_prediction.label;
1619
+ if (resultConfidence) resultConfidence.textContent = `信心度: ${(status.current_prediction.confidence * 100).toFixed(1)}%`;
1620
+
1621
+ // 更新信心度條
1622
+ const confidenceFill = document.querySelector('.confidence-fill');
1623
+ if (confidenceFill) {
1624
+ confidenceFill.style.width = `${status.current_prediction.confidence * 100}%`;
1625
+ }
1626
  }
1627
 
1628
  // 更新機率條
1629
+ if (status.probabilities && probabilitiesContainer) {
1630
  probabilitiesContainer.innerHTML = '';
1631
  status.probabilities.forEach(function(item) {
1632
  const probContainer = document.createElement('div');
 
1651
  }
1652
 
1653
  // 更新單詞序列
1654
+ if (status.word_sequence && status.word_sequence.length > 0 && wordSequenceDisplay) {
1655
  wordSequenceDisplay.textContent = status.word_sequence.join(' ');
1656
+ } else if (wordSequenceDisplay) {
1657
  wordSequenceDisplay.textContent = '尚無偵測結果';
1658
  }
1659
 
1660
  // 更新生成的句子
1661
+ if (status.generated_sentence && status.display_sentence && sentenceDisplay) {
1662
  sentenceDisplay.textContent = status.generated_sentence;
1663
+ } else if (sentenceDisplay && !status.display_sentence) {
1664
  sentenceDisplay.textContent = '等待手語輸入完成...';
1665
  }
1666
+
1667
+ // 更新幀數
1668
+ if (status.frame_count && frameCountDisplay) {
1669
+ frameCountDisplay.textContent = status.frame_count;
1670
+ }
1671
+ }
1672
+
1673
+ // 入場動畫
1674
+ function animateEntry() {
1675
+ if (typeof anime === 'undefined') return;
1676
+
1677
+ anime.timeline()
1678
+ .add({
1679
+ targets: '.header-bar',
1680
+ opacity: [0, 1],
1681
+ translateY: [-50, 0],
1682
+ duration: 1000,
1683
+ easing: 'easeOutCubic'
1684
+ })
1685
+ .add({
1686
+ targets: '.left-panel',
1687
+ opacity: [0, 1],
1688
+ translateX: [-100, 0],
1689
+ duration: 800,
1690
+ easing: 'easeOutCubic'
1691
+ }, '-=500')
1692
+ .add({
1693
+ targets: '.main-workspace',
1694
+ opacity: [0, 1],
1695
+ scale: [0.9, 1],
1696
+ duration: 800,
1697
+ easing: 'easeOutCubic'
1698
+ }, '-=600')
1699
+ .add({
1700
+ targets: '.data-panel',
1701
+ opacity: [0, 1],
1702
+ translateX: [100, 0],
1703
+ duration: 800,
1704
+ easing: 'easeOutCubic'
1705
+ }, '-=600');
1706
+ }
1707
+
1708
+ // 更新系統統計
1709
+ function updateSystemStats() {
1710
+ setInterval(() => {
1711
+ if (isProcessing) {
1712
+ // 處理中的動態數據
1713
+ const cpuUsage = document.getElementById('cpu-usage');
1714
+ const memoryUsage = document.getElementById('memory-usage');
1715
+ const fpsDisplay = document.getElementById('fps-display');
1716
+ const latencyDisplay = document.getElementById('latency-display');
1717
+ const inferenceTime = document.getElementById('inference-time');
1718
+
1719
+ if (cpuUsage) cpuUsage.textContent = Math.floor(Math.random() * 30 + 40) + '%';
1720
+ if (memoryUsage) memoryUsage.textContent = (Math.random() * 1.5 + 2.5).toFixed(1) + 'GB';
1721
+ if (fpsDisplay) fpsDisplay.textContent = (Math.random() * 5 + 25).toFixed(1);
1722
+ if (latencyDisplay) latencyDisplay.textContent = Math.floor(Math.random() * 20 + 5) + 'ms';
1723
+ if (inferenceTime) inferenceTime.textContent = Math.floor(Math.random() * 50 + 10) + 'ms';
1724
+ } else {
1725
+ // 待機狀態的穩定數據
1726
+ const cpuUsage = document.getElementById('cpu-usage');
1727
+ const memoryUsage = document.getElementById('memory-usage');
1728
+ const fpsDisplay = document.getElementById('fps-display');
1729
+ const latencyDisplay = document.getElementById('latency-display');
1730
+ const inferenceTime = document.getElementById('inference-time');
1731
+
1732
+ if (cpuUsage) cpuUsage.textContent = Math.floor(Math.random() * 10 + 15) + '%';
1733
+ if (memoryUsage) memoryUsage.textContent = (Math.random() * 0.5 + 2.0).toFixed(1) + 'GB';
1734
+ if (fpsDisplay) fpsDisplay.textContent = (Math.random() * 2 + 29).toFixed(1);
1735
+ if (latencyDisplay) latencyDisplay.textContent = Math.floor(Math.random() * 10 + 8) + 'ms';
1736
+ if (inferenceTime) inferenceTime.textContent = Math.floor(Math.random() * 20 + 5) + 'ms';
1737
+ }
1738
+ }, 2000);
1739
+ }
1740
+
1741
+ // 顯示通知
1742
+ function showNotification(message, type = 'success', duration = 4000) {
1743
+ if (!notification) return;
1744
+
1745
+ notification.textContent = message;
1746
+ notification.className = `cyber-notification ${type}`;
1747
+ notification.style.display = 'block';
1748
+
1749
+ if (typeof anime !== 'undefined') {
1750
+ anime({
1751
+ targets: notification,
1752
+ opacity: [0, 1],
1753
+ translateX: [50, 0],
1754
+ duration: 500,
1755
+ easing: 'easeOutCubic'
1756
+ });
1757
+
1758
+ setTimeout(() => {
1759
+ anime({
1760
+ targets: notification,
1761
+ opacity: 0,
1762
+ translateX: 50,
1763
+ duration: 500,
1764
+ complete: () => {
1765
+ notification.style.display = 'none';
1766
+ }
1767
+ });
1768
+ }, duration);
1769
+ } else {
1770
+ setTimeout(() => {
1771
+ notification.style.display = 'none';
1772
+ }, duration);
1773
+ }
1774
  }
1775
+
1776
+ // 清理資源
1777
+ window.addEventListener('beforeunload', () => {
1778
+ if (animationId) {
1779
+ cancelAnimationFrame(animationId);
1780
+ }
1781
+ });
1782
  });
1783
  </script>
1784
  </body>
1785
+ </html>