XiaoBai1221 commited on
Commit
cc931c8
·
1 Parent(s): 62d60e0

最終修復

Browse files
Files changed (3) hide show
  1. .cursor/rules/reviewgate.mdc +0 -131
  2. app.py +47 -32
  3. templates/index.html +35 -11
.cursor/rules/reviewgate.mdc DELETED
@@ -1,131 +0,0 @@
1
- ---
2
- description:
3
- globs:
4
- alwaysApply: true
5
- ---
6
- ---
7
- description:
8
- globs:
9
- alwaysApply: true
10
- ---
11
- # AI Agent Protocol: Self-Contained Mandatory Interactive Final Review Loop with Auto-Script Creation (Version 3)
12
-
13
- ## 0. User Advisory & Safety:
14
- This is an advanced, experimental rule. It instructs the AI Agent to check for, potentially create, and then execute a Python script (`final_review_gate.py`) in your project's root directory to facilitate an interactive review loop. By using this rule, you acknowledge that the AI will attempt file system operations (read, write, execute) as directed. Ensure the AI's execution environment has appropriate permissions if you expect script auto-creation to work, and be aware of the security implications.
15
-
16
- ## 1. Core Principle & Context:
17
- Your primary task processing includes a **mandatory final interactive review stage** driven by user input through a specific Python script (`final_review_gate.py`). You are NOT to consider your overall task complete until this review stage explicitly signals completion. This protocol leverages your known capability to run commands in a sub-terminal (displayed within the chat interface) and read their output.
18
-
19
- ## Phase 0: Ensure `final_review_gate.py` Script Exists
20
- (This phase is executed ONCE per user request that triggers this overall protocol, or if the script is missing or its content is incorrect.)
21
-
22
- 1. **Define Script Details:**
23
- * **Script Name:** `final_review_gate.py`
24
- * **Target Location:** Directly in the root of the current project/workspace.
25
- * **Python Script Content (ensure this exact content is used):**
26
- ```python
27
- # final_review_gate.py
28
- import sys
29
- import os
30
-
31
- if __name__ == "__main__":
32
- # Try to make stdout unbuffered for more responsive interaction.
33
- # This might not work on all platforms or if stdout is not a TTY,
34
- # but it's a good practice for this kind of interactive script.
35
- try:
36
- sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
37
- except Exception:
38
- pass # Ignore if unbuffering fails, e.g., in certain environments
39
-
40
- try:
41
- sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', buffering=1)
42
- except Exception:
43
- pass # Ignore
44
-
45
- print("--- FINAL REVIEW GATE ACTIVE ---", flush=True)
46
- print("AI has completed its primary actions. Awaiting your review or further sub-prompts.", flush=True)
47
- print("Type your sub-prompt, or one of: 'TASK_COMPLETE', 'Done', 'Quit', 'q' to signal completion.", flush=True) # MODIFIED
48
-
49
- active_session = True
50
- while active_session:
51
- try:
52
- # Signal that the script is ready for input.
53
- # The AI doesn't need to parse this, but it's good for user visibility.
54
- print("REVIEW_GATE_AWAITING_INPUT:", end="", flush=True)
55
-
56
- line = sys.stdin.readline()
57
-
58
- if not line: # EOF
59
- print("--- REVIEW GATE: STDIN CLOSED (EOF), EXITING SCRIPT ---", flush=True)
60
- active_session = False
61
- break
62
-
63
- user_input = line.strip()
64
-
65
- # Check for exit conditions
66
- if user_input.upper() in ['TASK_COMPLETE', 'DONE', 'QUIT', 'Q']: # MODIFIED: Empty string no longer exits
67
- print(f"--- REVIEW GATE: USER SIGNALED COMPLETION WITH '{user_input.upper()}' ---", flush=True)
68
- active_session = False
69
- break
70
- elif user_input: # If there's any other non-empty input (and not a completion command)
71
- # This is the critical line the AI will "listen" for.
72
- print(f"USER_REVIEW_SUB_PROMPT: {user_input}", flush=True)
73
- # If user_input was empty (and not a completion command),
74
- # the loop simply continues, and "REVIEW_GATE_AWAITING_INPUT:" will be printed again.
75
-
76
- except KeyboardInterrupt:
77
- print("--- REVIEW GATE: SESSION INTERRUPTED BY USER (KeyboardInterrupt) ---", flush=True)
78
- active_session = False
79
- break
80
- except Exception as e:
81
- print(f"--- REVIEW GATE SCRIPT ERROR: {e} ---", flush=True)
82
- active_session = False
83
- break
84
-
85
- print("--- FINAL REVIEW GATE SCRIPT EXITED ---", flush=True)
86
- ```
87
-
88
- 2. **Ensure Script Existence and Correctness:**
89
- a. Use your file system tools to check if the file `final_review_gate.py` exists in the project root.
90
- b. **If the file `final_review_gate.py` does NOT exist OR if its content does not EXACTLY match the Python script provided in section "Phase 0.1.Python Script Content" (you may need to read the existing file and compare):**
91
- i. You MUST create or overwrite the `final_review_gate.py` file in the project root.
92
- ii. The content of this file MUST be *exactly* the Python script content provided above. Ensure all indentation, comments, and lines are preserved perfectly. Use your "create file" or "edit file" tool.
93
- iii. After creating/overwriting it, announce: "The `final_review_gate.py` script has been created/updated in the project root with the latest interaction enhancements."
94
- c. **If the file `final_review_gate.py` already exists and its content is correct:** You may skip announcing its creation.
95
- d. If you encounter any errors checking for, reading, or creating/writing the file (e.g., permission issues), you MUST report this to the user. State clearly: "Error managing `final_review_gate.py`: [specific error]. Cannot proceed with interactive review." and then conclude the interaction for the main task as best as you can without this interactive step.
96
-
97
- ## Phase 1: Primary Task Execution
98
- 1. Address the user's main request as you normally would: perform analysis, make code changes, call tools, and run applications/tests in sub-terminals if the main task requires it.
99
- * **Application Execution Confirmation:** Before attempting to run an entire application (e.g., a web server, a full front-end build, a long-running background process), you MUST ask the user for explicit confirmation. For example: "I am about to run the [application name/command]. Is it okay to proceed, or is it possibly already running?"
100
- * This confirmation is NOT required for running individual test scripts, linters, build tools for specific components, or other non-application-level commands necessary for task completion.
101
- * If the user indicates an application is already running or denies permission, adapt your strategy accordingly (e.g., focus on tests or specific file executions).
102
- 2. Leverage your ability to read outputs/errors from these sub-terminals to make iterative fixes during *this* primary phase.
103
-
104
- ## Phase 2: Mandatory Final Interactive Review Script Execution
105
- 1. **Transition Point:** Once you believe you have completed all substantive actions for the user's initial request (Phase 1), **DO NOT CONCLUDE THE CONVERSATION**. You MUST now transition to this Final Interactive Review phase.
106
- 2. **Action:** Execute the `final_review_gate.py` script (which you ensured exists and is correct in Phase 0) from the project root.
107
- * Determine the correct Python interpreter command (e.g., `python`, `python3`) available in the execution environment. A robust command attempt would be `python3 ./final_review_gate.py` and if that fails, try `python ./final_review_gate.py`.
108
- * **Crucially, the terminal for this script MUST run directly within the chat window, and its output MUST be continuously visible to the user as it is generated.** It must NOT pop open as a separate window or run hidden in the background. You MUST wait for the script (and its terminal interaction) to fully complete and exit before you proceed with any further actions or attempt to conclude the overall task. Your processing will be blocked by this script's execution.
109
- 3. **Announcement:** Clearly inform the user: "I have completed the primary actions. Now initiating the mandatory Final Interactive Review by running `./final_review_gate.py`. The review terminal is now active **in this chat window, and its output will be displayed live**. Please provide any sub-prompts directly into that script's input area, or type 'TASK_COMPLETE', 'Done', 'Quit', or 'q' when satisfied." (MODIFIED: Removed "or simply press Enter")
110
-
111
- ## Phase 3: Interactive Review Loop (Monitoring Script Output)
112
- 1. **Active Monitoring & Display:** Continuously monitor the standard output (stdout) of the launched `final_review_gate.py` script **as it appears live in the chat window**. Ensure the user sees all script output, including the `REVIEW_GATE_AWAITING_INPUT:` prompt. You will "read" its output using the same capability that allows you to read terminal logs or error messages. The script will loop and re-prompt if an empty input is given.
113
- 2. **User Sub-Prompt Detection:** When the script's stdout prints a line formatted EXACTLY as:
114
- `USER_REVIEW_SUB_PROMPT: <user's sub-prompt text>`
115
- You MUST interpret `<user's sub-prompt text>` as a new, direct, actionable instruction from the user.
116
- 3. **Processing Sub-Prompts:**
117
- a. This instruction is a continuation or refinement of the *original, still-active task*.
118
- b. Analyze the sub-prompt. Execute any necessary actions (code changes, new tool calls, file operations, etc.). **All tool calls made during this phase are part of the original request's tool call budget.** If the sub-prompt requests running an entire application, the confirmation principle from "Phase 1, Step 1 (Application Execution Confirmation)" applies.
119
- c. Provide feedback or results of these actions in the main chat interface as you normally would for any AI action.
120
- d. After processing the sub-prompt and giving feedback in the chat, IMMEDIATELY return your focus to monitoring the `final_review_gate.py` script's terminal output (which remains live and visible in the chat) for the next user instruction or completion signal. This loop is critical.
121
- 4. **Completion Signal Detection:** The interactive review loop continues until the script's stdout (visible in the chat window) prints a line containing:
122
- * `--- REVIEW GATE: USER SIGNALED COMPLETION WITH 'TASK_COMPLETE' ---` (or 'DONE', 'QUIT', 'Q' as per script logic)
123
- * OR `--- FINAL REVIEW GATE SCRIPT EXITED ---` (or any other script exit/error message like `REVIEW GATE: STDIN CLOSED` or `REVIEW GATE SCRIPT ERROR:`)
124
- (MODIFIED: Removed specific "EMPTY INPUT RECEIVED" as a completion signal, as the script no longer behaves this way for completion.)
125
-
126
- ## Phase 4: True Task Conclusion
127
- 1. Only after the `final_review_gate.py` script has terminated (as observed by its terminal session in the chat window closing and its final exit messages being printed and visible) are you permitted to consider the user's original request fully satisfied.
128
- 2. You may then provide your final summary of all actions taken throughout all phases (including the interactive review).
129
-
130
- ## Overriding Default Behavior:
131
- This entire "Final Interactive Review" process (Phases 0, 2, 3, and 4) is a **strict, non-negotiable requirement** that overrides any default tendency you have to end the conversation after completing Phase 1. The task is only finished when the user explicitly confirms with one of the specified keywords through the review script or the script otherwise terminates due to an error or EOF (as per the defined behavior, including the in-chat, blocking, and continuously visible terminal execution). Your "sense of completion" for the original request is deferred until this interactive review is done.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -352,7 +352,7 @@ class VideoSignLanguageRecognizer:
352
  return None, 0
