GameSentenceMiner 2.18.6__py3-none-any.whl → 2.18.8__py3-none-any.whl

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.
GameSentenceMiner/anki.py CHANGED
@@ -234,7 +234,7 @@ def update_anki_card(last_note: 'AnkiCard', note=None, audio_path='', video_path
234
234
  sentence = note['fields'].get(config.anki.sentence_field, last_note.get_field(config.anki.sentence_field))
235
235
 
236
236
  use_voice, sentence, translation, new_ss_path = config_app.show_anki_confirmation_dialog(
237
- tango, sentence, assets.screenshot_path, assets.audio_path, translation, ss_time
237
+ tango, sentence, assets.screenshot_path, assets.audio_path if update_audio_flag else None, translation, ss_time
238
238
  )
239
239
  note['fields'][config.anki.sentence_field] = sentence
240
240
  note['fields'][config.ai.anki_field] = translation
@@ -279,7 +279,8 @@ def update_anki_card(last_note: 'AnkiCard', note=None, audio_path='', video_path
279
279
  sentence_in_anki=game_line.text if game_line else '',
280
280
  multi_line=bool(selected_lines and len(selected_lines) > 1),
281
281
  video_in_anki=assets.video_in_anki or '',
282
- word_path=word_path
282
+ word_path=word_path,
283
+ word=tango
283
284
  )
284
285
 
285
286
  # 9. Update the local application database with final paths
@@ -554,14 +555,17 @@ def update_new_card():
554
555
  else:
555
556
  logger.info("New card(s) detected! Added to Processing Queue!")
556
557
  gsm_state.last_mined_line = game_line
557
- card_queue.append((last_card, datetime.now(), lines))
558
- texthooking_page.reset_checked_lines()
559
- try:
560
- obs.save_replay_buffer()
561
- except Exception as e:
562
- card_queue.pop(0)
563
- logger.error(f"Error saving replay buffer: {e}")
564
- return
558
+ queue_card_for_processing(last_card, lines)
559
+
560
+ def queue_card_for_processing(last_card, lines):
561
+ card_queue.append((last_card, datetime.now(), lines))
562
+ texthooking_page.reset_checked_lines()
563
+ try:
564
+ obs.save_replay_buffer()
565
+ except Exception as e:
566
+ card_queue.pop(0)
567
+ logger.error(f"Error saving replay buffer: {e}")
568
+ return
565
569
 
566
570
  def update_card_from_same_sentence(last_card, lines, game_line):
567
571
  time_elapsed = 0
@@ -570,16 +574,15 @@ def update_card_from_same_sentence(last_card, lines, game_line):
570
574
  time_elapsed += 0.5
571
575
  if time_elapsed > 15:
572
576
  logger.info(f"Timed out waiting for Anki update for card {last_card.noteId}, retrieving new audio")
573
- card_queue.append((last_card, datetime.now(), lines))
574
- texthooking_page.reset_checked_lines()
575
- try:
576
- obs.save_replay_buffer()
577
- except Exception as e:
578
- card_queue.pop(0)
579
- logger.error(f"Error saving replay buffer: {e}")
580
- return
577
+ queue_card_for_processing(last_card, lines)
581
578
  return
582
579
  anki_result = anki_results[game_line.id]
580
+
581
+ if anki_result.word == last_card.get_field(get_config().anki.word_field):
582
+ logger.info(f"Same word detected, attempting to get new audio for card {last_card.noteId}")
583
+ queue_card_for_processing(last_card, lines)
584
+ return
585
+
583
586
  if anki_result.success:
584
587
  note, last_card = get_initial_card_info(last_card, lines)
585
588
  tango = last_card.get_field(get_config().anki.word_field)
@@ -497,11 +497,11 @@
497
497
  },
498
498
  "texthooker_comm_port": {
499
499
  "label": "Texthooker Communication WebSocket Port:",
500
- "tooltip": "Port for GSM Texthooker WebSocket communication. Does nothing right now, hardcoded to 55001"
500
+ "tooltip": "Port for GSM Texthooker WebSocket communication. "
501
501
  },
