DawnC commited on
Commit
bb01345
·
verified ·
1 Parent(s): 77026b9

Upload 6 files

Browse files

fixed format issues and improved number counting

enhanced_scene_describer.py CHANGED
@@ -241,7 +241,7 @@ class EnhancedSceneDescriber:
241
  secondary_desc = self.scene_types[current_scene_type]["secondary_description"]
242
  if secondary_desc:
243
  description = self.text_formatter.smart_append(description, secondary_desc)
244
-
245
  # 處理人物相關的描述
246
  people_objs = [obj for obj in current_detected_objects if obj.get("class_id") == 0]
247
  if people_objs:
@@ -333,110 +333,6 @@ class EnhancedSceneDescriber:
333
  except:
334
  return "A scene with various elements is visible."
335
 
336
- def deduplicate_sentences_in_description(self, description: str, similarity_threshold: float = 0.80) -> str:
337
- """
338
- 從一段描述文本中移除重複或高度相似的句子。
339
- 此方法會嘗試保留更長、資訊更豐富的句子版本。
340
-
341
- Args:
342
- description (str): 原始描述文本。
343
- similarity_threshold (float): 判斷句子是否相似的 Jaccard 相似度閾值 (0 到 1)。
344
- 預設為 0.8,表示詞彙重疊度達到80%即視為相似。
345
-
346
- Returns:
347
- str: 移除了重複或高度相似句子後的文本。
348
- """
349
- try:
350
- if not description or not description.strip():
351
- self.logger.debug("deduplicate_sentences_in_description: Received empty or blank description.")
352
- return ""
353
-
354
- # 使用正則表達式分割句子,保留句尾標點符號
355
- sentences = re.split(r'(?<=[.!?])\s+', description.strip())
356
-
357
- if not sentences:
358
- self.logger.debug("deduplicate_sentences_in_description: No sentences found after splitting.")
359
- return ""
360
-
361
- unique_sentences_data = [] # 存儲 (原始句子文本, 該句子的詞彙集合)
362
-
363
- for current_sentence_text in sentences:
364
- current_sentence_text = current_sentence_text.strip()
365
- if not current_sentence_text:
366
- continue
367
-
368
- # 預處理當前句子以進行比較:轉小寫、移除標點、分割成詞彙集合
369
- simplified_current_text = re.sub(r'[^\w\s\d]', '', current_sentence_text.lower()) # 保留數字
370
- current_sentence_words = set(simplified_current_text.split())
371
-
372
- if not current_sentence_words: # 如果處理後是空集合 (例如句子只包含標點)
373
- # 如果原始句子有內容(例如只有一個標點),就保留它
374
- if current_sentence_text and not unique_sentences_data: # 避免在開頭加入孤立標點
375
- unique_sentences_data.append((current_sentence_text, current_sentence_words))
376
- continue
377
-
378
- is_subsumed_or_highly_similar = False
379
- index_to_replace = -1
380
-
381
- for i, (kept_sentence_text, kept_sentence_words) in enumerate(unique_sentences_data):
382
- if not kept_sentence_words: # 跳過已保留的空詞彙集合
383
- continue
384
-
385
- # 計算 Jaccard 相似度
386
- intersection_len = len(current_sentence_words.intersection(kept_sentence_words))
387
- union_len = len(current_sentence_words.union(kept_sentence_words))
388
-
389
- jaccard_similarity = 0.0
390
- if union_len > 0:
391
- jaccard_similarity = intersection_len / union_len
392
- elif not current_sentence_words and not kept_sentence_words: # 兩個都是空的
393
- jaccard_similarity = 1.0
394
-
395
-
396
- if jaccard_similarity >= similarity_threshold:
397
- # 如果當前句子比已保留的句子長,則標記替換舊的
398
- if len(current_sentence_words) > len(kept_sentence_words):
399
- self.logger.debug(f"Deduplication: Replacing shorter \"{kept_sentence_text[:50]}...\" "
400
- f"with longer similar \"{current_sentence_text[:50]}...\" (Jaccard: {jaccard_similarity:.2f})")
401
- index_to_replace = i
402
- break # 找到一個可以被替換的,就跳出內層循環
403
- # 如果當前句子比已保留的句子短,或者長度相近但內容高度相似,則標記當前句子為重複
404
- else: # current_sentence_words is shorter or of similar length
405
- is_subsumed_or_highly_similar = True
406
- self.logger.debug(f"Deduplication: Current sentence \"{current_sentence_text[:50]}...\" "
407
- f"is subsumed by or highly similar to \"{kept_sentence_text[:50]}...\" (Jaccard: {jaccard_similarity:.2f}). Skipping.")
408
- break
409
-
410
- if index_to_replace != -1:
411
- unique_sentences_data[index_to_replace] = (current_sentence_text, current_sentence_words)
412
- elif not is_subsumed_or_highly_similar:
413
- unique_sentences_data.append((current_sentence_text, current_sentence_words))
414
-
415
- # 從 unique_sentences_data 中提取最終的句子文本
416
- final_sentences = [s_data[0] for s_data in unique_sentences_data]
417
-
418
- # 重組句子,確保每個句子以標點符號結尾,並且句子間有空格
419
- reconstructed_response = ""
420
- for i, s_text in enumerate(final_sentences):
421
- s_text = s_text.strip()
422
- if not s_text:
423
- continue
424
- # 確保句子以標點結尾
425
- if not re.search(r'[.!?]$', s_text):
426
- s_text += "."
427
-
428
- reconstructed_response += s_text
429
- if i < len(final_sentences) - 1: # 如果不是最後一句,添加空格
430
- reconstructed_response += " "
431
-
432
- self.logger.debug(f"Deduplicated description (len {len(reconstructed_response.strip())}): '{reconstructed_response.strip()[:150]}...'")
433
- return reconstructed_response.strip()
434
-
435
- except Exception as e:
436
- self.logger.error(f"Error in deduplicate_sentences_in_description: {str(e)}")
437
- self.logger.error(traceback.format_exc())
438
- return description # 發生錯誤時返回原始描述
439
-
440
  def _extract_placeholders(self, template: str) -> List[str]:
441
  """提取模板中的佔位符"""
442
  import re
 
241
  secondary_desc = self.scene_types[current_scene_type]["secondary_description"]
242
  if secondary_desc:
243
  description = self.text_formatter.smart_append(description, secondary_desc)
244
+
245
  # 處理人物相關的描述
