GameSentenceMiner 2.18.6__tar.gz → 2.18.7__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.
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/anki.py +21 -18
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/locales/en_us.json +3 -3
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/locales/ja_jp.json +4 -4
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/locales/zh_cn.json +3 -3
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ui/anki_confirmation.py +72 -17
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ui/config_gui.py +16 -13
- gamesentenceminer-2.18.7/GameSentenceMiner/util/audio_player.py +220 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/configuration.py +22 -3
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/get_overlay_coords.py +93 -36
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/vad.py +18 -12
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/service.py +68 -17
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/js/overview.js +253 -33
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/overview.html +38 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/texthooking_page.py +17 -2
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner.egg-info/PKG-INFO +1 -1
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner.egg-info/SOURCES.txt +1 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/PKG-INFO +1 -1
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/pyproject.toml +1 -1
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ai/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ai/ai_prompting.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/assets/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/assets/icon.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/assets/icon128.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/assets/icon256.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/assets/icon32.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/assets/icon512.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/assets/icon64.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/assets/pickaxe.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/gametext.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/gsm.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/obs.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ocr/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ocr/ss_picker.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/owocr/owocr/config.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/owocr/owocr/run.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/tools/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/tools/audio_offset_selector.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/tools/furigana_filter_preview.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/tools/ss_selector.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/tools/window_transparency.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ui/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ui/screenshot_selector.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/communication/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/communication/send.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/communication/websocket.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/db.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/downloader/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/electron_config.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/ffmpeg.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/gsm_utils.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/model.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/notification.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/text_log.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/win10toast/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/win10toast/__main__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/database_api.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/events.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/gsm_websocket.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/__init__.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/css/dashboard-shared.css +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/css/kanji-grid.css +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/css/overview.css +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/css/popups-shared.css +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/css/search.css +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/css/shared.css +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/css/stats.css +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/favicon.ico +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/favicon.svg +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/js/anki_stats.js +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/js/database.js +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/js/goals.js +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/js/kanji-grid.js +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/js/search.js +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/js/shared.js +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/js/stats.js +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/site.webmanifest +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/style.css +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/stats.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/anki_stats.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/basic_kanji_book_bkb_v1_v2.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/duolingo_kanji.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/grade.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/hk_primary_learning.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/hkscs2016.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/hsk_levels.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/humanum_frequency_list.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/jis_levels.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/jlpt_level.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/jpdb_kanji_frequency_list.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/jpdbv2_kanji_frequency_list.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/jun_das_modern_chinese_character_frequency_list.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/kanji_in_context_revised_edition.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/kanji_kentei_level.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/mainland_china_elementary_textbook_characters.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/moe_way_quiz.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/official_kanji.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/remembering_the_kanji.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/standard_form_of_national_characters.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/table_of_general_standard_chinese_characters.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/the_kodansha_kanji_learners_course_klc.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/navigation.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/components/theme-styles.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/database.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/goals.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/index.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/search.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/stats.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/web/templates/utility.html +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/wip/__init___.py +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner.egg-info/requires.txt +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner.egg-info/top_level.txt +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/LICENSE +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/README.md +0 -0
- {gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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.
|
|
500
|
+
"tooltip": "Port for GSM Texthooker WebSocket communication. "
|
|
501
501
|
},
|
|
502
502
|
"plaintext_export_port": {
|
|
503
|
-
"label": "Plaintext Websocket Export Port:",
|
|
504
|
-
"tooltip": "Port
|
|
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
|
|
498
|
+
"label": "Texthooker通信WebSocketポート:",
|
|
499
|
+
"tooltip": "GSM Texthooker WebSocket通信用のポート。"
|
|
500
500
|
},
|
|
501
501
|
"plaintext_export_port": {
|
|
502
|
-
"label": "
|
|
503
|
-
"tooltip": "GSM
|
|
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
|
|
500
|
+
"tooltip": "GSM Texthooker WebSocket 通信的端口。"
|
|
501
501
|
},
|
|
502
502
|
"plaintext_export_port": {
|
|
503
|
-
"label": "
|
|
504
|
-
"tooltip": "GSM
|
|
503
|
+
"label": "纯文本/JL Websocket 导出端口:",
|
|
504
|
+
"tooltip": "GSM 将纯文本发送到的端口。对于与 JL 等其他工具集成很有用。"
|
|
505
505
|
},
|
|
506
506
|
"reset_line_hotkey": {
|
|
507
507
|
"label": "重置行热键:",
|
{gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/ui/anki_confirmation.py
RENAMED
|
@@ -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
|
|
@@ -21,6 +22,10 @@ class AnkiConfirmationDialog(tk.Toplevel):
|
|
|
21
22
|
# Initialize screenshot_path here, will be updated by button if needed
|
|
22
23
|
self.screenshot_path = screenshot_path
|
|
23
24
|
|
|
25
|
+
# Audio player management
|
|
26
|
+
self.audio_player = AudioPlayer(finished_callback=self._audio_finished)
|
|
27
|
+
self.audio_button = None # Store reference to audio button
|
|
28
|
+
|
|
24
29
|
self.title("Confirm Anki Card Details")
|
|
25
30
|
self.result = None # This will store the user's choice
|
|
26
31
|
|
|
@@ -55,6 +60,10 @@ class AnkiConfirmationDialog(tk.Toplevel):
|
|
|
55
60
|
|
|
56
61
|
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
|
|
57
62
|
self.attributes('-topmost', True)
|
|
63
|
+
|
|
64
|
+
# Ensure audio cleanup on window close
|
|
65
|
+
self.protocol("WM_DELETE_WINDOW", self._cleanup_and_close)
|
|
66
|
+
|
|
58
67
|
self.wait_window(self)
|
|
59
68
|
|
|
60
69
|
def _create_widgets(self, expression, sentence, screenshot_path, audio_path, translation):
|
|
@@ -76,13 +85,14 @@ class AnkiConfirmationDialog(tk.Toplevel):
|
|
|
76
85
|
self.sentence_text = sentence_text
|
|
77
86
|
row += 1
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
if translation:
|
|
89
|
+
# Translation
|
|
90
|
+
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)
|
|
91
|
+
translation_text = scrolledtext.ScrolledText(main_frame, height=4, width=50, wrap=tk.WORD)
|
|
92
|
+
translation_text.insert(tk.END, translation)
|
|
93
|
+
translation_text.grid(row=row, column=1, sticky="w", padx=5, pady=2)
|
|
94
|
+
self.translation_text = translation_text
|
|
95
|
+
row += 1
|
|
86
96
|
|
|
87
97
|
# Screenshot
|
|
88
98
|
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 +119,20 @@ class AnkiConfirmationDialog(tk.Toplevel):
|
|
|
109
119
|
row += 1
|
|
110
120
|
|
|
111
121
|
# 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
122
|
if audio_path and os.path.isfile(audio_path):
|
|
115
|
-
ttk.
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
ttk.Label(main_frame, text="Audio Path:", font=("-weight bold")).grid(row=row, column=0, sticky="ne", padx=5, pady=2)
|
|
124
|
+
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)
|
|
125
|
+
if audio_path and os.path.isfile(audio_path):
|
|
126
|
+
self.audio_button = ttk.Button(
|
|
127
|
+
main_frame,
|
|
128
|
+
text="▶",
|
|
129
|
+
command=lambda: self._play_audio(audio_path),
|
|
130
|
+
bootstyle="outline-info",
|
|
131
|
+
width=12
|
|
132
|
+
)
|
|
133
|
+
self.audio_button.grid(row=row, column=2, sticky="w", padx=5, pady=2)
|
|
134
|
+
|
|
135
|
+
row += 1
|
|
118
136
|
|
|
119
137
|
# Action Buttons
|
|
120
138
|
button_frame = ttk.Frame(main_frame)
|
|
@@ -161,22 +179,59 @@ class AnkiConfirmationDialog(tk.Toplevel):
|
|
|
161
179
|
if not os.path.isfile(audio_path):
|
|
162
180
|
print(f"Audio file does not exist: {audio_path}")
|
|
163
181
|
return
|
|
182
|
+
|
|
164
183
|
try:
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
184
|
+
# Check if we have a configuration for external audio player
|
|
185
|
+
if get_config().advanced.audio_player_path:
|
|
186
|
+
# Use external audio player
|
|
187
|
+
import platform
|
|
188
|
+
import subprocess
|
|
189
|
+
if platform.system() == "Windows":
|
|
190
|
+
os.startfile(audio_path)
|
|
191
|
+
elif platform.system() == "Darwin":
|
|
192
|
+
subprocess.run(["open", audio_path])
|
|
193
|
+
else:
|
|
194
|
+
subprocess.run(["xdg-open", audio_path])
|
|
169
195
|
else:
|
|
170
|
-
|
|
196
|
+
# Use internal audio player
|
|
197
|
+
success = self.audio_player.play_audio_file(audio_path)
|
|
198
|
+
if success:
|
|
199
|
+
self._update_audio_button()
|
|
200
|
+
|
|
171
201
|
except Exception as e:
|
|
172
202
|
print(f"Failed to play audio: {e}")
|
|
203
|
+
|
|
204
|
+
def _audio_finished(self):
|
|
205
|
+
"""Called when audio playback finishes"""
|
|
206
|
+
self._update_audio_button()
|
|
207
|
+
|
|
208
|
+
def _update_audio_button(self):
|
|
209
|
+
"""Update the audio button text and style based on playing state"""
|
|
210
|
+
if self.audio_button:
|
|
211
|
+
if self.audio_player.is_playing:
|
|
212
|
+
self.audio_button.config(text="⏹ Stop", bootstyle="outline-warning")
|
|
213
|
+
else:
|
|
214
|
+
self.audio_button.config(text="▶ Play Audio", bootstyle="outline-info")
|
|
215
|
+
|
|
216
|
+
def _cleanup_audio(self):
|
|
217
|
+
"""Clean up audio stream resources"""
|
|
218
|
+
self.audio_player.cleanup()
|
|
219
|
+
|
|
220
|
+
def _cleanup_and_close(self):
|
|
221
|
+
"""Clean up resources and close dialog"""
|
|
222
|
+
self._cleanup_audio()
|
|
223
|
+
self._on_cancel()
|
|
173
224
|
|
|
174
225
|
def _on_voice(self):
|
|
226
|
+
# Clean up audio before closing
|
|
227
|
+
self._cleanup_audio()
|
|
175
228
|
# The screenshot_path is now correctly updated if the user chose a new one
|
|
176
229
|
self.result = (True, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
|
|
177
230
|
self.destroy()
|
|
178
231
|
|
|
179
232
|
def _on_no_voice(self):
|
|
233
|
+
# Clean up audio before closing
|
|
234
|
+
self._cleanup_audio()
|
|
180
235
|
self.result = (False, self.sentence_text.get("1.0", tk.END).strip(), self.translation_text.get("1.0", tk.END).strip(), self.screenshot_path)
|
|
181
236
|
self.destroy()
|
|
182
237
|
|
|
@@ -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
|
-
|
|
2346
|
-
|
|
2347
|
-
self.overlay_monitor = ttk.Combobox(overlay_frame, values=self.monitors
|
|
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.
|
|
2351
|
-
|
|
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
|
{gamesentenceminer-2.18.6 → gamesentenceminer-2.18.7}/GameSentenceMiner/util/configuration.py
RENAMED
|
@@ -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
|
|
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
|