frontengine 1.0.22__tar.gz → 1.0.24__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {frontengine-1.0.22 → frontengine-1.0.24}/PKG-INFO +1 -1
  2. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/pet/desktop_pet.py +40 -2
  3. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/pet/pet_setting_ui.py +10 -0
  4. frontengine-1.0.24/frontengine/utils/audio_meter/system_audio_meter.py +136 -0
  5. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/multi_language/english.py +1 -0
  6. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/multi_language/traditional_chinese.py +1 -0
  7. frontengine-1.0.24/frontengine/worker/__init__.py +0 -0
  8. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine.egg-info/PKG-INFO +1 -1
  9. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine.egg-info/SOURCES.txt +2 -0
  10. {frontengine-1.0.22 → frontengine-1.0.24}/pyproject.toml +1 -1
  11. {frontengine-1.0.22 → frontengine-1.0.24}/LICENSE +0 -0
  12. {frontengine-1.0.22 → frontengine-1.0.24}/README.md +0 -0
  13. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/__init__.py +0 -0
  14. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/__main__.py +0 -0
  15. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/__init__.py +0 -0
  16. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/base_widget.py +0 -0
  17. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/gif/__init__.py +0 -0
  18. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/gif/paint_gif.py +0 -0
  19. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/image/__init__.py +0 -0
  20. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/image/paint_image.py +0 -0
  21. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/load/__init__.py +0 -0
  22. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/load/load_someone_make_ui.py +0 -0
  23. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/overlay_factory.py +0 -0
  24. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/particle/__init__.py +0 -0
  25. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/particle/particle_ui.py +0 -0
  26. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/pet/__init__.py +0 -0
  27. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/scene/__init__.py +0 -0
  28. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/scene/extend_graphic_scene.py +0 -0
  29. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/scene/extend_graphic_view.py +0 -0
  30. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/scene/scene.py +0 -0
  31. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/sound_player/__init__.py +0 -0
  32. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/sound_player/sound_effect.py +0 -0
  33. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/sound_player/sound_player.py +0 -0
  34. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/text/__init__.py +0 -0
  35. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/text/draw_text.py +0 -0
  36. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/video/__init__.py +0 -0
  37. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/video/video_player.py +0 -0
  38. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/web/__init__.py +0 -0
  39. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/web/webview.py +0 -0
  40. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/show/window_helpers.py +0 -0
  41. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/system_tray/__init__.py +0 -0
  42. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/system_tray/extend_system_tray.py +0 -0
  43. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/__init__.py +0 -0
  44. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/color/__init__.py +0 -0
  45. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/color/global_color.py +0 -0
  46. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/dialog/__init__.py +0 -0
  47. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/dialog/choose_file_dialog.py +0 -0
  48. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/dialog/hotkey_settings_dialog.py +0 -0
  49. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/dialog/save_file_dialog.py +0 -0
  50. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/main_ui.py +0 -0
  51. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/menu/__init__.py +0 -0
  52. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/menu/help_menu.py +0 -0
  53. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/menu/how_to_menu.py +0 -0
  54. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/menu/language_menu.py +0 -0
  55. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/menu/preset_menu.py +0 -0
  56. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/menu/settings_menu.py +0 -0
  57. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/__init__.py +0 -0
  58. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/control_center/__init__.py +0 -0
  59. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/control_center/control_center_ui.py +0 -0
  60. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/gif/__init__.py +0 -0
  61. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/gif/gif_setting_ui.py +0 -0
  62. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/image/__init__.py +0 -0
  63. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/image/image_setting_ui.py +0 -0
  64. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/particle/__init__.py +0 -0
  65. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/particle/particle_setting_ui.py +0 -0
  66. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/pet/__init__.py +0 -0
  67. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/__init__.py +0 -0
  68. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_manager.py +0 -0
  69. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/__init__.py +0 -0
  70. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/base_scene_page.py +0 -0
  71. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/gif.py +0 -0
  72. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/image.py +0 -0
  73. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/registry.py +0 -0
  74. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/sound.py +0 -0
  75. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/text.py +0 -0
  76. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/video.py +0 -0
  77. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_page/web.py +0 -0
  78. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/scene_setting/scene_setting_ui.py +0 -0
  79. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/sound_player/__init__.py +0 -0
  80. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/sound_player/sound_player_setting_ui.py +0 -0
  81. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/text/__init__.py +0 -0
  82. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/text/text_setting_ui.py +0 -0
  83. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/utils.py +0 -0
  84. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/video/__init__.py +0 -0
  85. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/video/video_setting_ui.py +0 -0
  86. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/web/__init__.py +0 -0
  87. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/ui/page/web/web_setting_ui.py +0 -0
  88. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/user_setting/__init__.py +0 -0
  89. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/user_setting/preset_repository.py +0 -0
  90. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/user_setting/scene_setting.py +0 -0
  91. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/user_setting/user_setting_file.py +0 -0
  92. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/__init__.py +0 -0
  93. {frontengine-1.0.22/frontengine/utils/browser → frontengine-1.0.24/frontengine/utils/audio_meter}/__init__.py +0 -0
  94. {frontengine-1.0.22/frontengine/utils/critical_exit → frontengine-1.0.24/frontengine/utils/browser}/__init__.py +0 -0
  95. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/browser/browser.py +0 -0
  96. {frontengine-1.0.22/frontengine/utils/file → frontengine-1.0.24/frontengine/utils/critical_exit}/__init__.py +0 -0
  97. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/critical_exit/check_key_is_press.py +0 -0
  98. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/critical_exit/critical_exit.py +0 -0
  99. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/critical_exit/win32_vk.py +0 -0
  100. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/exception/__init__.py +0 -0
  101. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/exception/exception_tags.py +0 -0
  102. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/exception/exceptions.py +0 -0
  103. {frontengine-1.0.22/frontengine/utils/hotkey → frontengine-1.0.24/frontengine/utils/file}/__init__.py +0 -0
  104. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/file/open/__init__.py +0 -0
  105. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/file/open/open_file.py +0 -0
  106. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/file/save/__init__.py +0 -0
  107. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/file/save/save_file.py +0 -0
  108. {frontengine-1.0.22/frontengine/utils/json → frontengine-1.0.24/frontengine/utils/hotkey}/__init__.py +0 -0
  109. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/hotkey/hotkey_service.py +0 -0
  110. {frontengine-1.0.22/frontengine/utils/logging → frontengine-1.0.24/frontengine/utils/json}/__init__.py +0 -0
  111. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/json/json_file.py +0 -0
  112. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/json/json_repository.py +0 -0
  113. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/json_format/__init__.py +0 -0
  114. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/json_format/json_process.py +0 -0
  115. {frontengine-1.0.22/frontengine/utils/multi_language → frontengine-1.0.24/frontengine/utils/logging}/__init__.py +0 -0
  116. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/logging/loggin_instance.py +0 -0
  117. {frontengine-1.0.22/frontengine/utils/preset_schedule → frontengine-1.0.24/frontengine/utils/multi_language}/__init__.py +0 -0
  118. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/multi_language/france.py +0 -0
  119. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/multi_language/germany.py +0 -0
  120. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/multi_language/italy.py +0 -0
  121. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/multi_language/language_wrapper.py +0 -0
  122. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/multi_language/russian.py +0 -0
  123. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/multi_language/simplified_chinese.py +0 -0
  124. {frontengine-1.0.22/frontengine/utils/redirect_manager → frontengine-1.0.24/frontengine/utils/preset_schedule}/__init__.py +0 -0
  125. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/preset_schedule/preset_schedule_service.py +0 -0
  126. {frontengine-1.0.22/frontengine/utils/smart_pause → frontengine-1.0.24/frontengine/utils/redirect_manager}/__init__.py +0 -0
  127. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/redirect_manager/redirect_manager_class.py +0 -0
  128. {frontengine-1.0.22/frontengine/utils/theme_schedule → frontengine-1.0.24/frontengine/utils/smart_pause}/__init__.py +0 -0
  129. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/smart_pause/smart_pause_service.py +0 -0
  130. {frontengine-1.0.22/frontengine/worker → frontengine-1.0.24/frontengine/utils/theme_schedule}/__init__.py +0 -0
  131. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/theme_schedule/theme_schedule_service.py +0 -0
  132. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/utils/web_url.py +0 -0
  133. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine/worker/qthread_worker.py +0 -0
  134. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine.egg-info/dependency_links.txt +0 -0
  135. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine.egg-info/entry_points.txt +0 -0
  136. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine.egg-info/requires.txt +0 -0
  137. {frontengine-1.0.22 → frontengine-1.0.24}/frontengine.egg-info/top_level.txt +0 -0
  138. {frontengine-1.0.22 → frontengine-1.0.24}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.22