353
 
354
  # 進行預測
355
- prediction, confidence, word_sequence = self._predict_from_sequence(keypoints_sequence)
356
 
357
  # 使用GPT生成完整句子
358
  generated_sentence = self._generate_sentence_with_gpt(word_sequence)
@@ -360,7 +360,13 @@ class VideoSignLanguageRecognizer:
360
  print(f"🎯 辨識結果:{word_sequence}")
361
  print(f"📈 信心度:{confidence:.2f}")
362
 
363
- return word_sequence, generated_sentence, confidence
 
 
 
 
 
 
364
 
365
  def _extract_features(self, frame):
366
  """從單一幀提取手部和姿勢特徵"""
@@ -402,13 +408,16 @@ class VideoSignLanguageRecognizer:
402
  predicted_class = predicted_class.item()
403
  confidence = max_prob.item()
404
 
 
 
 
405
  if confidence >= self.threshold:
406
  predicted_word = self.label_map.get(predicted_class, f"類別{predicted_class}")
407
  word_sequence = [predicted_word]
408
  else:
409
  word_sequence = []
410
 
411
- return predicted_class, confidence, word_sequence
412
 
413
  def _generate_sentence_with_gpt(self, word_sequence):
414
  """使用GPT根據單詞序列生成一個完整句子"""