502
502
  "plaintext_export_port": {
503
- "label": "Plaintext Websocket Export Port:",
504
- "tooltip": "Port for GSM Plaintext WebSocket Export communication. Does nothing right now, hardcoded to 55002"
503
+ "label": "Plaintext/JL Websocket Export Port:",
504
+ "tooltip": "Port that GSM will send plaintext to. Useful for integrating with other tools like JL."
505
505
  },
506
506
  "reset_line_hotkey": {
507
507
  "label": "Reset Line Hotkey:",
@@ -495,12 +495,12 @@
495
495
  "tooltip": "OCR WebSocket通信用のポート。GSMもこのポートで待機します。"
496
496
  },
497
497
  "texthooker_comm_port": {
498
- "label": "Texthooker通信ポート:",
499
- "tooltip": "GSM Texthooker通信用のWebSocketポート(現在未使用)。"
498
+ "label": "Texthooker通信WebSocketポート:",
499
+ "tooltip": "GSM Texthooker WebSocket通信用のポート。"
500
500
  },
501
501
  "plaintext_export_port": {
502
- "label": "平文エクスポートポート:",
503
- "tooltip": "GSM平文エクスポート用のWebSocketポート(現在未使用)。"
502
+ "label": "平文/JL Websocketエクスポートポート:",
503
+ "tooltip": "GSMが平文を送信するポート。JLなどの他のツールとの統合に便利です。"
504
504
  },
505
505
  "reset_line_hotkey": {
506
506
  "label": "行リセットホットキー:",
@@ -497,11 +497,11 @@
497
497
  },
498
498
  "texthooker_comm_port": {
499
499
  "label": "Texthooker 通信 WebSocket 端口:",
500
- "tooltip": "GSM Texthooker WebSocket 通信的端口。目前不起作用,硬编码为 55001。"
500
+ "tooltip": "GSM Texthooker WebSocket 通信的端口。"
501
501
  },
502
502
  "plaintext_export_port": {
503
- "label": "纯文本 WebSocket 导出端口:",
504
- "tooltip": "GSM 纯文本 WebSocket 导出通信的端口。目前不起作用,硬编码为 55002。"
503
+ "label": "纯文本/JL Websocket 导出端口:",
504
+ "tooltip": "GSM 将纯文本发送到的端口。对于与 JL 等其他工具集成很有用。"
505
505
  },