3
+ Version: 1.0.24
4
4
  Summary: FrontEngine is use-define frontview or only use like screen saver
5
5
  Author-email: JE-Chen <jechenmailman@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Intergration-Automation-Testing/FrontEngine
@@ -369,6 +369,15 @@ def format_status(level: int, mood_level: str, hunger_level: str, labels: dict)
369
369
  return f"{level_label}{level} · {mood_text} · {hunger_text}"
370
370
 
371
371
 
372
+ def audio_pulse_scale(level, base: float = 0.7, amplitude: float = 0.3) -> float:
373
+ """把音訊音量 (0~1) 轉成脈動縮放比例 / Map an audio level (0..1) to a pulse scale."""
374
+ try:
375
+ clamped = max(0.0, min(1.0, float(level)))
376
+ except (TypeError, ValueError):
377
+ return 1.0
378
+ return base + amplitude * clamped
379
+
380
+
372
381
  def size_for_level(base_size: int, level: int) -> int:
373
382
  """依等級放大基礎尺寸(每級 +8%,上限 +50%)/ Grow base size by level (cap +50%)."""
374
383
  factor = min(1.5, 1.0 + 0.08 * max(0, int(level) - 1))
@@ -838,7 +847,7 @@ class DesktopPetWidget(BaseWidget):
838
847
  def __init__(self, image_path: str, size: int = 128, speed: int = 3,
839
848
  behaviour: str = BEHAVIOUR_FLOOR, climb: bool = True, talk: bool = True,
840
849
  sound_path: Optional[str] = None, sit_on_windows: bool = True,
841
- volume: float = 1.0):
850
+ volume: float = 1.0, audio_react: bool = False):
842
851
  front_engine_logger.info(
843
852
  f"[DesktopPetWidget] Init | path={image_path}, size={size}, speed={speed}, "
844
853
  f"behaviour={behaviour}, climb={climb}, talk={talk}, sound={sound_path}, "
@@ -888,6 +897,10 @@ class DesktopPetWidget(BaseWidget):
888
897
  self._init_sound(sound_path)
889
898
  self._sit_on_windows = bool(sit_on_windows)
890
899
  self._platform_tick = 0
900
+ # 音訊視覺化:依可注入的音量 provider 脈動 / Audio-reactive pulse
901
+ self._audio_react = bool(audio_react)
902
+ self._audio_level_provider = None # callable -> level 0..1 or None
903
+ self._audio_scale = 1.0
891
904
  # 多寵物互動 / Multi-pet interaction
892
905
  self._peers_provider = None
893
906
  self._peer_greeted = False
@@ -1026,13 +1039,35 @@ class DesktopPetWidget(BaseWidget):
1026
1039
  kind, obj = self._active
1027
1040
  return obj.currentPixmap() if kind == "movie" else obj
1028
1041
 
1042
+ def set_audio_level_provider(self, provider) -> None:
1043
+ """設定回傳目前音量 (0~1 或 None) 的函式 / Provider of the current audio level."""
1044
+ self._audio_level_provider = provider
1045
+
1046
+ def _update_audio_scale(self) -> None:
1047
+ if not self._audio_react or self._audio_level_provider is None:
1048
+ self._audio_scale = 1.0
1049
+ return
1050
+ try:
1051
+ level = self._audio_level_provider()
1052
+ except Exception: # pragma: no cover - defensive
1053
+ level = None
1054
+ self._audio_scale = 1.0 if level is None else audio_pulse_scale(level)
1055
+
1029
1056
  def draw_content(self, painter: QPainter) -> None:
1030
1057
  pixmap = self._current_pixmap()
1031
1058
  if pixmap is None or pixmap.isNull():
1032
1059
  return
1033
1060
  if self.motion.facing_left:
1034
1061
  pixmap = pixmap.transformed(QTransform().scale(-1, 1))
1035
- painter.drawPixmap(QRect(0, 0, self.width(), self.height()), pixmap)
1062
+ scale = self._audio_scale if self._audio_react else 1.0
1063
+ if scale >= 0.999:
1064
+ painter.drawPixmap(QRect(0, 0, self.width(), self.height()), pixmap)
1065
+ else:
1066
+ # Anchor to the bottom so the feet stay on the ground while it pulses.
1067
+ width = int(self.width() * scale)
1068
+ height = int(self.height() * scale)
1069
+ painter.drawPixmap(
1070
+ QRect((self.width() - width) // 2, self.height() - height, width, height), pixmap)
1036
1071
 
1037
1072
  def start_moving(self, bounds: Tuple[int, int, int, int], interval_ms: int = 33) -> None:
1038
1073
  front_engine_logger.info(f"[DesktopPetWidget] start_moving | bounds={bounds}")
@@ -1306,6 +1341,9 @@ class DesktopPetWidget(BaseWidget):
1306
1341
  x, y = self.motion.step()
1307
1342
  self.move(x, y)
1308
1343
  self._set_active_sprite(self._visual_state())
1344
+ if self._audio_react:
1345
+ self._update_audio_scale()
1346
+ self.update()
1309
1347
  self._peer_tick += 1
1310
1348
  if self._peer_tick >= 5:
1311
1349
  self._peer_tick = 0
@@ -17,6 +17,7 @@ from frontengine.ui.page.utils import (
17
17
  resolve_preferred_monitor,
18
18
  )
19
19
  from frontengine.user_setting.user_setting_file import add_recent_file
20
+ from frontengine.utils.audio_meter.system_audio_meter import system_audio_level
20
21
  from frontengine.utils.logging.loggin_instance import front_engine_logger
21
22
  from frontengine.utils.multi_language.language_wrapper import language_wrapper
22
23
 
@@ -90,6 +91,8 @@ class PetSettingUI(QWidget):
90
91
  for label, value in (("0%", 0), ("25%", 25), ("50%", 50), ("75%", 75), ("100%", 100)):
91
92
  self.volume_combobox.addItem(label, value)
92
93
  self.volume_combobox.setCurrentText("100%")
94
+ self.audio_react_checkbox = QCheckBox(
95
+ language_wrapper.language_word_dict.get("pet_audio_label", "React to audio"))
93
96
 
94
97
  # Start
95
98
  self.start_button = QPushButton(language_wrapper.language_word_dict.get("pet_start", "Spawn pet"))
@@ -124,6 +127,7 @@ class PetSettingUI(QWidget):
124
127
  self.grid_layout.addWidget(self.target_monitor_combobox, 6, 1)
125
128
  self.grid_layout.addWidget(self.volume_label, 6, 2)
126
129
  self.grid_layout.addWidget(self.volume_combobox, 7, 2)
130
+ self.grid_layout.addWidget(self.audio_react_checkbox, 7, 0)
127
131
 
128
132
  def _spawn_pet(self) -> None:
129
133
  """建立、顯示並開始移動一隻寵物(供 Start 與右鍵複製共用)。"""
@@ -139,9 +143,12 @@ class PetSettingUI(QWidget):
139
143
  sound_path=self.pet_sound_path,
140
144
  sit_on_windows=self.sit_checkbox.isChecked(),
141
145
  volume=int(self.volume_combobox.currentData()) / 100.0,
146
+ audio_react=self.audio_react_checkbox.isChecked(),
142
147
  )
143
148
  pet.clone_requested.connect(self._spawn_pet)
144
149
  pet.set_peers_provider(lambda me=pet: self._peer_centers(me))
150
+ if self.audio_react_checkbox.isChecked():
151
+ pet.set_audio_level_provider(system_audio_level) # real system output peak meter
145
152
  pet.set_pet_window_flag()
146
153
  self.pet_list.append(pet)
147
154
  pet.show()
@@ -253,6 +260,7 @@ class PetSettingUI(QWidget):
253
260
  "sit": self.sit_checkbox.isChecked(),
254
261
  "target_monitor": self.target_monitor_combobox.currentText(),
255
262
  "volume": self.volume_combobox.currentText(),
263
+ "audio_react": self.audio_react_checkbox.isChecked(),
256
264
  }
257
265
 
258
266
  def set_state(self, state: dict) -> None:
@@ -288,3 +296,5 @@ class PetSettingUI(QWidget):
288
296
  index = self.volume_combobox.findText(str(state["volume"]))
289
297
  if index >= 0:
290
298
  self.volume_combobox.setCurrentIndex(index)
299
+ if "audio_react" in state:
300
+ self.audio_react_checkbox.setChecked(bool(state["audio_react"]))
@@ -0,0 +1,136 @@
1
+ """
2
+ 以 WASAPI 的 IAudioMeterInformation::GetPeakValue 讀取「預設輸出裝置」的
3
+ 峰值電表 (0~1)。這只是讀取電表數值,不擷取或錄製任何音訊內容(隱私友善),
4
+ 僅 Windows 有效,任何失敗都退化為 None。
5
+
6
+ Read the default output device's peak meter (0..1) via WASAPI's
7
+ IAudioMeterInformation::GetPeakValue. This only reads a meter value — it does
8
+ NOT capture or record any audio content. Windows only; degrades to None.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ import uuid
14
+ from typing import Optional
15
+
16
+ from frontengine.utils.logging.loggin_instance import front_engine_logger
17
+
18
+ # WASAPI / Core Audio identifiers
19
+ _CLSID_MMDeviceEnumerator = "{BCDE0395-E52F-467C-8E3D-C4579291692E}"
20
+ _IID_IMMDeviceEnumerator = "{A95664D2-9614-4F35-A746-DE8DB63617E6}"
21
+ _IID_IAudioMeterInformation = "{C02216F6-8C67-4B5B-9D00-D008E73E0064}"
22
+ _CLSCTX_ALL = 23
23
+ _E_RENDER = 0 # audio output
24
+ _E_CONSOLE = 0 # default role
25
+
26
+
27
+ class SystemAudioMeter:
28
+ """讀取系統輸出峰值電表;建立失敗或非 Windows 時 level() 回傳 None。"""
29
+
30
+ def __init__(self) -> None:
31
+ self._ok = False
32
+ self._meter = None
33
+ self._device = None
34
+ self._enumerator = None
35
+ self._get_peak = None
36
+ self._byref = None
37
+ self._c_float = None
38
+ self._release_method = None
39
+ if sys.platform == "win32":
40
+ try:
41
+ self._init()
42
+ self._ok = True
43
+ except Exception as error: # pragma: no cover - COM boundary
44
+ front_engine_logger.warning(f"[SystemAudioMeter] init failed: {error!r}")
45
+ self._release_all()
46
+
47
+ # --- COM plumbing -------------------------------------------------
48
+ def _init(self) -> None:
49
+ import ctypes
50
+ from ctypes import POINTER, byref, c_float, c_void_p, c_ulong, HRESULT
51
+ from ctypes.wintypes import DWORD
52
+
53
+ class _GUID(ctypes.Structure):
54
+ _fields_ = [
55
+ ("Data1", ctypes.c_uint32),
56
+ ("Data2", ctypes.c_uint16),
57
+ ("Data3", ctypes.c_uint16),
58
+ ("Data4", ctypes.c_uint8 * 8),
59
+ ]
60
+
61
+ def guid(text: str) -> "_GUID":
62
+ value = _GUID()
63
+ ctypes.memmove(byref(value), uuid.UUID(text).bytes_le, ctypes.sizeof(value))
64
+ return value
65
+
66
+ def method(iface, index, restype, argtypes):
67
+ vtable = ctypes.cast(iface, POINTER(POINTER(c_void_p)))
68
+ func_address = vtable[0][index]
69
+ proto = ctypes.WINFUNCTYPE(restype, c_void_p, *argtypes)
70
+ return proto(func_address)
71
+
72
+ ole32 = ctypes.windll.ole32
73
+ ole32.CoInitialize(None) # S_FALSE if already initialised; harmless
74
+
75
+ enumerator = c_void_p()
76
+ clsid = guid(_CLSID_MMDeviceEnumerator)
77
+ iid_enum = guid(_IID_IMMDeviceEnumerator)
78
+ if ole32.CoCreateInstance(byref(clsid), None, _CLSCTX_ALL, byref(iid_enum), byref(enumerator)) != 0 \
79
+ or not enumerator.value:
80
+ raise OSError("CoCreateInstance(MMDeviceEnumerator) failed")
81
+ self._enumerator = enumerator
82
+
83
+ device = c_void_p()
84
+ get_default = method(enumerator, 4, HRESULT, [DWORD, DWORD, POINTER(c_void_p)])
85
+ if get_default(enumerator, _E_RENDER, _E_CONSOLE, byref(device)) != 0 or not device.value:
86
+ raise OSError("GetDefaultAudioEndpoint failed")
87
+ self._device = device
88
+
89
+ meter = c_void_p()
90
+ iid_meter = guid(_IID_IAudioMeterInformation)
91
+ activate = method(device, 3, HRESULT, [POINTER(_GUID), DWORD, c_void_p, POINTER(c_void_p)])
92
+ if activate(device, byref(iid_meter), _CLSCTX_ALL, None, byref(meter)) != 0 or not meter.value:
93
+ raise OSError("Activate(IAudioMeterInformation) failed")
94
+ self._meter = meter
95
+
96
+ self._get_peak = method(meter, 3, HRESULT, [POINTER(c_float)])
97
+ self._byref = byref
98
+ self._c_float = c_float
99
+ self._release_method = lambda iface: method(iface, 2, c_ulong, [])
100
+
101
+ def level(self) -> Optional[float]:
102
+ """回傳目前輸出峰值 0~1,或 None。"""
103
+ if not self._ok or self._meter is None or self._get_peak is None:
104
+ return None
105
+ try:
106
+ peak = self._c_float()
107
+ if self._get_peak(self._meter, self._byref(peak)) != 0:
108
+ return None
109
+ return max(0.0, min(1.0, float(peak.value)))
110
+ except Exception: # pragma: no cover - COM boundary
111
+ return None
112
+
113
+ def _release_all(self) -> None:
114
+ for iface in (self._meter, self._device, self._enumerator):
115
+ if iface is not None and getattr(iface, "value", None) and self._release_method is not None:
116
+ try:
117
+ self._release_method(iface)(iface)
118
+ except Exception as error: # pragma: no cover - COM boundary
119
+ # 釋放個別介面失敗不影響清理流程,記錄後繼續。
120
+ # A single interface failing to release must not abort cleanup.
121
+ front_engine_logger.debug(f"[SystemAudioMeter] release failed: {error!r}")
122
+ self._meter = self._device = self._enumerator = None
123
+ self._ok = False
124
+
125
+
126
+ _meter_singleton: Optional[SystemAudioMeter] = None
127
+
128
+
129
+ def system_audio_level() -> Optional[float]:
130
+ """便利函式:以單例電表回傳目前系統輸出峰值 0~1,或 None。"""
131
+ global _meter_singleton
132
+ if sys.platform != "win32":
133
+ return None
134
+ if _meter_singleton is None:
135
+ _meter_singleton = SystemAudioMeter()
136
+ return _meter_singleton.level()
@@ -247,6 +247,7 @@ english_word_dict = {
247
247
  "pet_talk_label": "Speech bubbles",
248
248
  "pet_sit_label": "Sit on windows",
249
249
  "pet_volume_label": "Volume",
250
+ "pet_audio_label": "React to audio",
250
251
  "pet_chatter_morning": "Good morning!|Rise and shine!|Ready for the day?",
251
252
  "pet_chatter_afternoon": "Good afternoon!|Taking a break?|Keep it up!",
252
253
  "pet_chatter_evening": "Good evening!|How was your day?|Almost done?",
@@ -248,6 +248,7 @@ traditional_chinese_word_dict = {
248
248
  "pet_talk_label": "對話泡泡",
249
249
  "pet_sit_label": "站在視窗上",
250
250
  "pet_volume_label": "音量",
251
+ "pet_audio_label": "隨音訊律動",
251
252
  "pet_chatter_morning": "早安!|新的一天開始囉!|準備好了嗎?",
252
253
  "pet_chatter_afternoon": "午安!|要不要休息一下?|繼續加油!",
253
254
  "pet_chatter_evening": "晚上好!|今天過得如何?|快完成了嗎?",
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: frontengine
3
- Version: 1.0.22
3
+ Version: 1.0.24
4
4
  Summary: FrontEngine is use-define frontview or only use like screen saver
5
5
  Author-email: JE-Chen <jechenmailman@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Intergration-Automation-Testing/FrontEngine
@@ -90,6 +90,8 @@ frontengine/user_setting/scene_setting.py
90
90
  frontengine/user_setting/user_setting_file.py
91
91
  frontengine/utils/__init__.py
92
92
  frontengine/utils/web_url.py
93
+ frontengine/utils/audio_meter/__init__.py
94
+ frontengine/utils/audio_meter/system_audio_meter.py
93
95
  frontengine/utils/browser/__init__.py
94
96
  frontengine/utils/browser/browser.py
95
97
  frontengine/utils/critical_exit/__init__.py
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "frontengine"
9
- version = "1.0.22"
9
+ version = "1.0.24"
10
10
  authors = [
11
11
  { name = "JE-Chen", email = "jechenmailman@gmail.com" },
12
12
  ]
File without changes
File without changes
File without changes