@@ -884,7 +893,7 @@ def process_video():
884
  video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
885
 
886
  # 處理影片
887
- word_sequence, recognition_result, confidence = video_recognizer.process_video(video_path)
888
 
889
  # 清理臨時檔案
890
  try:
@@ -892,36 +901,37 @@ def process_video():
892
  except:
893
  pass
894
 
895
- if recognition_result is not None:
896
- # 如果是來自 Messenger 的請求,直接回傳結果給用戶
897
- if sender_id != 'unknown':
898
- send_message(sender_id, recognition_result)
 
 
 
899
 
900
- # 解析辨識結果,提供完整的前端所需資訊
901
- word_sequence = []
 
 
 
 
 
 
902
 
903
- # 嘗試從辨識結果中提取單詞序列(簡單的文字分割)
904
- if recognition_result and recognition_result != "無法辨識手語內容":
905
- # 如果結果包含多個詞,可以分割
906
- if isinstance(recognition_result, list):
907
- potential_words = recognition_result # 如果是列表,直接使用
908
- else:
909
- potential_words = recognition_result.split() # 如果是字符串,使用 split()
910
- if len(potential_words) <= 4: # 假設是單詞序列
911
- word_sequence = potential_words
912
- else:
913
- # 否則視為生成的句子
914
- word_sequence = [recognition_result.split()[0]] if recognition_result.split() else []
915
 