506
506
  "reset_line_hotkey": {
507
507
  "label": "重置行热键:",
@@ -4,6 +4,7 @@ from PIL import Image, ImageTk
4
4
 
5
5
  import ttkbootstrap as ttk
6
6
  from GameSentenceMiner.util.configuration import get_config, logger, gsm_state
7
+ from GameSentenceMiner.util.audio_player import AudioPlayer
7
8
 
8
9
  import platform
9
10
  import subprocess
@@ -17,10 +18,16 @@ class AnkiConfirmationDialog(tk.Toplevel):
17
18
  super().__init__(parent)
18
19
  self.config_app = config_app
19
20
  self.screenshot_timestamp = screenshot_timestamp
21
+ self.translation_text = None
22
+ self.sentence_text = None
20
23
 
21
24
  # Initialize screenshot_path here, will be updated by button if needed
22
25
  self.screenshot_path = screenshot_path
23
26
 
27
+ # Audio player management
28
+ self.audio_player = AudioPlayer(finished_callback=self._audio_finished)
29
+ self.audio_button = None # Store reference to audio button
30
+
24
31
  self.title("Confirm Anki Card Details")
25
32
  self.result = None # This will store the user's choice
26
33
 
@@ -55,6 +62,10 @@ class AnkiConfirmationDialog(tk.Toplevel):
55
62
 
56
63
  self.protocol("WM_DELETE_WINDOW", self._on_cancel)
57
64
  self.attributes('-topmost', True)
65
+
66
+ # Ensure audio cleanup on window close
67
+ self.protocol("WM_DELETE_WINDOW", self._cleanup_and_close)
68
+
58
69
  self.wait_window(self)
59
70
 
60
71
  def _create_widgets(self, expression, sentence, screenshot_path, audio_path, translation):
@@ -76,13 +87,14 @@ class AnkiConfirmationDialog(tk.Toplevel):
76
87
  self.sentence_text = sentence_text
77
88
  row += 1
78
89
 
79
- # Translation
80
- ttk.Label(main_frame, text=f"{get_config().ai.anki_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
81
- translation_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
82
- translation_text.insert(tk.END, translation)
83
- translation_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
84
- self.translation_text = translation_text
85
- row += 1
90
+ if translation:
91
+ # Translation
92
+ ttk.Label(main_frame, text=f"{get_config().ai.anki_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
93
+ translation_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
94
+ translation_text.insert(tk.END, translation)
95
+ translation_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
96
+ self.translation_text = translation_text
97
+ row += 1
86
98
 
87
99
  # Screenshot
88
100
  ttk.Label(main_frame, text=f"{get_config().anki.picture_field}:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
@@ -109,12 +121,20 @@ class AnkiConfirmationDialog(tk.Toplevel):
109
121
  row += 1
110
122
 
111
123
  # Audio Path
112
- ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
113
- ttk.Label(main_frame, text=audio_path if audio_path else "No Audio", wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
114
124
  if audio_path and os.path.isfile(audio_path):
115
- ttk.Button(main_frame, text="Play Audio", command=lambda: self._play_audio(audio_path)).grid(row=row, column=2, sticky="w", padx=5, pady=2)
116
-
117
- row += 1
125
+ ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
126
+ ttk.Label(main_frame, text=audio_path if audio_path else "No Audio", wraplength=400, justify="left").grid(row=row, column=1, sticky="w", padx=5, pady=2)
127
+ if audio_path and os.path.isfile(audio_path):
128
+ self.audio_button = ttk.Button(
129
+ main_frame,
130
+ text="▶",
131
+ command=lambda: self._play_audio(audio_path),
132
+ bootstyle="outline-info",
133
+ width=12
134
+ )
135
+ self.audio_button.grid(row=row, column=2, sticky="w", padx=5, pady=2)
136
+
137
+ row += 1
118
138
 
119
139
  # Action Buttons
120
140
  button_frame = ttk.Frame(main_frame)
@@ -161,23 +181,60 @@ class AnkiConfirmationDialog(tk.Toplevel):
161
181
  if not os.path.isfile(audio_path):
162
182
  print(f"Audio file does not exist: {audio_path}")
163
183
  return
184
+
164
185
  try:
165
- if platform.system() == "Windows":
166
- os.startfile(audio_path)
167
- elif platform.system() == "Darwin":
168
- subprocess.run(["open", audio_path])
186
+ # Check if we have a configuration for external audio player
187
+ if get_config().advanced.audio_player_path:
188
+ # Use external audio player
189
+ import platform
190
+ import subprocess
191
+ if platform.system() == "Windows":
192
+ os.startfile(audio_path)
193
+ elif platform.system() == "Darwin":
194
+ subprocess.run(["open", audio_path])
195
+ else:
196
+ subprocess.run(["xdg-open", audio_path])
169
197
  else:
170
- subprocess.run(["xdg-open", audio_path])
198
+ # Use internal audio player
199
+ success = self.audio_player.play_audio_file(audio_path)
200
+ if success:
201
+ self._update_audio_button()
202
+
171
203
  except Exception as e:
172
204
  print(f"Failed to play audio: {e}")
205
+
206
+ def _audio_finished(self):
207
+ """Called when audio playback finishes"""
208
+ self._update_audio_button()
209
+
210
+ def _update_audio_button(self):
211
+ """Update the audio button text and style based on playing state"""
212
+ if self.audio_button:
213
+ if self.audio_player.is_playing:
214
+ self.audio_button.config(text="⏹ Stop", bootstyle="outline-warning")
215
+ else:
216
+ self.audio_button.config(text="▶ Play Audio", bootstyle="outline-info")
217
+
218
+ def _cleanup_audio(self):
219
+ """Clean up audio stream resources"""
220
+ self.audio_player.cleanup()
221
+
222
+ def _cleanup_and_close(self):
223
+ """Clean up resources and close dialog"""
224
+ self._cleanup_audio()
225
+ self._on_cancel()
173
226
 
174
227
  def _on_voice(self):
228
+ # Clean up audio before closing
229
+ self._cleanup_audio()
175
230
  # The screenshot_path is now correctly updated if the user chose a new one
176
- self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
231
+ self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip() if self.translation_text else None, self.screenshot_path)
177
232
  self.destroy()
178
233
 
179
234
  def _on_no_voice(self):
180
- self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
235
+ # Clean up audio before closing
236
+ self._cleanup_audio()
237
+ self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip() if self.translation_text else None, self.screenshot_path)
181
238
  self.destroy()
182
239
 
183
240
  def _on_cancel(self):
@@ -698,7 +698,7 @@ class ConfigApp:
698
698
  ),
699
699
  overlay=Overlay(
700
700
  websocket_port=int(self.overlay_websocket_port_value.get()),
701
- monitor_to_capture=self.overlay_monitor.current() if self.monitors else 0,
701
+ monitor_to_capture=int(self.overlay_monitor.current() if self.monitors else 0),
702
702
  engine=OverlayEngine(self.overlay_engine_value.get()).value if self.overlay_engine_value.get() else OverlayEngine.LENS.value,
703
703
  scan_delay=float(self.scan_delay_value.get()),
704
704
  periodic=float(self.periodic_value.get()),
@@ -732,6 +732,7 @@ class ConfigApp:
732
732
  self.master_config.set_config_for_profile(current_profile, config)
733
733
 
734
734
  self.master_config.locale = Locale[self.locale_value.get()].value
735
+ self.master_config.overlay = config.overlay
735
736
 
736
737
 
737
738
  config_backup_folder = os.path.join(get_app_directory(), "backup", "config")
@@ -877,6 +878,14 @@ class ConfigApp:
877
878
  ttk.Entry(self.general_tab, textvariable=self.texthooker_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
878
879
  self.current_row += 1
879
880
 
881
+ advanced_i18n = self.i18n.get('tabs', {}).get('advanced', {})
882
+ export_port_i18n = advanced_i18n.get('plaintext_export_port', {})
883
+ HoverInfoLabelWidget(self.general_tab, text=export_port_i18n.get('label', '...'),
884
+ tooltip=export_port_i18n.get('tooltip', '...'),
885
+ row=self.current_row, column=0)
886
+ ttk.Entry(self.general_tab, textvariable=self.plaintext_websocket_export_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
887
+ self.current_row += 1
888
+
880
889
  # locale_i18n = general_i18n.get('locale', {})
881
890
  # HoverInfoLabelWidget(self.general_tab, text=locale_i18n.get('label', 'Locale:'),
882
891
  # tooltip=locale_i18n.get('tooltip', '...'),
@@ -2047,13 +2056,6 @@ class ConfigApp:
2047
2056
  ttk.Entry(advanced_frame, textvariable=self.texthooker_communication_websocket_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
2048
2057
  self.current_row += 1
2049
2058
 
2050
- export_port_i18n = advanced_i18n.get('plaintext_export_port', {})
2051
- HoverInfoLabelWidget(advanced_frame, text=export_port_i18n.get('label', '...'),
2052
- tooltip=export_port_i18n.get('tooltip', '...'),
2053
- row=self.current_row, column=0)
2054
- ttk.Entry(advanced_frame, textvariable=self.plaintext_websocket_export_port_value).grid(row=self.current_row, column=1, sticky='EW', pady=2)
2055
- self.current_row += 1
2056
-
2057
2059
  reset_hotkey_i18n = advanced_i18n.get('reset_line_hotkey', {})
2058
2060
  HoverInfoLabelWidget(advanced_frame, text=reset_hotkey_i18n.get('label', '...'),
2059
2061
  tooltip=reset_hotkey_i18n.get('tooltip', '...'), row=self.current_row, column=0)
@@ -2342,13 +2344,14 @@ class ConfigApp:
2342
2344
 
2343
2345
  overlay_monitor_i18n = overlay_i18n.get('overlay_monitor', {})
2344
2346
  HoverInfoLabelWidget(overlay_frame, text=overlay_monitor_i18n.get('label', '...'),
2345
- tooltip=overlay_monitor_i18n.get('tooltip', '...'),
2346
- row=self.current_row, column=0)
2347
- self.overlay_monitor = ttk.Combobox(overlay_frame, values=self.monitors, state="readonly")
2347
+ tooltip=overlay_monitor_i18n.get('tooltip', '...'),
2348
+ row=self.current_row, column=0)
2349
+ self.overlay_monitor = ttk.Combobox(overlay_frame, values=self.monitors)
2348
2350
  self.overlay_monitor.grid(row=self.current_row, column=1, sticky='EW', pady=2)
2349
2351
  # disable selection for now, default to value 1
2350
- self.overlay_monitor.current(0)
2351
- self.overlay_monitor.config(state="disabled")
2352
+ if self.settings.overlay.monitor_to_capture >= len(self.monitors):
2353
+ self.settings.overlay.monitor_to_capture = 0
2354
+ self.overlay_monitor.current(self.settings.overlay.monitor_to_capture)
2352
2355
  self.current_row += 1
2353
2356
 
2354
2357
  # Overlay Engine Selection
@@ -0,0 +1,220 @@
1
+ """
2
+ Audio playback utility module for GameSentenceMiner.
3
+ Provides safe, non-blocking audio playback functionality.
4
+ """
5
+
6
+ import threading
7
+ from typing import Optional, Callable
8
+ import sounddevice as sd
9
+ import soundfile as sf
10
+ import numpy as np
11
+
12
+
13
+ class AudioPlayer:
14
+ """
15
+ A safe, non-blocking audio player class that handles audio stream management.
16
+ """
17
+
18
+ def __init__(self, finished_callback: Optional[Callable] = None):
19
+ """
20
+ Initialize the audio player.
21
+
22
+ Args:
23
+ finished_callback: Optional callback function to call when audio finishes playing
24
+ """
25
+ self.current_audio_stream: Optional[sd.OutputStream] = None
26
+ self.current_audio_data: Optional[np.ndarray] = None
27
+ self.current_audio_samplerate: Optional[int] = None
28
+ self.is_playing: bool = False
29
+ self._audio_position: int = 0
30
+ self.finished_callback = finished_callback
31
+ self._lock = threading.Lock()
32
+
33
+ def play_audio_file(self, audio_path: str) -> bool:
34
+ """
35
+ Play an audio file. If already playing, stop the current playback.
36
+
37
+ Args:
38
+ audio_path: Path to the audio file to play
39
+
40
+ Returns:
41
+ True if playback started successfully, False otherwise
42
+ """
43
+ try:
44
+ # If audio is currently playing, stop it
45
+ if self.is_playing:
46
+ self.stop_audio()
47
+ return True
48
+
49
+ # Load audio data
50
+ data, samplerate = sf.read(audio_path)
51
+ return self.play_audio_data(data, samplerate)
52
+
53
+ except Exception as e:
54
+ print(f"Failed to play audio file {audio_path}: {e}")
55
+ self._cleanup_stream()
56
+ return False
57
+
58
+ def play_audio_data(self, data: np.ndarray, samplerate: int) -> bool:
59
+ """
60
+ Play audio from numpy array data.
61
+
62
+ Args:
63
+ data: Audio data as numpy array
64
+ samplerate: Sample rate of the audio data
65
+
66
+ Returns:
67
+ True if playback started successfully, False otherwise
68
+ """
69
+ try:
70
+ with self._lock:
71
+ # Ensure data is float32 for sounddevice playback
72
+ data = data.astype('float32')
73
+
74
+ # Store audio data
75
+ self.current_audio_data = data
76
+ self.current_audio_samplerate = samplerate
77
+ self._audio_position = 0
78
+
79
+ # Create audio callback
80
+ def audio_callback(outdata, frames, time, status):
81
+ if status:
82
+ print(f"Audio callback status: {status}")
83
+
84
+ # Calculate how much data we need for this callback
85
+ start_frame = self._audio_position
86
+ end_frame = start_frame + frames
87
+
88
+ if end_frame <= len(data):
89
+ # We have enough data
90
+ if data.ndim == 1:
91
+ outdata[:, 0] = data[start_frame:end_frame]
92
+ else:
93
+ outdata[:] = data[start_frame:end_frame]
94
+ self._audio_position = end_frame
95
+ else:
96
+ # We've reached the end
97
+ remaining_frames = len(data) - start_frame
98
+ if remaining_frames > 0:
99
+ if data.ndim == 1:
100
+ outdata[:remaining_frames, 0] = data[start_frame:]
101
+ outdata[remaining_frames:, 0] = 0
102
+ else:
103
+ outdata[:remaining_frames] = data[start_frame:]
104
+ outdata[remaining_frames:] = 0
105
+ else:
106
+ outdata.fill(0)
107
+
108
+ # Schedule cleanup
109
+ self._schedule_finish()
110
+
111
+ # Create and start audio stream
112
+ stream = sd.OutputStream(
113
+ samplerate=samplerate,
114
+ channels=data.shape[1] if data.ndim > 1 else 1,
115
+ callback=audio_callback
116
+ )
117
+
118
+ self.current_audio_stream = stream
119
+ self.is_playing = True
120
+ stream.start()
121
+
122
+ return True
123
+
124
+ except Exception as e:
125
+ print(f"Failed to play audio data: {e}")
126
+ self._cleanup_stream()
127
+ return False
128
+
129
+ def stop_audio(self):
130
+ """Stop the currently playing audio."""
131
+ with self._lock:
132
+ if self.current_audio_stream and self.is_playing:
133
+ try:
134
+ self.current_audio_stream.stop()
135
+ except Exception:
136
+ pass
137
+ self._cleanup_stream()
138
+
139
+ def _schedule_finish(self):
140
+ """Schedule the finish callback to be called."""
141
+ # Use threading to avoid blocking the audio callback
142
+ def finish_task():
143
+ self._audio_finished()
144
+
145
+ threading.Thread(target=finish_task, daemon=True).start()
146
+
147
+ def _audio_finished(self):
148
+ """Called when audio playback finishes."""
149
+ with self._lock:
150
+ self._cleanup_stream()
151
+ if self.finished_callback:
152
+ try:
153
+ self.finished_callback()
154
+ except Exception as e:
155
+ print(f"Error in audio finished callback: {e}")
156
+
157
+ def _cleanup_stream(self):
158
+ """Clean up the current audio stream."""
159
+ if self.current_audio_stream:
160
+ try:
161
+ self.current_audio_stream.close()
162
+ except Exception:
163
+ pass
164
+
165
+ self.current_audio_stream = None
166
+ self.is_playing = False
167
+ self.current_audio_data = None
168
+ self.current_audio_samplerate = None
169
+ self._audio_position = 0
170
+
171
+ def cleanup(self):
172
+ """Clean up all resources."""
173
+ self.stop_audio()
174
+ self.finished_callback = None
175
+
176
+
177
+ def create_safe_audio_callback(data: np.ndarray, position_tracker: dict, finish_callback: Callable):
178
+ """
179
+ Create a safe audio callback function for use with sounddevice.
180
+
181
+ Args:
182
+ data: Audio data array
183
+ position_tracker: Dictionary to track playback position (mutable)
184
+ finish_callback: Function to call when playback finishes
185
+
186
+ Returns:
187
+ Audio callback function
188
+ """
189
+ def audio_callback(outdata, frames, time, status):
190
+ if status:
191
+ print(f"Audio callback status: {status}")
192
+
193
+ # Calculate how much data we need for this callback
194
+ start_frame = position_tracker.get('position', 0)
195
+ end_frame = start_frame + frames
196
+
197
+ if end_frame <= len(data):
198
+ # We have enough data
199
+ if data.ndim == 1:
200
+ outdata[:, 0] = data[start_frame:end_frame]
201
+ else:
202
+ outdata[:] = data[start_frame:end_frame]
203
+ position_tracker['position'] = end_frame
204
+ else:
205
+ # We've reached the end
206
+ remaining_frames = len(data) - start_frame
207
+ if remaining_frames > 0:
208
+ if data.ndim == 1:
209
+ outdata[:remaining_frames, 0] = data[start_frame:]
210
+ outdata[remaining_frames:, 0] = 0
211
+ else:
212
+ outdata[:remaining_frames] = data[start_frame:]
213
+ outdata[remaining_frames:] = 0
214
+ else:
215
+ outdata.fill(0)
216
+
217
+ # Schedule cleanup
218
+ threading.Thread(target=finish_callback, daemon=True).start()
219
+
220
+ return audio_callback
@@ -666,7 +666,17 @@ class Overlay:
666
666
  def __post_init__(self):
667
667
  if self.monitor_to_capture == -1:
668
668
  self.monitor_to_capture = 0 # Default to the first monitor if not set
669
-
669
+
670
+ try:
671
+ import mss as mss
672
+ monitors = [f"Monitor {i}: width: {monitor['width']}, height: {monitor['height']}" for i, monitor in enumerate(mss.mss().monitors[1:], start=1)]
673
+ if len(monitors) == 0:
674
+ monitors = [1]
675
+ self.monitors = monitors
676
+ except ImportError:
677
+ self.monitors = []
678
+ if self.monitor_to_capture >= len(self.monitors):
679
+ self.monitor_to_capture = 0 # Reset to first monitor if out of range
670
680
 
671
681
  @dataclass_json
672
682
  @dataclass
@@ -823,6 +833,7 @@ class Config:
823
833
  switch_to_default_if_not_found: bool = True
824
834
  locale: str = Locale.English.value
825
835
  stats: StatsConfig = field(default_factory=StatsConfig)
836
+ overlay: Overlay = field(default_factory=Overlay)
826
837
  version: str = ""
827
838
 
828
839
  @classmethod
@@ -865,7 +876,7 @@ class Config:
865
876
  if self.version:
866
877
  if self.version != get_current_version():
867
878
  from packaging import version
868
- logger.info(f"Config version mismatch detected: {self.version} != {get_current_version()}")
879
+ logger.info(f"New Config Found: {self.version} != {get_current_version()}")
869
880
  # Handle version mismatch
870
881
  changed = False
871
882
  if version.parse(self.version) < version.parse("2.18.0"):
@@ -879,6 +890,7 @@ class Config:
879
890
 
880
891
  if changed:
881
892
  self.save()
893
+ self.overlay = self.get_config().overlay
882
894
 
883
895
  self.version = get_current_version()
884
896
 
@@ -1160,6 +1172,11 @@ def get_config():
1160
1172
  # print(config_instance.get_config())
1161
1173
  return config_instance.get_config()
1162
1174
 
1175
+ def get_overlay_config():
1176
+ global config_instance
1177
+ if config_instance is None:
1178
+ config_instance = load_config()
1179
+ return config_instance.overlay
1163
1180
 
1164
1181
  def reload_config():
1165
1182
  global config_instance
@@ -1284,6 +1301,7 @@ class GsmAppState:
1284
1301
  self.recording_started_time = None
1285
1302
  self.current_srt = None
1286
1303
  self.srt_index = 1
1304
+ self.current_audio_stream = None
1287
1305
 
1288
1306
 
1289
1307
  @dataclass_json
@@ -1297,10 +1315,11 @@ class AnkiUpdateResult:
1297
1315
  multi_line: bool = False
1298
1316
  video_in_anki: str = ''
1299
1317
  word_path: str = ''
1318
+ word: str = ''
1300
1319
 
1301
1320
  @staticmethod
1302
1321
  def failure():
1303
- return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False, video_in_anki='', word_path='')
1322
+ return AnkiUpdateResult(success=False, audio_in_anki='', screenshot_in_anki='', prev_screenshot_in_anki='', sentence_in_anki='', multi_line=False, video_in_anki='', word_path='', word='')
1304
1323
 
1305
1324
 
1306
1325
  @dataclass_json