246
  people_objs = [obj for obj in current_detected_objects if obj.get("class_id") == 0]
247
  if people_objs:
 
333
  except:
334
  return "A scene with various elements is visible."
335
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  def _extract_placeholders(self, template: str) -> List[str]:
337
  """提取模板中的佔位符"""
338
  import re
llm_enhancer.py CHANGED
@@ -146,12 +146,23 @@ class LLMEnhancer:
146
  if perspective and perspective.lower() not in cleaned_response.lower():
147
  cleaned_response = f"{perspective}, {cleaned_response[0].lower()}{cleaned_response[1:]}"
148
 
 
 
 
 
 
 
 
 
 
 
 
149
  # 14. 最終驗證:如果結果過短,嘗試fallback
150
  final_result = cleaned_response.strip()
151
  if not final_result or len(final_result) < 20:
152
  self.logger.warning("Enhanced description too short; attempting fallback")
153
 
154
- # Fallback prompt
155
  fallback_scene_data = enhanced_scene_data.copy()
156
  fallback_scene_data["is_fallback"] = True
157
  fallback_prompt = self.prompt_manager.format_enhancement_prompt_with_landmark(
 
146
  if perspective and perspective.lower() not in cleaned_response.lower():
147
  cleaned_response = f"{perspective}, {cleaned_response[0].lower()}{cleaned_response[1:]}"
148
 
149
+ # 13.5. 最終的 identical 詞彙清理(確保LLM輸出不包含重複性描述)
150
+ identical_final_cleanup = [
151
+ (r'\b(\d+)\s+identical\s+([a-zA-Z\s]+)', r'\1 \2'),
152
+ (r'\b(two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+identical\s+([a-zA-Z\s]+)', r'\1 \2'),
153
+ (r'\bidentical\s+([a-zA-Z\s]+)', r'\1'),
154
+ (r'\bcomprehensive arrangement of\b', 'arrangement of'),
155
+ ]
156
+
157
+ for pattern, replacement in identical_final_cleanup:
158
+ cleaned_response = re.sub(pattern, replacement, cleaned_response, flags=re.IGNORECASE)
159
+
160
  # 14. 最終驗證:如果結果過短,嘗試fallback
161
  final_result = cleaned_response.strip()
162
  if not final_result or len(final_result) < 20:
163
  self.logger.warning("Enhanced description too short; attempting fallback")
164
 
165
+ # Fallback prompt
166
  fallback_scene_data = enhanced_scene_data.copy()
167
  fallback_scene_data["is_fallback"] = True
168
  fallback_prompt = self.prompt_manager.format_enhancement_prompt_with_landmark(
object_description_generator.py CHANGED
@@ -513,11 +513,10 @@ class ObjectDescriptionGenerator:
513
  # 使用置信度過濾
514
  confident_objects = [obj for obj in detected_objects
515
  if obj.get("confidence", 0) >= self.confidence_threshold_for_description]
516
-
517
- # print(f"DEBUG: After confidence filtering (threshold={self.confidence_threshold_for_description}):")
518
- # for class_name in ["car", "traffic light", "person", "handbag"]:
519
- # class_objects = [obj for obj in confident_objects if obj.get("class_name") == class_name]
520
- # print(f"DEBUG: {class_name}: {len(class_objects)} confident objects")
521
 
522
  if not confident_objects:
523
  no_confident_obj_msg = "While some elements might be present, no objects were identified with sufficient confidence for a detailed description."
@@ -557,10 +556,11 @@ class ObjectDescriptionGenerator:
557
  if name not in objects_by_class:
558
  objects_by_class[name] = []
559
  objects_by_class[name].append(obj)
560
- # print(f"DEBUG: Before spatial deduplication:")
561
- # for class_name in ["car", "traffic light", "person", "handbag"]:
562
- # if class_name in objects_by_class:
563
- # print(f"DEBUG: {class_name}: {len(objects_by_class[class_name])} objects before dedup")
 
564
 
565
  if not objects_by_class:
566
  description_segments.append("No common objects were confidently identified for detailed description.")
@@ -616,22 +616,19 @@ class ObjectDescriptionGenerator:
616
  deduplicated_objects_by_class[class_name] = unique_objects
617
 
618
  objects_by_class = deduplicated_objects_by_class
619
-
620
- # print(f"DEBUG: After spatial deduplication:")
621
- # for class_name in ["car", "traffic light", "person", "handbag"]:
622
- # if class_name in objects_by_class:
623
- # print(f"DEBUG: {class_name}: {len(objects_by_class[class_name])} objects after dedup")
624
-
625
  sorted_object_groups = sorted(objects_by_class.items(), key=sort_key_object_groups)
626
 
627
  object_clauses = []
628
 
629
  for class_name, group_of_objects in sorted_object_groups:
630
  count = len(group_of_objects)
631
-
632
- # if class_name in ["car", "traffic light", "person", "handbag"]:
633
- # print(f"DEBUG: Final count for {class_name}: {count}")
634
-
635
  if count == 0:
636
  continue
637
 
@@ -642,11 +639,15 @@ class ObjectDescriptionGenerator:
642
  if object_statistics and class_name in object_statistics:
643
  actual_count = object_statistics[class_name]["count"]
644
  formatted_name_with_exact_count = self._format_object_count_description(
645
- normalized_class_name, actual_count
 
 
646
  )
647
  else:
648
  formatted_name_with_exact_count = self._format_object_count_description(
649
- normalized_class_name, count
 
 
650
  )
651
 
652
  if formatted_name_with_exact_count == "no specific objects clearly identified" or not formatted_name_with_exact_count:
@@ -726,6 +727,9 @@ class ObjectDescriptionGenerator:
726
  if raw_description and not raw_description.endswith(('.', '!', '?')):
727
  raw_description += "."
728
 
 
 
 
729
  if not raw_description or len(raw_description.strip()) < 20:
730
  if 'confident_objects' in locals() and confident_objects:
731
  return "The scene contains several detected objects, but a detailed textual description could not be fully constructed."
@@ -739,45 +743,498 @@ class ObjectDescriptionGenerator:
739
  self.logger.error(f"{error_msg}\n{traceback.format_exc()}")
740
  raise ObjectDescriptionError(error_msg) from e
741
 
742
- def _format_object_count_description(self, class_name: str, count: int) -> str:
 
 
 
 
 
 
 
 
743
  """
744
- 格式化物件數量描述,提供多樣化的表達方式
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
745
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
  Args:
747
  class_name: 標準化後的類別名稱
748
  count: 物件數量
749
-
 
 
 
750
  Returns:
751
- str: 格式化的數量描述
752
  """
753
  try:
754
  if count <= 0:
755
  return ""
756
 
757
- # 單數情況
758
- if count == 1:
759
- article = "an" if class_name[0].lower() in 'aeiou' else "a"
760
- return f"{article} {class_name}"
761
-
762
- # 複數情況
763
  plural_form = self._get_plural_form(class_name)
764
-
765
- # 根據數量選擇不同的表達方式
766
- if count == 2:
767
- return f"two {plural_form}"
768
- elif count == 3:
769
- return f"three {plural_form}"
770
- elif count <= 5:
771
- return f"{count} {plural_form}"
772
- elif count <= 10:
773
- return f"several {plural_form}"
774
- else:
775
- return f"numerous {plural_form}"
776
 
777
  except Exception as e:
778
  self.logger.warning(f"Error formatting object count for '{class_name}': {str(e)}")
779
  return f"{count} {class_name}s" if count > 1 else class_name
780
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  def _get_plural_form(self, word: str) -> str:
782
  """
783
  獲取詞彙的複數形式
@@ -1282,4 +1739,4 @@ class ObjectDescriptionGenerator:
1282
 
1283
  except Exception as e:
1284
  self.logger.error(f"Error updating configuration: {str(e)}")
1285
- raise ObjectDescriptionError(f"Failed to update configuration: {str(e)}") from e
 
513
  # 使用置信度過濾
514
  confident_objects = [obj for obj in detected_objects
515
  if obj.get("confidence", 0) >= self.confidence_threshold_for_description]
516
+ print(f"DEBUG: After confidence filtering (threshold={self.confidence_threshold_for_description}):")
517
+ for class_name in ["car", "traffic light", "person", "handbag"]:
518
+ class_objects = [obj for obj in confident_objects if obj.get("class_name") == class_name]
519
+ print(f"DEBUG: {class_name}: {len(class_objects)} confident objects")
 
520
 
521
  if not confident_objects:
522
  no_confident_obj_msg = "While some elements might be present, no objects were identified with sufficient confidence for a detailed description."
 
556
  if name not in objects_by_class:
557
  objects_by_class[name] = []
558
  objects_by_class[name].append(obj)
559
+
560
+ print(f"DEBUG: Before spatial deduplication:")
561
+ for class_name in ["car", "traffic light", "person", "handbag"]:
562
+ if class_name in objects_by_class:
563
+ print(f"DEBUG: {class_name}: {len(objects_by_class[class_name])} objects before dedup")
564
 
565
  if not objects_by_class:
566
  description_segments.append("No common objects were confidently identified for detailed description.")
 
616
  deduplicated_objects_by_class[class_name] = unique_objects
617
 
618
  objects_by_class = deduplicated_objects_by_class
619
+ print(f"DEBUG: After spatial deduplication:")
620
+ for class_name in ["car", "traffic light", "person", "handbag"]:
621
+ if class_name in objects_by_class:
622
+ print(f"DEBUG: {class_name}: {len(objects_by_class[class_name])} objects after dedup")
623
+
 
624
  sorted_object_groups = sorted(objects_by_class.items(), key=sort_key_object_groups)
625
 
626
  object_clauses = []
627
 
628
  for class_name, group_of_objects in sorted_object_groups:
629
  count = len(group_of_objects)
630
+ if class_name in ["car", "traffic light", "person", "handbag"]:
631
+ print(f"DEBUG: Final count for {class_name}: {count}")
 
 
632
  if count == 0:
633
  continue
634
 
 
639
  if object_statistics and class_name in object_statistics:
640
  actual_count = object_statistics[class_name]["count"]
641
  formatted_name_with_exact_count = self._format_object_count_description(
642
+ normalized_class_name,
643
+ actual_count,
644
+ scene_type=scene_type
645
  )
646
  else:
647
  formatted_name_with_exact_count = self._format_object_count_description(
648
+ normalized_class_name,
649
+ count,
650
+ scene_type=scene_type
651
  )
652
 
653
  if formatted_name_with_exact_count == "no specific objects clearly identified" or not formatted_name_with_exact_count:
 
727
  if raw_description and not raw_description.endswith(('.', '!', '?')):
728
  raw_description += "."
729
 
730
+ # 移除重複性和不適當的描述詞彙
731
+ raw_description = self._remove_repetitive_descriptors(raw_description)
732
+
733
  if not raw_description or len(raw_description.strip()) < 20:
734
  if 'confident_objects' in locals() and confident_objects:
735
  return "The scene contains several detected objects, but a detailed textual description could not be fully constructed."
 
743
  self.logger.error(f"{error_msg}\n{traceback.format_exc()}")
744
  raise ObjectDescriptionError(error_msg) from e
745
 
746
+ def _remove_repetitive_descriptors(self, description: str) -> str:
747
+ """
748
+ 移除描述中的重複性和不適當的描述詞彙,特別是 "identical" 等詞彙
749
+
750
+ Args:
751
+ description: 原始描述文本
752
+
753
+ Returns:
754
+ str: 清理後的描述文本
755
  """
756
+ try:
757
+ import re
758
+
759
+ # 定義需要移除或替換的模式
760
+ cleanup_patterns = [
761
+ # 移除 "identical" 描述模式
762
+ (r'\b(\d+)\s+identical\s+([a-zA-Z\s]+)', r'\1 \2'),
763
+ (r'\b(two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+identical\s+([a-zA-Z\s]+)', r'\1 \2'),
764
+ (r'\bidentical\s+([a-zA-Z\s]+)', r'\1'),
765
+
766
+ # 改善 "comprehensive arrangement" 等過於技術性的表達
767
+ (r'\bcomprehensive arrangement of\b', 'arrangement of'),
768
+ (r'\bcomprehensive view featuring\b', 'scene featuring'),
769
+ (r'\bcomprehensive display of\b', 'display of'),
770
+
771
+ # 簡化過度描述性的短語
772
+ (r'\bpositioning around\s+(\d+)\s+identical\b', r'positioning around \1'),
773
+ (r'\barranged around\s+(\d+)\s+identical\b', r'arranged around \1'),
774
+ ]
775
+
776
+ processed_description = description
777
+ for pattern, replacement in cleanup_patterns:
778
+ processed_description = re.sub(pattern, replacement, processed_description, flags=re.IGNORECASE)
779
+
780
+ # 進一步清理可能的多餘空格
781
+ processed_description = re.sub(r'\s+', ' ', processed_description).strip()
782
+
783
+ self.logger.debug(f"Cleaned description: removed repetitive descriptors")
784
+ return processed_description
785
+
786
+ except Exception as e:
787
+ self.logger.warning(f"Error removing repetitive descriptors: {str(e)}")
788
+ return description
789
 
790
+ def _format_object_count_description(self, class_name: str, count: int,
791
+ scene_type: Optional[str] = None,
792
+ detected_objects: Optional[List[Dict]] = None,
793
+ avg_confidence: float = 0.0) -> str:
794
+ """
795
+ 格式化物件數量描述的核心方法,整合空間排列、材質推斷和場景語境
796
+
797
+ 這個方法是整個物件描述系統的核心,它將多個子功能整合在一起:
798
+ 1. 數字到文字的轉換(避免阿拉伯數字)
799
+ 2. 基於場景的材質推斷
800
+ 3. 空間排列模式的描述
801
+ 4. 語境化的物件描述
802
+
803
  Args:
804
  class_name: 標準化後的類別名稱
805
  count: 物件數量
806
+ scene_type: 場景類型,用於語境化描述
807
+ detected_objects: 該類型的所有檢測物件,用於空間分析
808
+ avg_confidence: 平均檢測置信度,影響材質推斷的可信度
809
+
810
  Returns:
811
+ str: 完整的格式化數量描述
812
  """
813
  try:
814
  if count <= 0:
815
  return ""
816
 
817
+ # 獲取基礎的複數形式
 
 
 
 
 
818
  plural_form = self._get_plural_form(class_name)
819
+
820
+ # 單數情況的處理
821
+ if count == 1:
822
+ return self._format_single_object_description(class_name, scene_type,
823
+ detected_objects, avg_confidence)
824
+
825
+ # 複數情況的處理
826
+ return self._format_multiple_objects_description(class_name, count, plural_form,
827
+ scene_type, detected_objects, avg_confidence)
 
 
 
828
 
829
  except Exception as e:
830
  self.logger.warning(f"Error formatting object count for '{class_name}': {str(e)}")
831
  return f"{count} {class_name}s" if count > 1 else class_name
832
 
833
+ def _format_single_object_description(self, class_name: str, scene_type: Optional[str],
834
+ detected_objects: Optional[List[Dict]],
835
+ avg_confidence: float) -> str:
836
+ """
837
+ 處理單個物件的描述生成
838
+
839
+ 對於單個物件,我們重點在於通過材質推斷和位置描述來豐富描述內容,
840
+ 避免簡單的 "a chair" 這樣的描述,而是生成 "a wooden dining chair" 這樣的表達
841
+
842
+ Args:
843
+ class_name: 物件類別名稱
844
+ scene_type: 場景類型
845
+ detected_objects: 檢測物件列表
846
+ avg_confidence: 平均置信度
847
+
848
+ Returns:
849
+ str: 單個物件的完整描述
850
+ """
851
+ article = "an" if class_name[0].lower() in 'aeiou' else "a"
852
+
853
+ # 獲取材質描述符
854
+ material_descriptor = self._get_material_descriptor(class_name, scene_type, avg_confidence)
855
+
856
+ # 獲取位置或特徵描述符
857
+ feature_descriptor = self._get_single_object_feature(class_name, scene_type, detected_objects)
858
+
859
+ # 組合描述
860
+ descriptors = []
861
+ if material_descriptor:
862
+ descriptors.append(material_descriptor)
863
+ if feature_descriptor:
864
+ descriptors.append(feature_descriptor)
865
+
866
+ if descriptors:
867
+ return f"{article} {' '.join(descriptors)} {class_name}"
868
+ else:
869
+ return f"{article} {class_name}"
870
+
871
+ def _format_multiple_objects_description(self, class_name: str, count: int, plural_form: str,
872
+ scene_type: Optional[str], detected_objects: Optional[List[Dict]],
873
+ avg_confidence: float) -> str:
874
+ """
875
+ 處理多個物件的描述生成
876
+
877
+ 對於多個物件,我們的重點是:
878
+ 1. 將數字轉換為文字表達
879
+ 2. 分析空間排列模式
880
+ 3. 添加適當的材質或功能描述
881
+ 4. 生成自然流暢的描述
882
+
883
+ Args:
884
+ class_name: 物件類別名稱
885
+ count: 物件數量
886
+ plural_form: 複數形式
887
+ scene_type: 場景類型
888
+ detected_objects: 檢測物件列表
889
+ avg_confidence: 平均置信度
890
+
891
+ Returns:
892
+ str: 多個物件的完整描述
893
+ """
894
+ # 數字到文字的轉換映射
895
+ number_words = {
896
+ 2: "two", 3: "three", 4: "four", 5: "five", 6: "six",
897
+ 7: "seven", 8: "eight", 9: "nine", 10: "ten",
898
+ 11: "eleven", 12: "twelve"
899
+ }
900
+
901
+ # 確定基礎數量表達
902
+ if count in number_words:
903
+ count_expression = number_words[count]
904
+ elif count <= 20:
905
+ count_expression = "several"
906
+ else:
907
+ count_expression = "numerous"
908
+
909
+ # 獲取材質或功能描述符
910
+ material_descriptor = self._get_material_descriptor(class_name, scene_type, avg_confidence)
911
+
912
+ # 獲取空間排列描述
913
+ spatial_descriptor = self._get_spatial_arrangement_descriptor(class_name, scene_type,
914
+ detected_objects, count)
915
+
916
+ # 組合最終描述
917
+ descriptors = []
918
+ if material_descriptor:
919
+ descriptors.append(material_descriptor)
920
+
921
+ # 構建基礎描述
922
+ base_description = f"{count_expression} {' '.join(descriptors)} {plural_form}".strip()
923
+
924
+ # 添加空間排列信息
925
+ if spatial_descriptor:
926
+ return f"{base_description} {spatial_descriptor}"
927
+ else:
928
+ return base_description
929
+
930
+ def _get_material_descriptor(self, class_name: str, scene_type: Optional[str],
931
+ avg_confidence: float) -> Optional[str]:
932
+ """
933
+ 基於場景語境和置信度進行材質推斷
934
+
935
+ 這個方法實現了智能的材質推斷,它不依賴複雜的圖像分析,
936
+ 而是基於常識和場景邏輯來推斷最可能的材質描述
937
+
938
+ Args:
939
+ class_name: 物件類別名稱
940
+ scene_type: 場景類型
941
+ avg_confidence: 檢測置信度,影響推斷的保守程度
942
+
943
+ Returns:
944
+ Optional[str]: 材質描述符,如果無法推斷則返回None
945
+ """
946
+ # 只有在置信度足夠高時才進行材質推斷
947
+ if avg_confidence < 0.5:
948
+ return None
949
+
950
+ # 餐廳和用餐相關場景
951
+ if scene_type and scene_type in ["dining_area", "restaurant", "upscale_dining", "cafe"]:
952
+ material_mapping = {
953
+ "chair": "wooden" if avg_confidence > 0.7 else None,
954
+ "dining table": "wooden",
955
+ "couch": "upholstered",
956
+ "vase": "decorative"
957
+ }
958
+ return material_mapping.get(class_name)
959
+
960
+ # 辦公場景
961
+ elif scene_type and scene_type in ["office_workspace", "meeting_room", "conference_room"]:
962
+ material_mapping = {
963
+ "chair": "office",
964
+ "dining table": "conference", # 在辦公環境中,餐桌通常是會議桌
965
+ "laptop": "modern",
966
+ "book": "reference"
967
+ }
968
+ return material_mapping.get(class_name)
969
+
970
+ # 客廳場景
971
+ elif scene_type and scene_type in ["living_room"]:
972
+ material_mapping = {
973
+ "couch": "comfortable",
974
+ "chair": "accent",
975
+ "tv": "large",
976
+ "vase": "decorative"
977
+ }
978
+ return material_mapping.get(class_name)
979
+
980
+ # 室外場景
981
+ elif scene_type and scene_type in ["city_street", "park_area", "parking_lot"]:
982
+ material_mapping = {
983
+ "car": "parked",
984
+ "person": "walking",
985
+ "bicycle": "stationed"
986
+ }
987
+ return material_mapping.get(class_name)
988
+
989
+ # 如果沒有特定的場景映射,返回通用描述符
990
+ generic_mapping = {
991
+ "chair": "comfortable",
992
+ "dining table": "sturdy",
993
+ "car": "parked",
994
+ "person": "present"
995
+ }
996
+
997
+ return generic_mapping.get(class_name)
998
+
999
+ def _get_spatial_arrangement_descriptor(self, class_name: str, scene_type: Optional[str],
1000
+ detected_objects: Optional[List[Dict]],
1001
+ count: int) -> Optional[str]:
1002
+ """
1003
+ 分析物件的空間排列模式並生成相應描述
1004
+
1005
+ 這個方法通過分析物件的位置分布來判斷排列模式,
1006
+ 然後根據物件類型和場景生成適當的空間描述
1007
+
1008
+ Args:
1009
+ class_name: 物件類別名稱
1010
+ scene_type: 場景類型
1011
+ detected_objects: 該類型的所有檢測物件
1012
+ count: 物件數量
1013
+
1014
+ Returns:
1015
+ Optional[str]: 空間排列描述,如果無法分析則返回None
1016
+ """
1017
+ if not detected_objects or len(detected_objects) < 2:
1018
+ return None
1019
+
1020
+ try:
1021
+ # 提取物件的標準化位置
1022
+ positions = []
1023
+ for obj in detected_objects:
1024
+ center = obj.get("normalized_center", [0.5, 0.5])
1025
+ if isinstance(center, (list, tuple)) and len(center) >= 2:
1026
+ positions.append(center)
1027
+
1028
+ if len(positions) < 2:
1029
+ return None
1030
+
1031
+ # 分析排列模式
1032
+ arrangement_pattern = self._analyze_arrangement_pattern(positions)
1033
+
1034
+ # 根據物件類型和場景生成描述
1035
+ return self._generate_arrangement_description(class_name, scene_type,
1036
+ arrangement_pattern, count)
1037
+
1038
+ except Exception as e:
1039
+ self.logger.warning(f"Error analyzing spatial arrangement: {str(e)}")
1040
+ return None
1041
+
1042
+ def _analyze_arrangement_pattern(self, positions: List[List[float]]) -> str:
1043
+ """
1044
+ 分析位置點的排列模式
1045
+
1046
+ 這個方法使用簡單的幾何分析來判斷物件的排列類型,
1047
+ 幫助我們理解物件在空間中的組織方式
1048
+
1049
+ Args:
1050
+ positions: 標準化的位置座標列表
1051
+
1052
+ Returns:
1053
+ str: 排列模式類型(linear, clustered, scattered, circular等)
1054
+ """
1055
+ import numpy as np
1056
+
1057
+ if len(positions) < 2:
1058
+ return "single"
1059
+
1060
+ # 轉換為numpy陣列便於計算
1061
+ pos_array = np.array(positions)
1062
+
1063
+ # 計算��置的分布特徵
1064
+ x_coords = pos_array[:, 0]
1065
+ y_coords = pos_array[:, 1]
1066
+
1067
+ # 分析x和y方向的變異程度
1068
+ x_variance = np.var(x_coords)
1069
+ y_variance = np.var(y_coords)
1070
+
1071
+ # 計算物件間的平均距離
1072
+ distances = []
1073
+ for i in range(len(positions)):
1074
+ for j in range(i + 1, len(positions)):
1075
+ dist = np.sqrt((positions[i][0] - positions[j][0])**2 +
1076
+ (positions[i][1] - positions[j][1])**2)
1077
+ distances.append(dist)
1078
+
1079
+ avg_distance = np.mean(distances) if distances else 0
1080
+ distance_variance = np.var(distances) if distances else 0
1081
+
1082
+ # 判斷排列模式
1083
+ if len(positions) >= 4 and self._is_circular_pattern(positions):
1084
+ return "circular"
1085
+ elif x_variance < 0.05 or y_variance < 0.05: # 一個方向變異很小
1086
+ return "linear"
1087
+ elif avg_distance < 0.3 and distance_variance < 0.02: # 物件聚集且距離相近
1088
+ return "clustered"
1089
+ elif avg_distance > 0.6: # 物件分散
1090
+ return "scattered"
1091
+ elif distance_variance < 0.03: # 距離一致,可能是規則排列
1092
+ return "regular"
1093
+ else:
1094
+ return "distributed"
1095
+
1096
+ def _is_circular_pattern(self, positions: List[List[float]]) -> bool:
1097
+ """
1098
+ 檢查位置是否形成圓形或環形排列
1099
+
1100
+ Args:
1101
+ positions: 位置座標列表
1102
+
1103
+ Returns:
1104
+ bool: 是否為圓形排列
1105
+ """
1106
+ import numpy as np
1107
+
1108
+ if len(positions) < 4:
1109
+ return False
1110
+
1111
+ try:
1112
+ pos_array = np.array(positions)
1113
+
1114
+ # 計算中心點
1115
+ center_x = np.mean(pos_array[:, 0])
1116
+ center_y = np.mean(pos_array[:, 1])
1117
+
1118
+ # 計算每個點到中心的距離
1119
+ distances_to_center = []
1120
+ for pos in positions:
1121
+ dist = np.sqrt((pos[0] - center_x)**2 + (pos[1] - center_y)**2)
1122
+ distances_to_center.append(dist)
1123
+
1124
+ # 如果所有距離都相近,可能是圓形排列
1125
+ distance_variance = np.var(distances_to_center)
1126
+ return distance_variance < 0.05 and np.mean(distances_to_center) > 0.2
1127
+
1128
+ except:
1129
+ return False
1130
+
1131
+ def _generate_arrangement_description(self, class_name: str, scene_type: Optional[str],
1132
+ arrangement_pattern: str, count: int) -> Optional[str]:
1133
+ """
1134
+ 根據物件類型、場景和排列模式生成空間描述
1135
+
1136
+ 這個方法將抽象的排列模式轉換為自然語言描述,
1137
+ 並根據具體的物件類型和場景語境進行定制
1138
+
1139
+ Args:
1140
+ class_name: 物件類別名稱
1141
+ scene_type: 場景類型
1142
+ arrangement_pattern: 排列模式
1143
+ count: 物件數量
1144
+
1145
+ Returns:
1146
+ Optional[str]: 生成的空間排列描述
1147
+ """
1148
+ # 基於物件類型的描述模板
1149
+ arrangement_templates = {
1150
+ "chair": {
1151
+ "linear": "arranged in a row",
1152
+ "clustered": "grouped together for conversation",
1153
+ "circular": "arranged around the table",
1154
+ "scattered": "positioned throughout the space",
1155
+ "regular": "evenly spaced",
1156
+ "distributed": "thoughtfully positioned"
1157
+ },
1158
+ "dining table": {
1159
+ "linear": "aligned to create a unified dining space",
1160
+ "clustered": "grouped to form intimate dining areas",
1161
+ "scattered": "distributed to optimize space flow",
1162
+ "regular": "systematically positioned",
1163
+ "distributed": "strategically placed"
1164
+ },
1165
+ "car": {
1166
+ "linear": "parked in sequence",
1167
+ "clustered": "grouped in the parking area",
1168
+ "scattered": "distributed throughout the lot",
1169
+ "regular": "neatly parked",
1170
+ "distributed": "positioned across the area"
1171
+ },
1172
+ "person": {
1173
+ "linear": "moving in a line",
1174
+ "clustered": "gathered together",
1175
+ "circular": "forming a circle",
1176
+ "scattered": "spread across the area",
1177
+ "distributed": "positioned throughout the scene"
1178
+ }
1179
+ }
1180
+
1181
+ # 獲取對應的描述模板
1182
+ if class_name in arrangement_templates:
1183
+ template_dict = arrangement_templates[class_name]
1184
+ base_description = template_dict.get(arrangement_pattern, "positioned in the scene")
1185
+ else:
1186
+ # 通用的排列描述
1187
+ generic_templates = {
1188
+ "linear": "arranged in a line",
1189
+ "clustered": "grouped together",
1190
+ "circular": "arranged in a circular pattern",
1191
+ "scattered": "distributed across the space",
1192
+ "regular": "evenly positioned",
1193
+ "distributed": "thoughtfully placed"
1194
+ }
1195
+ base_description = generic_templates.get(arrangement_pattern, "positioned in the scene")
1196
+
1197
+ return base_description
1198
+
1199
+ def _get_single_object_feature(self, class_name: str, scene_type: Optional[str],
1200
+ detected_objects: Optional[List[Dict]]) -> Optional[str]:
1201
+ """
1202
+ 為單個物件生成特徵描述符
1203
+
1204
+ 當只有一個物件時,我們可以提供更具體的位置或功能描述
1205
+
1206
+ Args:
1207
+ class_name: 物件類別名稱
1208
+ scene_type: 場景類型
1209
+ detected_objects: 檢測物件(單個)
1210
+
1211
+ Returns:
1212
+ Optional[str]: 特徵描述符
1213
+ """
1214
+ if not detected_objects or len(detected_objects) != 1:
1215
+ return None
1216
+
1217
+ obj = detected_objects[0]
1218
+ region = obj.get("region", "").lower()
1219
+
1220
+ # 基於位置的描述
1221
+ if "center" in region:
1222
+ if class_name == "dining table":
1223
+ return "central"
1224
+ elif class_name == "chair":
1225
+ return "centrally placed"
1226
+ elif "corner" in region or "left" in region or "right" in region:
1227
+ return "positioned"
1228
+
1229
+ # 基於場景的功能描述
1230
+ if scene_type and scene_type in ["dining_area", "restaurant"]:
1231
+ if class_name == "chair":
1232
+ return "dining"
1233
+ elif class_name == "vase":
1234
+ return "decorative"
1235
+
1236
+ return None
1237
+
1238
  def _get_plural_form(self, word: str) -> str:
1239
  """
1240
  獲取詞彙的複數形式
 
1739
 
1740
  except Exception as e:
1741
  self.logger.error(f"Error updating configuration: {str(e)}")
1742
+ raise ObjectDescriptionError(f"Failed to update configuration: {str(e)}") from e
response_processor.py CHANGED
@@ -652,6 +652,44 @@ class ResponseProcessor:
652
  pattern = re.compile(r'\b' + re.escape(word_to_replace) + r'\b', re.IGNORECASE)
653
  processed_response = pattern.sub(replacer_instance, processed_response)
654
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
655
  return processed_response
656
 
657
  except Exception as e:
 
652
  pattern = re.compile(r'\b' + re.escape(word_to_replace) + r'\b', re.IGNORECASE)
653
  processed_response = pattern.sub(replacer_instance, processed_response)
654
 
655
+ # 移除 identical 等重複性描述詞彙
656
+ identical_cleanup_patterns = [
657
+ (r'\b(\d+)\s+identical\s+([a-zA-Z\s]+)', r'\1 \2'),
658
+ (r'\b(two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+identical\s+([a-zA-Z\s]+)', r'\1 \2'),
659
+ (r'\bidentical\s+([a-zA-Z\s]+)', r'\1'),
660
+ (r'\bcomprehensive arrangement of\b', 'arrangement of'),
661
+ (r'\bcomprehensive view featuring\b', 'scene featuring'),
662
+ (r'\bcomprehensive display of\b', 'display of'),
663
+ ]
664
+
665
+ for pattern, replacement in identical_cleanup_patterns:
666
+ processed_response = re.sub(pattern, replacement, processed_response, flags=re.IGNORECASE)
667
+
668
+ # 數字到文字
669
+ number_conversions = {
670
+ '2': 'two', '3': 'three', '4': 'four', '5': 'five', '6': 'six',
671
+ '7': 'seven', '8': 'eight', '9': 'nine', '10': 'ten',
672
+ '11': 'eleven', '12': 'twelve'
673
+ }
674
+
675
+ # 處理各種語法結構中的數字
676
+ for digit, word in number_conversions.items():
677
+ # 模式1: 數字 + 單一複數詞 (如 "7 chairs")
678
+ pattern1 = rf'\b{digit}\s+([a-zA-Z]+s)\b'
679
+ processed_response = re.sub(pattern1, rf'{word} \1', processed_response)
680
+
681
+ # 模式2: 數字 + 修飾詞 + 複數詞 (如 "7 more chairs")
682
+ pattern2 = rf'\b{digit}\s+(more|additional|other|identical)\s+([a-zA-Z]+s)\b'
683
+ processed_response = re.sub(pattern2, rf'{word} \1 \2', processed_response, flags=re.IGNORECASE)
684
+
685
+ # 模式3: 數字 + 形容詞 + 複數詞 (如 "2 dining tables")
686
+ pattern3 = rf'\b{digit}\s+([a-zA-Z]+)\s+([a-zA-Z]+s)\b'
687
+ processed_response = re.sub(pattern3, rf'{word} \1 \2', processed_response)
688
+
689
+ # 模式4: 介詞片語中的數字 (如 "around 2 tables")
690
+ pattern4 = rf'\b(around|approximately|about)\s+{digit}\s+([a-zA-Z]+s)\b'
691
+ processed_response = re.sub(pattern4, rf'\1 {word} \2', processed_response, flags=re.IGNORECASE)
692
+
693
  return processed_response
694
 
695
  except Exception as e:
template_manager.py CHANGED
@@ -35,7 +35,7 @@ class TemplateManager:
35
  custom_templates_db: 可選的自定義模板數據庫,如果提供則會與默認模板合併
36
  """
37
  self.logger = logging.getLogger(self.__class__.__name__)
38
- self.template_registry = {}
39
 
40
  try:
41
  # 載入模板數據庫
@@ -1047,10 +1047,43 @@ class TemplateManager:
1047
  count = object_statistics["chair"]["count"]
1048
  if count == 1:
1049
  replacements["seating"] = "a chair"
 
1050
  elif count <= 4:
1051
- replacements["seating"] = f"{count} chairs"
 
 
 
 
 
 
1052
  else:
1053
  replacements["seating"] = f"numerous chairs ({count} total)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1054
 
1055
  # 處理人員
1056
  if "person" in object_statistics:
 
35
  custom_templates_db: 可選的自定義模板數據庫,如果提供則會與默認模板合併
36
  """
37
  self.logger = logging.getLogger(self.__class__.__name__)
38
+ self.template_registry = {}
39
 
40
  try:
41
  # 載入模板數據庫
 
1047
  count = object_statistics["chair"]["count"]
1048
  if count == 1:
1049
  replacements["seating"] = "a chair"
1050
+ replacements["furniture"] = "a chair" # 新增:同時處理furniture佔位符
1051
  elif count <= 4:
1052
+ number_word = ["", "one", "two", "three", "four"][count] # 轉換為文字
1053
+ replacements["seating"] = f"{number_word} chairs"
1054
+ replacements["furniture"] = f"{number_word} chairs" # 同時處理furniture佔位符
1055
+ elif count <= 6:
1056
+ number_words = ["", "one", "two", "three", "four", "five", "six"]
1057
+ replacements["seating"] = f"{number_words[count]} chairs"
1058
+ replacements["furniture"] = f"{number_words[count]} chairs" # 同時處理furniture佔位符
1059
  else:
1060
  replacements["seating"] = f"numerous chairs ({count} total)"
1061
+ replacements["furniture"] = f"numerous chairs" # 通用情況下的家具描述
1062
+
1063
+ # 處理混合家具情況(當存在多種家具類型時)
1064
+ furniture_items = []
1065
+ furniture_counts = []
1066
+
1067
+ # 收集所有家具類型的統計
1068
+ for furniture_type in ["chair", "dining table", "couch", "bed"]:
1069
+ if furniture_type in object_statistics:
1070
+ count = object_statistics[furniture_type]["count"]
1071
+ if count > 0:
1072
+ furniture_items.append(furniture_type)
1073
+ furniture_counts.append(count)
1074
+
1075
+ # 如果只有椅子,那就用上面的方式
1076
+ # 如果有多種家具類型,生成組合描述
1077
+ if len(furniture_items) > 1 and "furniture" not in replacements:
1078
+ main_furniture = furniture_items[0] # 數量最多的家具類型
1079
+ main_count = furniture_counts[0]
1080
+
1081
+ if main_furniture == "chair":
1082
+ number_words = ["", "one", "two", "three", "four", "five", "six"]
1083
+ if main_count <= 6:
1084
+ replacements["furniture"] = f"{number_words[main_count]} chairs and other furniture"
1085
+ else:
1086
+ replacements["furniture"] = "multiple chairs and other furniture"
1087
 
1088
  # 處理人員
1089
  if "person" in object_statistics:
text_formatter.py CHANGED
@@ -239,6 +239,16 @@ class TextFormatter:
239
  # 11. 移除最終標點符號前的空格(如果規則7意外添加)
240
  text = re.sub(r'\s+([.!?])$', r'\1', text)
241
 
 
 
 
 
 
 
 
 
 
 
242
  return text.strip() # 最終修剪
243
 
244
  except Exception as e:
@@ -543,3 +553,107 @@ class TextFormatter:
543
  except Exception as e:
544
  self.logger.warning(f"Error getting text statistics: {str(e)}")
545
  return {"characters": 0, "words": 0, "sentences": 0}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  # 11. 移除最終標點符號前的空格(如果規則7意外添加)
240
  text = re.sub(r'\s+([.!?])$', r'\1', text)
241
 
242
+ # 12. 移除重複性描述詞彙的最終檢查
243
+ identical_cleanup_patterns = [
244
+ (r'\b(\d+)\s+identical\s+([a-zA-Z\s]+)', r'\1 \2'),
245
+ (r'\b(two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+identical\s+([a-zA-Z\s]+)', r'\1 \2'),
246
+ (r'\bidentical\s+([a-zA-Z\s]+)', r'\1'),
247
+ (r'\bcomprehensive arrangement of\b', 'arrangement of'),
248
+ ]
249
+ for pattern, replacement in identical_cleanup_patterns:
250
+ text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
251
+
252
  return text.strip() # 最終修剪
253
 
254
  except Exception as e:
 
553
  except Exception as e:
554
  self.logger.warning(f"Error getting text statistics: {str(e)}")
555
  return {"characters": 0, "words": 0, "sentences": 0}
556
+
557
+ def deduplicate_sentences_in_description(self, description: str, similarity_threshold: float = 0.80) -> str:
558
+ """
559
+ 從一段描述文本中移除重複或高度相似的句子。
560
+ 此方法會嘗試保留更長、資訊更豐富的句子版本。
561
+
562
+ Args:
563
+ description (str): 原始描述文本。
564
+ similarity_threshold (float): 判斷句子是否相似的 Jaccard 相似度閾值 (0 到 1)。
565
+ 預設為 0.8,表示詞彙重疊度達到80%即視為相似。
566
+
567
+ Returns:
568
+ str: 移除了重複或高度相似句子後的文本。
569
+ """
570
+ try:
571
+ if not description or not description.strip():
572
+ self.logger.debug("deduplicate_sentences_in_description: Received empty or blank description.")
573
+ return ""
574
+
575
+ # 使用正則表達式分割句子,保留句尾標點符號
576
+ sentences = re.split(r'(?<=[.!?])\s+', description.strip())
577
+
578
+ if not sentences:
579
+ self.logger.debug("deduplicate_sentences_in_description: No sentences found after splitting.")
580
+ return ""
581
+
582
+ unique_sentences_data = [] # 存儲 (原始句子文本, 該句子的詞彙集合)
583
+
584
+ for current_sentence_text in sentences:
585
+ current_sentence_text = current_sentence_text.strip()
586
+ if not current_sentence_text:
587
+ continue
588
+
589
+ # 預處理當前句子以進行比較:轉小寫、移除標點、分割成詞彙集合
590
+ simplified_current_text = re.sub(r'[^\w\s\d]', '', current_sentence_text.lower()) # 保留數字
591
+ current_sentence_words = set(simplified_current_text.split())
592
+
593
+ if not current_sentence_words: # 如果處理後是空集合 (例如句子只包含標點)
594
+ # 如果原始句子有內容(例如只有一個標點),就保留它
595
+ if current_sentence_text and not unique_sentences_data: # 避免在開頭加入孤立標點
596
+ unique_sentences_data.append((current_sentence_text, current_sentence_words))
597
+ continue
598
+
599
+ is_subsumed_or_highly_similar = False
600
+ index_to_replace = -1
601
+
602
+ for i, (kept_sentence_text, kept_sentence_words) in enumerate(unique_sentences_data):
603
+ if not kept_sentence_words: # 跳過已保留的空詞彙集合
604
+ continue
605
+
606
+ # 計算 Jaccard 相似度
607
+ intersection_len = len(current_sentence_words.intersection(kept_sentence_words))
608
+ union_len = len(current_sentence_words.union(kept_sentence_words))
609
+
610
+ jaccard_similarity = 0.0
611
+ if union_len > 0:
612
+ jaccard_similarity = intersection_len / union_len
613
+ elif not current_sentence_words and not kept_sentence_words: # 兩個都是空的
614
+ jaccard_similarity = 1.0
615
+
616
+
617
+ if jaccard_similarity >= similarity_threshold:
618
+ # 如果當前句子比已保留的句子長,則標記替換舊的
619
+ if len(current_sentence_words) > len(kept_sentence_words):
620
+ self.logger.debug(f"Deduplication: Replacing shorter \"{kept_sentence_text[:50]}...\" "
621
+ f"with longer similar \"{current_sentence_text[:50]}...\" (Jaccard: {jaccard_similarity:.2f})")
622
+ index_to_replace = i
623
+ break # 找到一個可以被替換的,就跳出內層循環
624
+ # 如果當前句子比已保留的句子短,或者長度相近但內容高度相似,則標記當前句子為重複
625
+ else: # current_sentence_words is shorter or of similar length
626
+ is_subsumed_or_highly_similar = True
627
+ self.logger.debug(f"Deduplication: Current sentence \"{current_sentence_text[:50]}...\" "
628
+ f"is subsumed by or highly similar to \"{kept_sentence_text[:50]}...\" (Jaccard: {jaccard_similarity:.2f}). Skipping.")
629
+ break
630
+
631
+ if index_to_replace != -1:
632
+ unique_sentences_data[index_to_replace] = (current_sentence_text, current_sentence_words)
633
+ elif not is_subsumed_or_highly_similar:
634
+ unique_sentences_data.append((current_sentence_text, current_sentence_words))
635
+
636
+ # 從 unique_sentences_data 中提取最終的句子文本
637
+ final_sentences = [s_data[0] for s_data in unique_sentences_data]
638
+
639
+ # 重組句子,確保每個句子以標點符號結尾,並且句子間有空格
640
+ reconstructed_response = ""
641
+ for i, s_text in enumerate(final_sentences):
642
+ s_text = s_text.strip()
643
+ if not s_text:
644
+ continue
645
+ # 確保句子以標點結尾
646
+ if not re.search(r'[.!?]$', s_text):
647
+ s_text += "."
648
+
649
+ reconstructed_response += s_text
650
+ if i < len(final_sentences) - 1: # 如果不是最後一句,添加空格
651
+ reconstructed_response += " "
652
+
653
+ self.logger.debug(f"Deduplicated description (len {len(reconstructed_response.strip())}): '{reconstructed_response.strip()[:150]}...'")
654
+ return reconstructed_response.strip()
655
+
656
+ except Exception as e:
657
+ self.logger.error(f"Error in deduplicate_sentences_in_description: {str(e)}")
658
+ self.logger.error(traceback.format_exc())
659
+ return description # 發生錯誤時返回原始描述