916
- # 使用 GPT 生成完整句子(而不是直接複製辨識結果)
917
- video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
918
- generated_sentence = video_recognizer._generate_sentence_with_gpt(word_sequence)
919
 
920
  return jsonify({
921
  "status": "success",
922
- "recognition_result": recognition_result,
923
- "confidence": float(confidence),
924
  "word_sequence": word_sequence,
 
 
925
  "generated_sentence": generated_sentence,
926
  "sender_id": sender_id
927
  })
@@ -1035,7 +1045,7 @@ def process_messenger_video(video_url, sender_id):
1035
  video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
1036
 
1037
  # 處理影片
1038
- word_sequence, recognition_result, confidence = video_recognizer.process_video(file_path)
1039
 
1040
  # 清理臨時檔案
1041
  try:
@@ -1043,13 +1053,18 @@ def process_messenger_video(video_url, sender_id):
1043
  except:
1044
  pass
1045
 
1046
- if recognition_result:
 
 
 
 
1047
  print(f"✅ 手語辨識完成 - 用戶:{sender_id}")
1048
- print(f"📝 辨識結果:{recognition_result}")
 
1049
  print(f"🎯 信心度:{confidence:.2f}")
1050
 
1051
- # 發送結果給用戶
1052
- send_message(sender_id, recognition_result)
1053
  else:
1054
  send_message(sender_id, "抱歉,無法辨識您的手語內容,請再試一次。")
1055
 
 
352
  return None, 0
353
 
354
  # 進行預測
355
+ prediction, confidence, word_sequence, probabilities = self._predict_from_sequence(keypoints_sequence)
356
 
