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

修復前端和後端資料傳輸問題 - 改善錯誤處理和欄位名稱一致性

Browse files
Files changed (5) hide show
  1. Dockerfile +8 -3
  2. README.md +119 -133
  3. app.py +67 -23
  4. requirements.txt +12 -12
  5. templates/index.html +345 -55
Dockerfile CHANGED
@@ -1,7 +1,10 @@
1
- FROM python:3.10-slim
2
 
3
  WORKDIR /app
4
 
 
 
 
5
  # 安裝系統依賴
6
  RUN apt-get update && apt-get install -y \
7
  libglib2.0-0 \
@@ -9,7 +12,8 @@ RUN apt-get update && apt-get install -y \
9
  libxext6 \
10
  libxrender-dev \
11
  libgomp1 \
12
- libglib2.0-0 \
 
13
  && rm -rf /var/lib/apt/lists/*
14
 
15
  # 複製依賴檔案
@@ -22,13 +26,14 @@ RUN pip install --no-cache-dir -r requirements.txt
22
  COPY . .
23
 
24
  # 建立必要目錄
25
- RUN mkdir -p uploads data/models data/features/keypoints
26
 
27
  # 暴露端口
28
  EXPOSE 7860
29
 
30
  # 設定環境變數
31
  ENV PYTHONUNBUFFERED=1
 
32
 
33
  # 啟動命令
34
  CMD ["python", "app.py"]
 
1
+ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
+ # 更新pip到最新版本
6
+ RUN pip install --upgrade pip
7
+
8
  # 安裝系統依賴
9
  RUN apt-get update && apt-get install -y \
10
  libglib2.0-0 \
 
12
  libxext6 \
13
  libxrender-dev \
14
  libgomp1 \
15
+ libgl1-mesa-glx \
16
+ libglib2.0-dev \
17
  && rm -rf /var/lib/apt/lists/*
18
 
19
  # 複製依賴檔案
 
26
  COPY . .
27
 
28
  # 建立必要目錄
29
+ RUN mkdir -p uploads data/models data/features/keypoints data/features/optical_flow
30
 
31
  # 暴露端口
32
  EXPOSE 7860
33
 
34
  # 設定環境變數
35
  ENV PYTHONUNBUFFERED=1
36
+ ENV PYTHONDONTWRITEBYTECODE=1
37
 
38
  # 啟動命令
39
  CMD ["python", "app.py"]
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: blue
5
  colorTo: green
6
  sdk: docker
7
  app_port: 7860
8
- pinned: boolean
9
  duplicated_from: XiaoBai1221/SignView
10
  ---
11
 
@@ -13,175 +13,161 @@ duplicated_from: XiaoBai1221/SignView
13
 
14
  一個整合的手語辨識系統,支援即時攝像頭辨識、影片上傳處理和 Facebook Messenger Bot 功能。使用 PyTorch 深度學習模型、MediaPipe 特徵提取和 OpenAI GPT 生成自然語句。
15
 
16
- ## 🚀 快速開始
17
 
18
- ### HuggingFace Spaces 部署 (推薦)
 
 
19
 
20
- 1. **Fork 此專案到你的 HuggingFace Spaces**
21
- 2. **設定環境變數**:
22
- ```
23
- OPENAI_API_KEY=你的OpenAI_API金鑰
24
- VERIFY_TOKEN=你的Messenger驗證Token
25
- PAGE_ACCESS_TOKEN=你的Facebook頁面存取Token
26
- ```
27
- 3. **自動部署** - HuggingFace 會自動建置和部署
28
 
29
- ### 本地開發
30
 
31
- ```bash
32
- # 1. 安裝依賴
33
- pip install -r requirements.txt
34
 
35
- # 2. 設定環境變數
36
- export OPENAI_API_KEY="你的OpenAI_API金鑰"
37
- export VERIFY_TOKEN="你的Messenger驗證Token"
38
- export PAGE_ACCESS_TOKEN="你的Facebook頁面存取Token"
39
 
40
- # 3. 啟動應用
41
- python3 app.py
42
- ```
 
 
 
43
 
44
- ## 📁 專案結構
 
 
 
45
 
46
- ```
47
- Sign-bot/
48
- ├── app.py # 🎯 主應用程式 (整合所有功能)
49
- ├── app_config.py # ⚙️ 配置管理
50
- ├── requirements.txt # 📦 Python依賴套件
51
- ├── Dockerfile # 🐳 Docker容器配置
52
- ├── README.md # 📖 專案文檔
53
- ├── final_review_gate.py # 🔍 測試腳本
54
- ├── data/ # 📊 資料目錄
55
- │ ├── models/ # 🤖 訓練好的模型檔案
56
- │ │ └── sign_language_model.pth
57
- │ ├── labels.csv # 🏷️ 標籤映射檔案
58
- │ └── features/ # 🎬 訓練特徵資料
59
- │ ├── keypoints/ # ✋ 關鍵點特徵檔案
60
- │ └── optical_flow/ # 🌊 光流特徵檔案
61
- ├── templates/ # 🌐 網頁範本
62
- │ └── index.html # 首頁範本
63
- └── uploads/ # 📁 暫時檔案上傳目錄
64
- ```
65
 
66
- ## 功能特色
 
 
 
 
 
 
67
 
68
- ### 🎯 **整合設計**
69
- - **統一入口**: 所有功能整合在 `app.py` 單一檔案
70
- - **環境適配**: 自動檢測本地/雲端環境並調整功能
71
- - **模組化**: 清晰的類別結構,易於維護
 
72
 
73
- ### 🤖 **AI 手語辨識**
74
- - **深度學習模型**: PyTorch LSTM + Attention 機制
75
- - **特徵提取**: MediaPipe 提取手部、姿態關鍵點
76
- - **自然語句生成**: OpenAI GPT-4o-mini 生成流暢句子
77
- - **支援手語**: 目前支援 eat, fish, like, want 四個手語
78
 
79
- ### 🌐 **多平台支援**
80
- - **Web 介面**: 即時攝像頭辨識 + 影片上傳處理
81
- - **Messenger Bot**: Facebook 整合,自動處理使用者影片
82
- - **RESTful API**: 提供第三方整合接口
83
- - **WebSocket**: 即時雙向通訊
84
 
85
- ### 📱 **使用方式**
 
 
 
86
 
87
- #### Web 介面 (本地環境)
88
- 1. 造訪 `http://localhost:7860`
89
- 2. 點擊「開始辨識」使用攝像頭
90
- 3. 或上傳 MP4 影片檔案
91
 
92
- #### Messenger Bot
93
- 1. 找到你的 Facebook 頁面
94
- 2. 發送手語影片
95
- 3. 系統自動辨識並回傳結果
96
 
97
- #### API 呼叫
98
- ```bash
99
- # 上傳影片進行辨識
100
- curl -X POST http://localhost:7860/process_video \
101
- -F "video=@your_video.mp4" \
102
- -F "sender_id=test_user"
103
  ```
104
 
105
- ## 🔧 技術架構
106
 
107
- ### 核心類別
108
- - **FeatureExtractor**: MediaPipe 特徵提取器
109
- - **SignLanguageModel**: PyTorch LSTM 神經網絡
110
- - **VideoSignLanguageRecognizer**: 影片手語辨識器
111
- - **SignLanguageRecognizer**: 即時手語辨識器
112
 
113
- ### 技術棧
114
- - **後端**: Flask + SocketIO
115
- - **AI框架**: PyTorch + MediaPipe
116
- - **自然語言**: OpenAI GPT-4o-mini
117
- - **前端**: HTML5 + WebSocket
118
- - **部署**: HuggingFace Spaces + Docker
119
 
120
- ## 🌍 環境變數
 
 
 
121
 
122
- | 變數名稱 | 說明 | 必須 |
123
- |---------|------|------|
124
- | `OPENAI_API_KEY` | OpenAI API 金鑰 | ✅ |
125
- | `VERIFY_TOKEN` | Messenger 驗證 Token | Messenger功能需要 |
126
- | `PAGE_ACCESS_TOKEN` | Facebook 頁面存取 Token | Messenger功能需要 |
127
- | `SPACE_ID` | HuggingFace Space ID | 自動設定 |
128
- | `PORT` | 服務埠號 | 預設 7860 |
129
 
130
- ## 🎮 API 端點
 
 
 
131
 
132
- ### Web 路由
133
- - `GET /` - 主頁面
134
- - `GET /health` - 健康檢查
135
- - `POST /process_video` - 影片處理
 
 
136
 
137
- ### Messenger 整合
138
- - `GET /webhook` - Webhook 驗證
139
- - `POST /webhook` - 訊息處理
 
140
 
141
- ### WebSocket 事件
142
- - `start_stream` - 開始視頻流
143
- - `stop_stream` - 停止視頻流
144
 
145
- ## 🚀 部署指南
146
 
147
- ### HuggingFace Spaces
148
- 1. 建立新的 Space (Gradio/Docker)
149
- 2. 上傳所有檔案
150
- 3. 設定環境變數
151
- 4. 自動部署完成
 
152
 
153
- ### Docker 部署
154
- ```bash
155
- # 建置映像
156
- docker build -t sign-language-recognition .
157
 
158
- # 執行容器
159
- docker run -p 7860:7860 \
160
- -e OPENAI_API_KEY="你的金鑰" \
161
- sign-language-recognition
162
- ```
163
 
164
- ## 🎯 使用限制
 
 
 
165
 
166
- - **模型準確度**: 目前為測試版本,準確度可能有限
167
- - **支援手語**: 僅支援 4 個基礎手語詞彙
168
- - **攝像頭功能**: 雲端環境不支援,請使用影片上傳
169
- - **檔案大小**: 影片檔案限制 100MB
170
 
171
- ## 🔄 未來規劃
 
 
 
172
 
173
- - [ ] 增加更多手語詞彙支援
174
- - [ ] 提升模型準確度
175
- - [ ] 支援手語語法結構
176
- - [ ] 加入使用者自訓練功能
177
- - [ ] 支援多語言介面
178
 
179
  ## 📞 技術支援
180
 
181
- 如有問題請透過以下方式聯絡:
182
- - GitHub Issues
183
- - 或直接在 HuggingFace Space 留言
184
 
185
  ---
186
 
187
- > **🎉 這是一個整合型手語辨識系統,將所有功能統一整合在 `app.py` 中,提供最佳的使用體驗和部署便利性!**
 
5
  colorTo: green
6
  sdk: docker
7
  app_port: 7860
8
+ pinned: false
9
  duplicated_from: XiaoBai1221/SignView
10
  ---
11
 
 
13
 
14
  一個整合的手語辨識系統,支援即時攝像頭辨識、影片上傳處理和 Facebook Messenger Bot 功能。使用 PyTorch 深度學習模型、MediaPipe 特徵提取和 OpenAI GPT 生成自然語句。
15
 
16
+ ## 🌟 主要功能
17
 
18
+ ### 🖥️ 本地環境
19
+ - **即時攝像頭辨識**: 使用WebSocket進行即時手語辨識
20
+ - **完整功能**: 支援所有功能包括攝像頭、影片上傳、Messenger Bot
21
 
22
+ ### ☁️ HuggingFace Spaces (雲端環境)
23
+ - **影片上傳辨識**: 拖拽或選擇影片檔案進行辨識
24
+ - **智慧環境檢測**: 自動偵測執行環境並切換適合的功能
25
+ - **Facebook Messenger Bot**: 支援Webhook接收訊息
 
 
 
 
26
 
27
+ ## 🚀 快速開始
28
 
29
+ ### HuggingFace Spaces 部署
 
 
30
 
31
+ 1. **訪問 Space**: https://huggingface.co/spaces/XiaoBai1221/SignView
 
 
 
32
 
33
+ 2. **設定環境變數** (在 Space Settings 中):
34
+ ```
35
+ OPENAI_API_KEY=your_openai_api_key_here
36
+ VERIFY_TOKEN=your_messenger_verify_token
37
+ PAGE_ACCESS_TOKEN=your_facebook_page_access_token
38
+ ```
39
 
40
+ 3. **Facebook Messenger Bot Webhook 設定**:
41
+ - **Webhook URL**: `https://xiaobai1221-signview.hf.space/webhook`
42
+ - **驗證Token**: 使用你設定的 `VERIFY_TOKEN`
43
+ - **訂閱事件**: `messages`, `messaging_postbacks`
44
 
45
+ ### 📱 Facebook Messenger Bot 設定步驟
46
+
47
+ #### 1. 創建 Facebook 應用程式
48
+ 1. 前往 [Facebook Developers](https://developers.facebook.com/)
49
+ 2. 創建新應用程式,選擇「商業」類型
50
+ 3. 添加「Messenger」產品
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ #### 2. 設定 Webhook
53
+ 1. 在 Messenger 設定中,找到「Webhooks」
54
+ 2. 點擊「設定 Webhooks」
55
+ 3. 填入以下資訊:
56
+ - **回調 URL**: `https://xiaobai1221-signview.hf.space/webhook`
57
+ - **驗證權杖**: 你的自訂驗證token (設為環境變數 `VERIFY_TOKEN`)
58
+ - **訂閱欄位**: 勾選 `messages` 和 `messaging_postbacks`
59
 
60
+ #### 3. 取得 Page Access Token
61
+ 1. Messenger 設定中,找到「存取權杖」
62
+ 2. 選擇你的 Facebook 粉絲專頁
63
+ 3. 複製產生的 Page Access Token
64
+ 4. 將此 token 設為環境變數 `PAGE_ACCESS_TOKEN`
65
 
66
+ #### 4. 測試 Webhook
67
+ 1. Webhook 設定中點擊「測試」
68
+ 2. 如果設定正確,應該會看到驗證成功的訊息
 
 
69
 
70
+ ### 🔧 本地開發
 
 
 
 
71
 
72
+ ```bash
73
+ # 克隆專案
74
+ git clone https://github.com/your-username/sign-bot.git
75
+ cd sign-bot
76
 
77
+ # 安裝依賴
78
+ pip install -r requirements.txt
 
 
79
 
80
+ # 設定環境變數
81
+ export OPENAI_API_KEY=your_openai_api_key
82
+ export VERIFY_TOKEN=your_verify_token
83
+ export PAGE_ACCESS_TOKEN=your_page_access_token
84
 
85
+ # 啟動應用
86
+ python app.py
 
 
 
 
87
  ```
88
 
89
+ ## 📊 支援的手語
90
 
91
+ 系統目前支援以下 4 種手語辨識:
92
+ - **eat** (吃)
93
+ - **fish** (魚)
94
+ - **like** (喜歡)
95
+ - **want** (想要)
96
 
97
+ ## 🛠️ 技術架構
 
 
 
 
 
98
 
99
+ ### 🧠 AI 模型
100
+ - **PyTorch LSTM + Attention**: 深度學習手語辨識模型
101
+ - **MediaPipe**: 手部關鍵點特徵提取
102
+ - **OpenAI GPT-4o-mini**: 自然語句生成
103
 
104
+ ### 🌐 Web 技術
105
+ - **Flask**: Web 框架
106
+ - **WebSocket**: 即時通訊 (本地環境)
107
+ - **Bootstrap**: 響應式 UI 設計
108
+ - **JavaScript**: 前端互動邏輯
 
 
109
 
110
+ ### 📱 整合服務
111
+ - **Facebook Messenger API**: 聊天機器人
112
+ - **HuggingFace Spaces**: 雲端部署平台
113
+ - **Docker**: 容器化部署
114
 
115
+ ## 🔄 Webhook 網址說明
116
+
117
+ ### HuggingFace Spaces 自動產生的網址格式:
118
+ ```
119
+ https://[username]-[space-name].hf.space/webhook
120
+ ```
121
 
122
+ ### 你的 Webhook 網址:
123
+ ```
124
+ https://xiaobai1221-signview.hf.space/webhook
125
+ ```
126
 
127
+ ### 驗證方式:
128
+ - GET 請求用於 Facebook 驗證
129
+ - POST 請求用於接收訊息
130
 
131
+ ## 📋 API 端點
132
 
133
+ - `GET /` - 主頁面 (Web 介面)
134
+ - `GET /health` - 健康檢查
135
+ - `GET /webhook` - Facebook Webhook 驗證
136
+ - `POST /webhook` - 接收 Facebook 訊息
137
+ - `POST /process_video` - 影片辨識處理
138
+ - `POST /receive_recognition_result` - 辨識結果接收
139
 
140
+ ## 🎯 使用方式
 
 
 
141
 
142
+ ### 💻 Web 介面
143
+ 1. 訪問 HuggingFace Space 網址
144
+ 2. 上傳手語影片檔案
145
+ 3. 點擊「開始辨識」
146
+ 4. 查看辨識結果和翻譯
147
 
148
+ ### 📱 Messenger Bot
149
+ 1. 在 Facebook 找到你的粉絲專頁
150
+ 2. 發送訊息或影片
151
+ 3. Bot 會自動辨識並回覆結果
152
 
153
+ ## 🔍 故障排除
 
 
 
154
 
155
+ ### Webhook 無法連接
156
+ 1. 確認 HuggingFace Space 狀態為 "Running"
157
+ 2. 檢查環境變數是否正確設定
158
+ 3. 確認 Webhook URL 格式正確
159
 
160
+ ### 辨識結果不準確
161
+ 1. 確保影片畫質清晰
162
+ 2. 手部動作要完整且明顯
163
+ 3. 光線充足,背景簡潔
 
164
 
165
  ## 📞 技術支援
166
 
167
+ 如有問題請聯繫:
168
+ - GitHub Issues: [專案頁面](https://github.com/your-username/sign-bot)
169
+ - Email: your-email@example.com
170
 
171
  ---
172
 
173
+ **© 2023 手語辨識整合系統 | 使用 Flask + PyTorch + OpenAI + HuggingFace Spaces**
app.py CHANGED
@@ -22,7 +22,13 @@ from flask_socketio import SocketIO, emit
22
  from openai import OpenAI
23
 
24
  # 環境變數設定
25
- os.environ.setdefault("OPENAI_API_KEY", "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")
 
 
 
 
 
 
26
 
27
  # 環境檢測
28
  IS_HUGGINGFACE = os.environ.get('SPACE_ID') is not None
@@ -384,8 +390,9 @@ class VideoSignLanguageRecognizer:
384
 
385
  def _predict_from_sequence(self, keypoints_sequence):
386
  """從關鍵點序列進行預測"""
387
- # 簡化版預測 - 直接使用整個序列
388
- sequence_tensor = torch.FloatTensor(keypoints_sequence).unsqueeze(0).to(self.device)
 
389
 
390
  with torch.no_grad():
391
  outputs = self.model(sequence_tensor)
@@ -412,18 +419,28 @@ class VideoSignLanguageRecognizer:
412
  return " ".join(word_sequence)
413
 
414
  try:
415
- prompt = f"我使用手語表達了以下單詞序列: {', '.join(word_sequence)}。請將這些單詞組織成一個有意義、通順的完整句子。"
 
416
 
417
  response = self.openai_client.chat.completions.create(
418
  model="gpt-4o-mini",
419
  messages=[
420
- {"role": "system", "content": "你是一個專業的手語翻譯助手。"},
421
  {"role": "user", "content": prompt}
422
  ],
423
- max_tokens=100
 
424
  )
425
 
426
- return response.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
427
 
428
  except Exception as e:
429
  print(f"調用GPT API時出錯: {e}")
@@ -622,18 +639,29 @@ class SignLanguageRecognizer:
622
  return
623
 
624
  try:
625
- prompt = f"我使用手語表達了以下單詞序列: {', '.join(self.word_sequence)}。請將這些單詞組織成一個有意義、通順的完整句子。"
 
626
 
627
  response = self.openai_client.chat.completions.create(
628
  model="gpt-4o-mini",
629
  messages=[
630
- {"role": "system", "content": "你是一個專業的手語翻譯助手。"},
631
  {"role": "user", "content": prompt}
632
  ],
633
- max_tokens=100
 
634
  )
635
 
636
- self.generated_sentence = response.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
 
637
  self.display_sentence_time = time.time()
638
  print(f"GPT生成句子: {self.generated_sentence}")
639
 
@@ -671,8 +699,9 @@ class SignLanguageRecognizer:
671
  if len(self.keypoints_buffer) < 2:
672
  return
673
 
674
- keypoints_array = np.array(list(self.keypoints_buffer))
675
- keypoints_tensor = torch.FloatTensor(keypoints_array).unsqueeze(0).to(self.device)
 
676
 
677
  with torch.no_grad():
678
  outputs = self.model(keypoints_tensor)
@@ -829,13 +858,16 @@ def process_video():
829
  if video_file.filename == '':
830
  return jsonify({"status": "error", "message": "沒有選擇檔案"}), 400
831
 
832
- # 儲存檔案
 
833
  filename = secure_filename(video_file.filename)
834
  timestamp = int(time.time())
835
- filename = f"{timestamp}_{sender_id}_{filename}"
836
- video_path = os.path.join(UPLOAD_FOLDER, filename)
837
 
838
- video_file.save(video_path)
 
 
 
 
839
  print(f"📁 影片已儲存:{video_path}")
840
 
841
  # 初始化影片辨識器
@@ -843,6 +875,12 @@ def process_video():
843
  print(f"🔍 模型路徑: {model_path}")
844
  print(f"🔍 模型檔案是否存在: {os.path.exists(model_path)}")
845
 
 
 
 
 
 
 
846
  video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
847
 
848
  # 處理影片
@@ -874,7 +912,9 @@ def process_video():
874
 
875
  except Exception as e:
876
  print(f"處理影片時發生錯誤:{e}")
877
- return jsonify({"status": "error", "message": str(e)}), 500
 
 
878
 
879
  #--------------------
880
  # Messenger Bot 輔助函數
@@ -944,6 +984,8 @@ def send_message(recipient_id, message_text):
944
 
945
  def process_messenger_video(video_url, sender_id):
946
  """處理來自 Messenger 的影片(HuggingFace 整合版本)"""
 
 
947
  try:
948
  print(f"🎬 開始處理 Messenger 影片:{video_url}")
949
 
@@ -951,16 +993,18 @@ def process_messenger_video(video_url, sender_id):
951
  response = requests.get(video_url, stream=True, timeout=30)
952
  response.raise_for_status()
953
 
954
- # 生成檔案名稱
955
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
956
  filename = f"messenger_video_{sender_id}_{timestamp}.mp4"
957
- file_path = os.path.join(UPLOAD_FOLDER, filename)
958
 
959
- # 寫入檔案
960
- with open(file_path, 'wb') as f:
 
 
 
961
  for chunk in response.iter_content(chunk_size=8192):
962
  if chunk:
963
- f.write(chunk)
964
 
965
  print(f"✅ 影片下載完成:{file_path}")
966
 
 
22
  from openai import OpenAI
23
 
24
  # 環境變數設定
25
+ # OpenAI API KEY 應該從環境變數獲取,不要硬編碼
26
+ # 請在 HuggingFace Spaces 設定中添加 OPENAI_API_KEY 環境變數
27
+
28
+ # 設定環境變數避免權限問題和減少日誌
29
+ os.environ['MPLCONFIGDIR'] = '/tmp/matplotlib'
30
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # 減少TensorFlow日誌
31
+ os.environ['MEDIAPIPE_DISABLE_GPU'] = '1' # 禁用GPU避免警告
32
 
33
  # 環境檢測
34
  IS_HUGGINGFACE = os.environ.get('SPACE_ID') is not None
 
390
 
391
  def _predict_from_sequence(self, keypoints_sequence):
392
  """從關鍵點序列進行預測"""
393
+ # 優化tensor創建避免效能警告
394
+ keypoints_array = np.array(keypoints_sequence, dtype=np.float32)
395
+ sequence_tensor = torch.from_numpy(keypoints_array).unsqueeze(0).to(self.device)
396
 
397
  with torch.no_grad():
398
  outputs = self.model(sequence_tensor)
 
419
  return " ".join(word_sequence)
420
 
421
  try:
422
+ # 優化prompt,要求GPT只回覆簡潔句子
423
+ prompt = f"手語詞彙: {', '.join(word_sequence)}。請組成一個簡潔的中文句子,只回覆句子內容,不要額外說明。"
424
 
425
  response = self.openai_client.chat.completions.create(
426
  model="gpt-4o-mini",
427
  messages=[
428
+ {"role": "system", "content": "你是手語翻譯助手。只回覆簡潔的中文句子,不要額外說明或範例。"},
429
  {"role": "user", "content": prompt}
430
  ],
431
+ max_tokens=50, # 減少token數量
432
+ temperature=0.3 # 降低隨機性,更準確
433
  )
434
 
435
+ result = response.choices[0].message.content.strip()
436
+ # 移除可能的引號和額外文字
437
+ result = result.replace('"', '').replace("'", '').strip()
438
+
439
+ # 如果結果太長或包含解釋性文字,回退到原詞彙
440
+ if len(result) > 30 or '例如' in result or '可以' in result:
441
+ return " ".join(word_sequence)
442
+
443
+ return result
444
 
445
  except Exception as e:
446
  print(f"調用GPT API時出錯: {e}")
 
639
  return
640
 
641
  try:
642
+ # 優化prompt,要求GPT只回覆簡潔句子
643
+ prompt = f"手語詞彙: {', '.join(self.word_sequence)}。請組成一個簡潔的中文句子,只回覆句子內容,不要額外說明。"
644
 
645
  response = self.openai_client.chat.completions.create(
646
  model="gpt-4o-mini",
647
  messages=[
648
+ {"role": "system", "content": "你是手語翻譯助手。只回覆簡潔的中文句子,不要額外說明或範例。"},
649
  {"role": "user", "content": prompt}
650
  ],
651
+ max_tokens=50, # 減少token數量
652
+ temperature=0.3 # 降低隨機性,更準確
653
  )
654
 
655
+ result = response.choices[0].message.content.strip()
656
+ # 移除可能的引號和額外文字
657
+ result = result.replace('"', '').replace("'", '').strip()
658
+
659
+ # 如果結果太長或包含解釋性文字,回退到原詞彙
660
+ if len(result) > 30 or '例如' in result or '可以' in result:
661
+ self.generated_sentence = " ".join(self.word_sequence)
662
+ else:
663
+ self.generated_sentence = result
664
+
665
  self.display_sentence_time = time.time()
666
  print(f"GPT生成句子: {self.generated_sentence}")
667
 
 
699
  if len(self.keypoints_buffer) < 2:
700
  return
701
 
702
+ # 優化tensor創建避免效能警告
703
+ keypoints_array = np.array(list(self.keypoints_buffer), dtype=np.float32)
704
+ keypoints_tensor = torch.from_numpy(keypoints_array).unsqueeze(0).to(self.device)
705
 
706
  with torch.no_grad():
707
  outputs = self.model(keypoints_tensor)
 
858
  if video_file.filename == '':
859
  return jsonify({"status": "error", "message": "沒有選擇檔案"}), 400
860
 
861
+ # 使用臨時檔案避免權限問題
862
+ import tempfile
863
  filename = secure_filename(video_file.filename)
864
  timestamp = int(time.time())
 
 
865
 
866
+ # 創建臨時檔案
867
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4', prefix=f'upload_{sender_id}_') as temp_file:
868
+ video_path = temp_file.name
869
+ video_file.save(video_path)
870
+
871
  print(f"📁 影片已儲存:{video_path}")
872
 
873
  # 初始化影片辨識器
 
875
  print(f"🔍 模型路徑: {model_path}")
876
  print(f"🔍 模型檔案是否存在: {os.path.exists(model_path)}")
877
 
878
+ if not os.path.exists(model_path):
879
+ return jsonify({
880
+ "status": "error",
881
+ "message": f"模型檔案不存在: {model_path}"
882
+ }), 500
883
+
884
  video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
885
 
886
  # 處理影片
 
912
 
913
  except Exception as e:
914
  print(f"處理影片時發生錯誤:{e}")
915
+ import traceback
916
+ traceback.print_exc() # 印出完整的錯誤堆疊
917
+ return jsonify({"status": "error", "message": f"處理影片時發生錯誤: {str(e)}"}), 500
918
 
919
  #--------------------
920
  # Messenger Bot 輔助函數
 
984
 
985
  def process_messenger_video(video_url, sender_id):
986
  """處理來自 Messenger 的影片(HuggingFace 整合版本)"""
987
+ import tempfile
988
+
989
  try:
990
  print(f"🎬 開始處理 Messenger 影片:{video_url}")
991
 
 
993
  response = requests.get(video_url, stream=True, timeout=30)
994
  response.raise_for_status()
995
 
996
+ # 使用臨時檔案避免權限問題
997
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
998
  filename = f"messenger_video_{sender_id}_{timestamp}.mp4"
 
999
 
1000
+ # 創建臨時檔案
1001
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4', prefix=f'messenger_{sender_id}_') as temp_file:
1002
+ file_path = temp_file.name
1003
+
1004
+ # 寫入檔案
1005
  for chunk in response.iter_content(chunk_size=8192):
1006
  if chunk:
1007
+ temp_file.write(chunk)
1008
 
1009
  print(f"✅ 影片下載完成:{file_path}")
1010
 
requirements.txt CHANGED
@@ -1,12 +1,12 @@
1
- flask==2.3.2
2
- flask-socketio==5.3.4
3
- opencv-python==4.8.0.74
4
- numpy==1.24.3
5
- pandas==2.0.3
6
- torch==2.0.1
7
- mediapipe==0.10.1
8
- openai==1.6.1
9
- requests==2.31.0
10
- werkzeug==2.3.6
11
- python-dotenv==1.0.0
12
- gunicorn==21.2.0
 
1
+ flask>=2.3.0,<3.1.0
2
+ flask-socketio>=5.3.0,<6.0.0
3
+ opencv-python-headless>=4.8.0,<5.0.0
4
+ numpy>=1.21.0,<2.0.0
5
+ pandas>=1.5.0,<3.0.0
6
+ torch>=2.0.0,<2.2.0
7
+ mediapipe>=0.10.5
8
+ openai>=1.0.0,<2.0.0
9
+ requests>=2.25.0,<3.0.0
10
+ werkzeug>=2.3.0,<4.0.0
11
+ python-dotenv>=0.19.0
12
+ gunicorn>=20.0.0
templates/index.html CHANGED
@@ -43,6 +43,13 @@
43
  margin-bottom: 20px;
44
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
45
  }
 
 
 
 
 
 
 
46
  .btn-primary {
47
  background-color: #4aa3df;
48
  border: none;
@@ -51,12 +58,19 @@
51
  background-color: #e74c3c;
52
  border: none;
53
  }
 
 
 
 
54
  .btn-primary:hover {
55
  background-color: #3498db;
56
  }
57
  .btn-danger:hover {
58
  background-color: #c0392b;
59
  }
 
 
 
60
  .panel-title {
61
  color: #4aa3df;
62
  border-bottom: 1px solid #3a3a3a;
@@ -143,35 +157,114 @@
143
  color: #7f8c8d;
144
  font-size: 0.9rem;
145
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  </style>
147
  </head>
148
  <body>
149
  <div class="container">
150
- <h1 class="main-title">手語辨識系統</h1>
 
 
 
 
 
151
 
152
  <div class="row">
153
  <div class="col-lg-8">
154
- <div class="video-container">
155
- <div id="hand-status" class="camera-status bg-secondary">未偵測</div>
156
- <img id="video-display" src="" alt="即時視頻畫面">
157
- </div>
158
-
159
- <div class="control-panel">
160
- <h4 class="panel-title">控制面板</h4>
161
- <div class="d-flex justify-content-between">
162
- <button id="start-btn" class="btn btn-primary">開始辨識</button>
163
- <button id="stop-btn" class="btn btn-danger" disabled>停止辨識</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  </div>
165
  </div>
166
 
167
- <div class="word-sequence">
168
- <h4 class="panel-title">單詞序列</h4>
169
- <div id="word-sequence-display" class="fs-5">尚無偵測結果</div>
170
- </div>
171
-
172
- <div class="sentence-result">
173
- <h4 class="panel-title">翻譯結果</h4>
174
- <div id="sentence-display" class="fs-5">等待手語輸入完成...</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  </div>
176
  </div>
177
 
@@ -194,6 +287,22 @@
194
 
195
  <script>
196
  document.addEventListener('DOMContentLoaded', function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  // 獲取DOM元素
198
  const videoDisplay = document.getElementById('video-display');
199
  const startBtn = document.getElementById('start-btn');
@@ -205,50 +314,231 @@
205
  const sentenceDisplay = document.getElementById('sentence-display');
206
  const handStatus = document.getElementById('hand-status');
207
 
208
- // 連接Socket.IO
209
- const socket = io();
 
 
 
 
 
 
 
210
 
211
- // 連接事件
212
- socket.on('connect', function() {
213
- console.log('已連接到伺服器');
214
- });
 
215
 
216
- // 接收幀更新
217
- socket.on('update_frame', function(data) {
218
- // 更新視頻顯示
219
- videoDisplay.src = `data:image/jpeg;base64,${data.image}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
- // 更新狀態
222
- updateStatus(data.status);
223
- });
 
 
 
 
 
 
 
 
 
 
 
224
 
225
- // 開始按鈕點擊事件
226
- startBtn.addEventListener('click', function() {
227
- socket.emit('start_stream', {}, function(response) {
228
- if (response.status === 'success') {
229
- startBtn.disabled = true;
230
- stopBtn.disabled = false;
231
- resultLabel.textContent = '等待偵測...';
232
- resultConfidence.textContent = '信心度: 0%';
233
- } else {
234
- alert('啟動失敗: ' + (response.message || '未知錯誤'));
 
 
 
 
 
 
 
 
 
235
  }
236
  });
237
- });
238
-
239
- // 停止按鈕點擊事件
240
- stopBtn.addEventListener('click', function() {
241
- socket.emit('stop_stream', {}, function(response) {
242
- if (response.status === 'success') {
243
- startBtn.disabled = false;
244
- stopBtn.disabled = true;
245
- resultLabel.textContent = '未開始';
246
- resultConfidence.textContent = '信心度: 0%';
247
- handStatus.textContent = '未偵測';
248
- handStatus.className = 'camera-status bg-secondary';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  });
251
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
  // 更新所有狀態顯示
254
  function updateStatus(status) {
 
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;
 
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;
 
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
 
 
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');
 
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) {