357
  # 使用GPT生成完整句子
358
  generated_sentence = self._generate_sentence_with_gpt(word_sequence)
 
360
  print(f"🎯 辨識結果:{word_sequence}")
361
  print(f"📈 信心度:{confidence:.2f}")
362
 
363
+ return {
364
+ 'predicted_class': prediction,
365
+ 'word_sequence': word_sequence,
366
+ 'confidence': confidence,
367
+ 'probabilities': probabilities,
368
+ 'generated_sentence': generated_sentence
369
+ }
370
 
371
  def _extract_features(self, frame):
372
  """從單一幀提取手部和姿勢特徵"""
 
408
  predicted_class = predicted_class.item()
409
  confidence = max_prob.item()
410
 
411
+ # 提取所有類別的機率
412
+ probs = probabilities[0].cpu().numpy()
413
+
414
  if confidence >= self.threshold:
415
  predicted_word = self.label_map.get(predicted_class, f"類別{predicted_class}")
416
  word_sequence = [predicted_word]
417
  else:
418
  word_sequence = []
419
 
420
+ return predicted_class, confidence, word_sequence, probs
421
 
422
  def _generate_sentence_with_gpt(self, word_sequence):
423
  """使用GPT根據單詞序列生成一個完整句子"""
 
893
  video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
894
 
895
  # 處理影片
896
+ result = video_recognizer.process_video(video_path)
897
 
898
  # 清理臨時檔案
899
  try:
 
901
  except:
902
  pass
903
 
904
+ if result is not None:
905
+ # 提取結果數據
906
+ predicted_class = result.get('predicted_class', -1)
907
+ word_sequence = result.get('word_sequence', [])
908
+ confidence = result.get('confidence', 0.0)
909
+ probabilities = result.get('probabilities', [])
910
+ generated_sentence = result.get('generated_sentence', '無法辨識手語內容')
911
 
912
+ # 創建類別機率數據供前端使用
913
+ prob_data = []
914
+ if len(probabilities) > 0:
915
+ sorted_indices = np.argsort(probabilities)[::-1][:4] # 取前4個最高機率
916
+ for idx in sorted_indices:
917
+ prob = float(probabilities[idx])
918
+ class_label = video_recognizer.label_map.get(idx, f"類別{idx}")
919
+ prob_data.append({"label": class_label, "probability": prob})
920
 
921
+ # 獲取預測類別的標籤
922
+ predicted_label = video_recognizer.label_map.get(predicted_class, "未知") if predicted_class >= 0 else "未知"
 
 
 
 
 
 
 
 
 
 
923
 
924
+ # 如果是來自 Messenger 的請求,發送GPT生成的句子
925
+ if sender_id != 'unknown':
926
+ send_message(sender_id, generated_sentence)
927
 
928
  return jsonify({
929
  "status": "success",
930
+ "predicted_class": predicted_class,
931
+ "predicted_label": predicted_label,
932
  "word_sequence": word_sequence,
933
+ "confidence": float(confidence),
934
+ "probabilities": prob_data,
935
  "generated_sentence": generated_sentence,
936
  "sender_id": sender_id
937
  })
 
1045
  video_recognizer = VideoSignLanguageRecognizer(model_path, threshold=0.5)
1046
 
1047
  # 處理影片
1048
+ result = video_recognizer.process_video(file_path)
1049
 
1050
  # 清理臨時檔案
1051
  try:
 
1053
  except:
1054
  pass
1055
 
1056
+ if result:
1057
+ generated_sentence = result.get('generated_sentence', '無法辨識手語內容')
1058
+ confidence = result.get('confidence', 0.0)
1059
+ word_sequence = result.get('word_sequence', [])
1060
+
1061
  print(f"✅ 手語辨識完成 - 用戶:{sender_id}")
1062
+ print(f"📝 模型辨識:{word_sequence}")
1063
+ print(f"💬 GPT翻譯:{generated_sentence}")
1064
  print(f"🎯 信心度:{confidence:.2f}")
1065
 
1066
+ # 發送GPT翻譯結果給用戶
1067
+ send_message(sender_id, generated_sentence)
1068
  else:
1069
  send_message(sender_id, "抱歉,無法辨識您的手語內容,請再試一次。")
1070
 
templates/index.html CHANGED
@@ -1472,9 +1472,9 @@
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;
@@ -1491,25 +1491,46 @@
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
 
@@ -1524,6 +1545,9 @@
1524
  if (resultConfidence) resultConfidence.textContent = '信心度: 0%';
1525
  if (videoWordSequenceDisplay) videoWordSequenceDisplay.textContent = '分析失敗';
1526
  if (videoSentenceDisplay) videoSentenceDisplay.textContent = '分析失敗';
 
 
 
1527
  }
1528
  }
1529
 
 
1472
  // 完成進度
1473
  updateProgress(100, '神經網路分析完成!');
1474
 
1475
+ // 顯示當前預測 (模型預測的類別)
1476
+ const predictedLabel = result.predicted_label || '未知';
1477
+ if (resultLabel) resultLabel.textContent = predictedLabel;
1478
 
1479
  // 顯示信心度
1480
  const confidence = result.confidence || 0;
 
1491
  });
1492
  }
1493
 
1494
+ // 更新類別機率顯示
1495
+ if (result.probabilities && probabilitiesContainer) {
1496
+ probabilitiesContainer.innerHTML = '';
1497
+ result.probabilities.forEach(function(item) {
1498
+ const probContainer = document.createElement('div');
1499
+ probContainer.className = 'mb-3';
1500
+
1501
+ const probLabel = document.createElement('div');
1502
+ probLabel.className = 'prob-label';
1503
+ probLabel.innerHTML = `<span>${item.label}</span><span>${(item.probability * 100).toFixed(1)}%</span>`;
1504
+
1505
+ const barContainer = document.createElement('div');
1506
+ barContainer.className = 'prob-bar-container';
1507
+
1508
+ const bar = document.createElement('div');
1509
+ bar.className = 'prob-bar';
1510
+ bar.style.width = `${item.probability * 100}%`;
1511
+
1512
+ barContainer.appendChild(bar);
1513
+ probContainer.appendChild(probLabel);
1514
+ probContainer.appendChild(barContainer);
1515
+ probabilitiesContainer.appendChild(probContainer);
1516
+ });
1517
+ }
1518
+
1519
+ // 顯示辨識結果 (模型識別的單詞序列)
1520
  if (videoWordSequenceDisplay) {
1521
  if (result.word_sequence && result.word_sequence.length > 0) {
1522
  videoWordSequenceDisplay.textContent = result.word_sequence.join(' ');
 
 
1523
  } else {
1524
+ videoWordSequenceDisplay.textContent = '低於辨識閾值';
1525
  }
1526
  }
1527
 
1528
+ // 顯示AI翻譯結果 (GPT生成的句子)
1529
  if (videoSentenceDisplay) {
1530
  if (result.generated_sentence) {
1531
  videoSentenceDisplay.textContent = result.generated_sentence;
 
 
1532
  } else {
1533
+ videoSentenceDisplay.textContent = '無法生成翻譯';
1534
  }
1535
  }
1536
 
 
1545
  if (resultConfidence) resultConfidence.textContent = '信心度: 0%';
1546
  if (videoWordSequenceDisplay) videoWordSequenceDisplay.textContent = '分析失敗';
1547
  if (videoSentenceDisplay) videoSentenceDisplay.textContent = '分析失敗';
1548
+ if (probabilitiesContainer) {
1549
+ probabilitiesContainer.innerHTML = '<div class="metric-label" style="text-align: center; color: var(--text-tertiary);">分析失敗</div>';
1550
+ }
1551
  }
1552
  }
1553