GameSentenceMiner 2.8.6__tar.gz → 2.8.8__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.8.8/GameSentenceMiner/ai/ai_prompting.py +201 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/anki.py +4 -3
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/config_gui.py +42 -12
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/configuration.py +39 -15
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/gametext.py +26 -34
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/gsm.py +58 -42
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/obs.py +47 -24
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/ocr/owocr_area_selector.py +4 -2
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/ocr/owocr_helper.py +32 -3
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/owocr/owocr/config.py +3 -1
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/owocr/owocr/run.py +78 -6
- {gamesentenceminer-2.8.6/GameSentenceMiner/web/static → gamesentenceminer-2.8.8/GameSentenceMiner/web/templates}/utility.html +130 -20
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/texthooking_page.py +172 -15
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner.egg-info/PKG-INFO +2 -1
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner.egg-info/SOURCES.txt +4 -4
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner.egg-info/requires.txt +1 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/PKG-INFO +2 -1
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/pyproject.toml +4 -3
- gamesentenceminer-2.8.6/GameSentenceMiner/ai/gemini.py +0 -143
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/ai/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/communication/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/communication/send.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/communication/websocket.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/downloader/Untitled_json.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/downloader/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/downloader/download_tools.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/downloader/oneocr_dl.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/electron_config.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/ffmpeg.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/model.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/notification.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/ocr/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/owocr/owocr/ocr.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/package.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/text_log.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/util.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/vad/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/vad/silero_trim.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/vad/vosk_helper.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/vad/whisper_helper.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/__init__.py +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/favicon.ico +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/favicon.svg +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/site.webmanifest +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/style.css +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
- {gamesentenceminer-2.8.6/GameSentenceMiner/web/static → gamesentenceminer-2.8.8/GameSentenceMiner/web/templates}/text_replacements.html +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/GameSentenceMiner.egg-info/top_level.txt +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/LICENSE +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/README.md +0 -0
- {gamesentenceminer-2.8.6 → gamesentenceminer-2.8.8}/setup.cfg +0 -0
@@ -0,0 +1,201 @@
|
|
1
|
+
import logging
|
2
|
+
import textwrap
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from enum import Enum
|
6
|
+
from typing import List, Any, Optional
|
7
|
+
|
8
|
+
import google.generativeai as genai
|
9
|
+
from groq import Groq
|
10
|
+
|
11
|
+
from GameSentenceMiner.configuration import get_config, Ai, logger
|
12
|
+
from GameSentenceMiner.text_log import GameLine
|
13
|
+
|
14
|
+
# Suppress debug logs from httpcore
|
15
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
16
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
17
|
+
logging.getLogger("groq._base_client").setLevel(logging.WARNING)
|
18
|
+
|
19
|
+
|
20
|
+
TRANSLATION_PROMPT = textwrap.dedent(f"""Translate the following Japanese dialogue from this game into natural, context-aware English. Focus on preserving the tone, intent, and emotional nuance of the original text, paying close attention to the context provided by surrounding lines. The dialogue may include slang, idioms, implied meanings, or game-specific terminology that should be adapted naturally for English-speaking players. Ensure the translation feels immersive and aligns with the game's narrative style and character voices.
|
21
|
+
Translate only the specified line below, providing a single result. Do not include additional text, explanations, alternatives, or other lines unless explicitly requested. If there are alternatives, choose the best one. Allow expletives if more natural. Allow HTML tags for emphasis, italics, and other formatting as needed. Please also try to preserve existing HTML tags from the specified sentence if appropriate. Answer with nothing but the best translation, no alternatives or explanations.
|
22
|
+
|
23
|
+
Line to Translate:
|
24
|
+
""")
|
25
|
+
|
26
|
+
CONTEXT_PROMPT = textwrap.dedent(f"""Provide a very brief summary of the scene in English based on the provided Japanese dialogue and context. Focus on the characters' actions and the immediate situation being described.
|
27
|
+
|
28
|
+
Current Sentence:
|
29
|
+
""")
|
30
|
+
|
31
|
+
class AIType(Enum):
|
32
|
+
GEMINI = "Gemini"
|
33
|
+
GROQ = "Groq"
|
34
|
+
|
35
|
+
@dataclass
|
36
|
+
class AIConfig:
|
37
|
+
api_key: str
|
38
|
+
model: str
|
39
|
+
api_url: Optional[str]
|
40
|
+
type: 'AIType'
|
41
|
+
|
42
|
+
@dataclass
|
43
|
+
class GeminiAIConfig(AIConfig):
|
44
|
+
def __init__(self, api_key: str, model: str = "gemini-2.0-flash"):
|
45
|
+
super().__init__(api_key=api_key, model=model, api_url=None, type=AIType.GEMINI)
|
46
|
+
|
47
|
+
@dataclass
|
48
|
+
class GroqAiConfig(AIConfig):
|
49
|
+
def __init__(self, api_key: str, model: str = "meta-llama/llama-4-scout-17b-16e-instruct"):
|
50
|
+
super().__init__(api_key=api_key, model=model, api_url=None, type=AIType.GROQ)
|
51
|
+
|
52
|
+
|
53
|
+
class AIManager(ABC):
|
54
|
+
def __init__(self, ai_config: AIConfig, logger: Optional[logging.Logger] = None):
|
55
|
+
self.ai_config = ai_config
|
56
|
+
self.logger = logger
|
57
|
+
|
58
|
+
@abstractmethod
|
59
|
+
def process(self, lines: List[GameLine], sentence: str, current_line_index: int, game_title: str = "") -> str:
|
60
|
+
pass
|
61
|
+
|
62
|
+
@abstractmethod
|
63
|
+
def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
|
64
|
+
start_index = max(0, current_line.index - 10)
|
65
|
+
end_index = min(len(lines), current_line.index + 11)
|
66
|
+
|
67
|
+
context_lines_text = []
|
68
|
+
for i in range(start_index, end_index):
|
69
|
+
if i < len(lines):
|
70
|
+
context_lines_text.append(lines[i].text)
|
71
|
+
|
72
|
+
dialogue_context = "\n".join(context_lines_text)
|
73
|
+
|
74
|
+
if get_config().ai.use_canned_translation_prompt:
|
75
|
+
prompt_to_use = TRANSLATION_PROMPT
|
76
|
+
elif get_config().ai.use_canned_context_prompt:
|
77
|
+
prompt_to_use = CONTEXT_PROMPT
|
78
|
+
else:
|
79
|
+
prompt_to_use = getattr(self.ai_config, 'custom_prompt', "")
|
80
|
+
|
81
|
+
full_prompt = textwrap.dedent(f"""
|
82
|
+
Dialogue Context:
|
83
|
+
|
84
|
+
{dialogue_context}
|
85
|
+
|
86
|
+
I am playing the game {game_title}. With that, and the above dialogue context in mind, answer the following prompt.
|
87
|
+
|
88
|
+
{prompt_to_use}
|
89
|
+
|
90
|
+
{sentence}
|
91
|
+
""")
|
92
|
+
return full_prompt
|
93
|
+
|
94
|
+
|
95
|
+
class GeminiAI(AIManager):
|
96
|
+
def __init__(self, model, api_key, logger: Optional[logging.Logger] = None):
|
97
|
+
super().__init__(GeminiAIConfig(model=model, api_key=api_key), logger)
|
98
|
+
try:
|
99
|
+
genai.configure(api_key=self.ai_config.api_key)
|
100
|
+
model_name = self.ai_config.model
|
101
|
+
self.model = genai.GenerativeModel(model_name)
|
102
|
+
self.logger.info(f"GeminiAIManager initialized with model: {model_name}")
|
103
|
+
except Exception as e:
|
104
|
+
self.logger.error(f"Failed to initialize Gemini API: {e}")
|
105
|
+
self.model = None
|
106
|
+
|
107
|
+
def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
|
108
|
+
prompt = super()._build_prompt(lines, sentence, current_line, game_title)
|
109
|
+
return prompt
|
110
|
+
|
111
|
+
def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "") -> str:
|
112
|
+
if self.model is None:
|
113
|
+
return "Processing failed: AI model not initialized."
|
114
|
+
|
115
|
+
if not lines or not current_line:
|
116
|
+
self.logger.warning(f"Invalid input for process: lines={len(lines)}, current_line={current_line.index}")
|
117
|
+
return "Invalid input."
|
118
|
+
|
119
|
+
try:
|
120
|
+
prompt = self._build_prompt(lines, sentence, current_line, game_title)
|
121
|
+
self.logger.debug(f"Generated prompt:\n{prompt}")
|
122
|
+
response = self.model.generate_content(prompt)
|
123
|
+
result = response.text.strip()
|
124
|
+
self.logger.debug(f"Received response:\n{result}")
|
125
|
+
return result
|
126
|
+
except Exception as e:
|
127
|
+
self.logger.error(f"Gemini processing failed: {e}")
|
128
|
+
return f"Processing failed: {e}"
|
129
|
+
|
130
|
+
class GroqAI(AIManager):
|
131
|
+
def __init__(self, model, api_key, logger: Optional[logging.Logger] = None):
|
132
|
+
super().__init__(GroqAiConfig(model=model, api_key=api_key), logger)
|
133
|
+
self.api_key = self.ai_config.api_key
|
134
|
+
self.model_name = self.ai_config.model
|
135
|
+
try:
|
136
|
+
self.client = Groq(api_key=self.api_key)
|
137
|
+
self.logger.info(f"GroqAIManager initialized with model: {self.model_name}")
|
138
|
+
except Exception as e:
|
139
|
+
self.logger.error(f"Failed to initialize Groq client: {e}")
|
140
|
+
self.client = None
|
141
|
+
|
142
|
+
def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
|
143
|
+
prompt = super()._build_prompt(lines, sentence, current_line, game_title)
|
144
|
+
return prompt
|
145
|
+
|
146
|
+
def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "") -> str:
|
147
|
+
if self.client is None:
|
148
|
+
return "Processing failed: Groq client not initialized."
|
149
|
+
|
150
|
+
if not lines or not current_line:
|
151
|
+
self.logger.warning(f"Invalid input for process: lines={len(lines)}, current_line={current_line.index}")
|
152
|
+
return "Invalid input."
|
153
|
+
|
154
|
+
try:
|
155
|
+
prompt = self._build_prompt(lines, sentence, current_line, game_title)
|
156
|
+
self.logger.debug(f"Generated prompt:\n{prompt}")
|
157
|
+
completion = self.client.chat.completions.create(
|
158
|
+
model=self.model_name,
|
159
|
+
messages=[{"role": "user", "content": prompt}],
|
160
|
+
temperature=.5,
|
161
|
+
max_completion_tokens=1024,
|
162
|
+
top_p=1,
|
163
|
+
stream=False,
|
164
|
+
stop=None,
|
165
|
+
)
|
166
|
+
result = completion.choices[0].message.content.strip()
|
167
|
+
self.logger.debug(f"Received response:\n{result}")
|
168
|
+
return result
|
169
|
+
except Exception as e:
|
170
|
+
self.logger.error(f"Groq processing failed: {e}")
|
171
|
+
return f"Processing failed: {e}"
|
172
|
+
|
173
|
+
ai_manager: AIManager | None = None
|
174
|
+
current_ai_config: Ai | None = None
|
175
|
+
|
176
|
+
def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = ""):
|
177
|
+
global ai_manager, current_ai_config
|
178
|
+
if not ai_manager or get_config().ai != current_ai_config:
|
179
|
+
if get_config().ai.provider == AIType.GEMINI.value:
|
180
|
+
ai_manager = GeminiAI(model=get_config().ai.gemini_model, api_key=get_config().ai.gemini_api_key, logger=logger)
|
181
|
+
elif get_config().ai.provider == AIType.GROQ.value:
|
182
|
+
ai_manager = GroqAI(model=get_config().ai.groq_model, api_key=get_config().ai.groq_api_key, logger=logger)
|
183
|
+
current_ai_config = get_config().ai
|
184
|
+
return ai_manager.process(lines, sentence, current_line, game_title)
|
185
|
+
|
186
|
+
if __name__ == '__main__':
|
187
|
+
lines = [
|
188
|
+
GameLine(index=0, text="こんにちは、元気ですか?", id=None, time=None, prev=None, next=None),
|
189
|
+
GameLine(index=1, text="今日はいい天気ですね。",id=None, time=None, prev=None, next=None),
|
190
|
+
GameLine(index=2, text="ゲームを始めましょう!",id=None, time=None, prev=None, next=None),
|
191
|
+
]
|
192
|
+
sentence = "ゲームを始めましょう!"
|
193
|
+
current_line = lines[2]
|
194
|
+
game_title = "Test Game"
|
195
|
+
|
196
|
+
# Set up logging
|
197
|
+
logging.basicConfig(level=logging.DEBUG)
|
198
|
+
|
199
|
+
# Test the function
|
200
|
+
result = get_ai_prompt_result(lines, sentence, current_line, game_title)
|
201
|
+
print("AI Prompt Result:", result)
|
@@ -8,7 +8,7 @@ from datetime import datetime, timedelta
|
|
8
8
|
from requests import post
|
9
9
|
|
10
10
|
from GameSentenceMiner import obs, util, notification, ffmpeg
|
11
|
-
from GameSentenceMiner.ai.
|
11
|
+
from GameSentenceMiner.ai.ai_prompting import GeminiAI, get_ai_prompt_result
|
12
12
|
from GameSentenceMiner.configuration import *
|
13
13
|
from GameSentenceMiner.configuration import get_config
|
14
14
|
from GameSentenceMiner.model import AnkiCard
|
@@ -67,8 +67,8 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
|
|
67
67
|
sentence_field = note['fields'].get(get_config().anki.sentence_field, {})
|
68
68
|
sentence_to_translate = sentence_field if sentence_field else last_note.get_field(
|
69
69
|
get_config().anki.sentence_field)
|
70
|
-
translation =
|
71
|
-
|
70
|
+
translation = get_ai_prompt_result(get_all_lines(), sentence_to_translate,
|
71
|
+
game_line, get_current_game())
|
72
72
|
logger.info(translation)
|
73
73
|
note['fields'][get_config().ai.anki_field] = translation
|
74
74
|
|
@@ -286,6 +286,7 @@ def update_new_card():
|
|
286
286
|
try:
|
287
287
|
obs.save_replay_buffer()
|
288
288
|
except Exception as e:
|
289
|
+
card_queue.pop(0)
|
289
290
|
logger.error(f"Error saving replay buffer: {e}")
|
290
291
|
return
|
291
292
|
|
@@ -206,9 +206,12 @@ class ConfigApp:
|
|
206
206
|
),
|
207
207
|
ai=Ai(
|
208
208
|
enabled=self.ai_enabled.get(),
|
209
|
-
|
209
|
+
provider=self.ai_provider.get(),
|
210
|
+
gemini_model=self.gemini_model.get(),
|
211
|
+
groq_model=self.groq_model.get(),
|
212
|
+
gemini_api_key=self.gemini_api_key.get(),
|
213
|
+
groq_api_key=self.groq_api_key.get(),
|
210
214
|
anki_field=self.ai_anki_field.get(),
|
211
|
-
api_key=self.ai_api_key.get(),
|
212
215
|
use_canned_translation_prompt=self.use_canned_translation_prompt.get(),
|
213
216
|
use_canned_context_prompt=self.use_canned_context_prompt.get(),
|
214
217
|
custom_prompt=self.custom_prompt.get("1.0", tk.END)
|
@@ -1004,12 +1007,18 @@ class ConfigApp:
|
|
1004
1007
|
ttk.Checkbutton(ai_frame, variable=self.ai_enabled).grid(row=self.current_row, column=1, sticky='W')
|
1005
1008
|
self.add_label_and_increment_row(ai_frame, "Enable or disable AI integration.", row=self.current_row, column=2)
|
1006
1009
|
|
1007
|
-
ttk.Label(ai_frame, text="
|
1008
|
-
self.
|
1009
|
-
self.
|
1010
|
-
self.
|
1011
|
-
self.add_label_and_increment_row(ai_frame, "
|
1012
|
-
|
1010
|
+
ttk.Label(ai_frame, text="Provider:").grid(row=self.current_row, column=0, sticky='W')
|
1011
|
+
self.ai_provider = ttk.Combobox(ai_frame, values=['Gemini', 'Groq'])
|
1012
|
+
self.ai_provider.set(self.settings.ai.provider)
|
1013
|
+
self.ai_provider.grid(row=self.current_row, column=1)
|
1014
|
+
self.add_label_and_increment_row(ai_frame, "Select the AI provider.", row=self.current_row, column=2)
|
1015
|
+
|
1016
|
+
ttk.Label(ai_frame, text="Gemini AI Model:").grid(row=self.current_row, column=0, sticky='W')
|
1017
|
+
self.gemini_model = ttk.Combobox(ai_frame, values=['gemini-2.0-flash', 'gemini-2.0-flash-lite', 'gemini-2.5-pro-preview-03-25', 'gemini-2.5-flash-preview-04-17'])
|
1018
|
+
self.gemini_model.set(self.settings.ai.gemini_model)
|
1019
|
+
self.gemini_model.grid(row=self.current_row, column=1)
|
1020
|
+
self.add_label_and_increment_row(ai_frame, "Select the AI model to use.", row=self.current_row, column=2)
|
1021
|
+
|
1013
1022
|
|
1014
1023
|
# ttk.Label(ai_frame, text="Provider:").grid(row=self.current_row, column=0, sticky='W')
|
1015
1024
|
# self.provider = ttk.Combobox(ai_frame,
|
@@ -1018,13 +1027,34 @@ class ConfigApp:
|
|
1018
1027
|
# self.provider.grid(row=self.current_row, column=1)
|
1019
1028
|
# self.add_label_and_increment_row(ai_frame, "Select the AI provider. Currently only Gemini is supported.", row=self.current_row, column=2)
|
1020
1029
|
|
1021
|
-
ttk.Label(ai_frame, text="API Key:").grid(row=self.current_row, column=0, sticky='W')
|
1022
|
-
self.
|
1023
|
-
self.
|
1024
|
-
self.
|
1030
|
+
ttk.Label(ai_frame, text="Gemini API Key:").grid(row=self.current_row, column=0, sticky='W')
|
1031
|
+
self.gemini_api_key = ttk.Entry(ai_frame, show="*") # Mask the API key for security
|
1032
|
+
self.gemini_api_key.insert(0, self.settings.ai.gemini_api_key)
|
1033
|
+
self.gemini_api_key.grid(row=self.current_row, column=1)
|
1025
1034
|
self.add_label_and_increment_row(ai_frame, "API key for the selected AI provider (Gemini only currently).", row=self.current_row,
|
1026
1035
|
column=2)
|
1027
1036
|
|
1037
|
+
ttk.Label(ai_frame, text="Groq AI Model:").grid(row=self.current_row, column=0, sticky='W')
|
1038
|
+
self.groq_model = ttk.Combobox(ai_frame, values=['meta-llama/llama-4-maverick-17b-128e-instruct', 'meta-llama/llama-4-scout-17b-16e-instruct', 'llama-3.1-8b-instant'])
|
1039
|
+
self.groq_model.set(self.settings.ai.groq_model)
|
1040
|
+
self.groq_model.grid(row=self.current_row, column=1)
|
1041
|
+
self.add_label_and_increment_row(ai_frame, "Select the Groq AI model to use.", row=self.current_row, column=2)
|
1042
|
+
|
1043
|
+
|
1044
|
+
ttk.Label(ai_frame, text="Groq API Key:").grid(row=self.current_row, column=0, sticky='W')
|
1045
|
+
self.groq_api_key = ttk.Entry(ai_frame, show="*") # Mask the API key for security
|
1046
|
+
self.groq_api_key.insert(0, self.settings.ai.groq_api_key)
|
1047
|
+
self.groq_api_key.grid(row=self.current_row, column=1)
|
1048
|
+
self.add_label_and_increment_row(ai_frame, "API key for Groq AI provider.", row=self.current_row,
|
1049
|
+
column=2)
|
1050
|
+
|
1051
|
+
ttk.Label(ai_frame, text="Anki Field:").grid(row=self.current_row, column=0, sticky='W')
|
1052
|
+
self.ai_anki_field = ttk.Entry(ai_frame)
|
1053
|
+
self.ai_anki_field.insert(0, self.settings.ai.anki_field)
|
1054
|
+
self.ai_anki_field.grid(row=self.current_row, column=1)
|
1055
|
+
self.add_label_and_increment_row(ai_frame, "Field in Anki for AI-generated content.", row=self.current_row,
|
1056
|
+
column=2)
|
1057
|
+
|
1028
1058
|
ttk.Label(ai_frame, text="Use Canned Translation Prompt:").grid(row=self.current_row, column=0, sticky='W')
|
1029
1059
|
self.use_canned_translation_prompt = tk.BooleanVar(value=self.settings.ai.use_canned_translation_prompt)
|
1030
1060
|
ttk.Checkbutton(ai_frame, variable=self.use_canned_translation_prompt).grid(row=self.current_row, column=1,
|
@@ -28,7 +28,8 @@ WHISPER_SMALL = 'small'
|
|
28
28
|
WHISPER_MEDIUM = 'medium'
|
29
29
|
WHSIPER_LARGE = 'large'
|
30
30
|
|
31
|
-
AI_GEMINI = '
|
31
|
+
AI_GEMINI = 'Gemini'
|
32
|
+
AI_GROQ = 'Groq'
|
32
33
|
|
33
34
|
INFO = 'INFO'
|
34
35
|
DEBUG = 'DEBUG'
|
@@ -134,7 +135,7 @@ class OBS:
|
|
134
135
|
open_obs: bool = True
|
135
136
|
close_obs: bool = True
|
136
137
|
host: str = "127.0.0.1"
|
137
|
-
port: int =
|
138
|
+
port: int = 7274
|
138
139
|
password: str = "your_password"
|
139
140
|
get_game_from_scene: bool = True
|
140
141
|
minimum_replay_size: int = 0
|
@@ -187,11 +188,18 @@ class Ai:
|
|
187
188
|
enabled: bool = False
|
188
189
|
anki_field: str = ''
|
189
190
|
provider: str = AI_GEMINI
|
190
|
-
|
191
|
+
gemini_model: str = 'gemini-2.0-flash'
|
192
|
+
groq_model: str = 'meta-llama/llama-4-scout-17b-16e-instruct'
|
193
|
+
api_key: str = '' # Deprecated
|
194
|
+
gemini_api_key: str = ''
|
195
|
+
groq_api_key: str = ''
|
191
196
|
use_canned_translation_prompt: bool = True
|
192
197
|
use_canned_context_prompt: bool = False
|
193
198
|
custom_prompt: str = ''
|
194
199
|
|
200
|
+
def __post_init__(self):
|
201
|
+
self.gemini_api_key = self.api_key
|
202
|
+
|
195
203
|
|
196
204
|
@dataclass_json
|
197
205
|
@dataclass
|
@@ -306,6 +314,21 @@ class Config:
|
|
306
314
|
instance = cls(configs={DEFAULT_CONFIG: ProfileConfig()}, current_profile=DEFAULT_CONFIG)
|
307
315
|
return instance
|
308
316
|
|
317
|
+
@classmethod
|
318
|
+
def load(cls):
|
319
|
+
config_path = get_config_path()
|
320
|
+
if os.path.exists(config_path):
|
321
|
+
with open(config_path, 'r') as file:
|
322
|
+
data = json.load(file)
|
323
|
+
return cls.from_dict(data)
|
324
|
+
else:
|
325
|
+
return cls.new()
|
326
|
+
|
327
|
+
def save(self):
|
328
|
+
with open(get_config_path(), 'w') as file:
|
329
|
+
json.dump(self.to_dict(), file, indent=4)
|
330
|
+
return self
|
331
|
+
|
309
332
|
def get_config(self) -> ProfileConfig:
|
310
333
|
return self.configs[self.current_profile]
|
311
334
|
|
@@ -355,7 +378,8 @@ class Config:
|
|
355
378
|
self.sync_shared_field(config.ai, profile.ai, "anki_field")
|
356
379
|
self.sync_shared_field(config.ai, profile.ai, "provider")
|
357
380
|
self.sync_shared_field(config.ai, profile.ai, "api_key")
|
358
|
-
|
381
|
+
self.sync_shared_field(config.ai, profile.ai, "gemini_api_key")
|
382
|
+
self.sync_shared_field(config.ai, profile.ai, "groq_api_key")
|
359
383
|
|
360
384
|
return self
|
361
385
|
|
@@ -513,22 +537,22 @@ sys.stderr.reconfigure(encoding='utf-8')
|
|
513
537
|
|
514
538
|
logger = logging.getLogger("GameSentenceMiner")
|
515
539
|
logger.setLevel(logging.DEBUG) # Set the base level to DEBUG so that all messages are captured
|
540
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
516
541
|
|
517
542
|
# Create console handler with level INFO
|
518
543
|
console_handler = logging.StreamHandler(sys.stdout)
|
519
544
|
console_handler.setLevel(logging.INFO)
|
520
545
|
|
521
|
-
# Create rotating file handler with level DEBUG
|
522
|
-
file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
|
523
|
-
file_handler.setLevel(logging.DEBUG)
|
524
|
-
|
525
|
-
# Create a formatter
|
526
|
-
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
527
|
-
|
528
|
-
# Add formatter to handlers
|
529
546
|
console_handler.setFormatter(formatter)
|
530
|
-
file_handler.setFormatter(formatter)
|
531
547
|
|
532
|
-
# Add handlers to the logger
|
533
548
|
logger.addHandler(console_handler)
|
534
|
-
|
549
|
+
|
550
|
+
# Create rotating file handler with level DEBUG
|
551
|
+
if 'gsm' in sys.argv[0]:
|
552
|
+
file_handler = RotatingFileHandler(get_log_path(), maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
|
553
|
+
file_handler.setLevel(logging.DEBUG)
|
554
|
+
file_handler.setFormatter(formatter)
|
555
|
+
logger.addHandler(file_handler)
|
556
|
+
|
557
|
+
DB_PATH = os.path.join(get_app_directory(), 'gsm.db')
|
558
|
+
|
@@ -1,7 +1,5 @@
|
|
1
1
|
import asyncio
|
2
2
|
import re
|
3
|
-
import threading
|
4
|
-
import time
|
5
3
|
|
6
4
|
import pyperclip
|
7
5
|
import websockets
|
@@ -24,31 +22,23 @@ websocket_connected = False
|
|
24
22
|
# def remove_old_events(self, cutoff_time: datetime):
|
25
23
|
# self.values = [line for line in self.values if line.time >= cutoff_time]
|
26
24
|
|
27
|
-
|
25
|
+
async def monitor_clipboard():
|
26
|
+
# Initial clipboard content
|
27
|
+
current_clipboard = pyperclip.paste()
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
# Initial clipboard content
|
37
|
-
current_line = pyperclip.paste()
|
38
|
-
|
39
|
-
skip_next_clipboard = False
|
40
|
-
while True:
|
41
|
-
if not get_config().general.use_both_clipboard_and_websocket and websocket_connected:
|
42
|
-
time.sleep(1)
|
43
|
-
skip_next_clipboard = True
|
44
|
-
continue
|
45
|
-
current_clipboard = pyperclip.paste()
|
29
|
+
skip_next_clipboard = False
|
30
|
+
while True:
|
31
|
+
if not get_config().general.use_both_clipboard_and_websocket and websocket_connected:
|
32
|
+
await asyncio.sleep(1)
|
33
|
+
skip_next_clipboard = True
|
34
|
+
continue
|
35
|
+
current_clipboard = pyperclip.paste()
|
46
36
|
|
47
|
-
|
48
|
-
|
49
|
-
|
37
|
+
if current_clipboard and current_clipboard != current_line and not skip_next_clipboard:
|
38
|
+
await handle_new_text_event(current_clipboard)
|
39
|
+
skip_next_clipboard = False
|
50
40
|
|
51
|
-
|
41
|
+
await asyncio.sleep(0.05)
|
52
42
|
|
53
43
|
|
54
44
|
async def listen_websocket():
|
@@ -69,6 +59,8 @@ async def listen_websocket():
|
|
69
59
|
line_time = None
|
70
60
|
while True:
|
71
61
|
message = await websocket.recv()
|
62
|
+
if not message:
|
63
|
+
continue
|
72
64
|
logger.debug(message)
|
73
65
|
try:
|
74
66
|
data = json.loads(message)
|
@@ -76,26 +68,25 @@ async def listen_websocket():
|
|
76
68
|
current_clipboard = data["sentence"]
|
77
69
|
if "time" in data:
|
78
70
|
line_time = datetime.fromisoformat(data["time"])
|
79
|
-
print(line_time)
|
80
71
|
except json.JSONDecodeError or TypeError:
|
81
72
|
current_clipboard = message
|
82
73
|
if current_clipboard != current_line:
|
83
|
-
handle_new_text_event(current_clipboard, line_time if line_time else None)
|
84
|
-
except (websockets.ConnectionClosed, ConnectionError, InvalidStatus) as e:
|
74
|
+
await handle_new_text_event(current_clipboard, line_time if line_time else None)
|
75
|
+
except (websockets.ConnectionClosed, ConnectionError, InvalidStatus, ConnectionResetError, Exception) as e:
|
85
76
|
if isinstance(e, InvalidStatus):
|
86
77
|
e: InvalidStatus
|
87
78
|
if e.response.status_code == 404:
|
88
79
|
logger.info("Texthooker WebSocket connection failed. Attempting some fixes...")
|
89
80
|
try_other = True
|
90
|
-
|
91
|
-
logger.error(f"
|
81
|
+
if not (isinstance(e, ConnectionResetError) or isinstance(e, ConnectionError) or isinstance(e, InvalidStatus) or isinstance(e, websockets.ConnectionClosed)):
|
82
|
+
logger.error(f"Unexpected error in Texthooker WebSocket connection: {e}")
|
92
83
|
websocket_connected = False
|
93
84
|
if not reconnecting:
|
94
85
|
logger.warning(f"Texthooker WebSocket connection lost, Defaulting to clipboard if enabled. Attempting to Reconnect...")
|
95
86
|
reconnecting = True
|
96
87
|
await asyncio.sleep(5)
|
97
88
|
|
98
|
-
def handle_new_text_event(current_clipboard, line_time=None):
|
89
|
+
async def handle_new_text_event(current_clipboard, line_time=None):
|
99
90
|
global current_line, current_line_time, current_line_after_regex
|
100
91
|
current_line = current_clipboard
|
101
92
|
if get_config().general.texthook_replacement_regex:
|
@@ -106,7 +97,7 @@ def handle_new_text_event(current_clipboard, line_time=None):
|
|
106
97
|
logger.info(f"Line Received: {current_line_after_regex}")
|
107
98
|
current_line_time = line_time if line_time else datetime.now()
|
108
99
|
add_line(current_line_after_regex, line_time)
|
109
|
-
add_event_to_texthooker(get_text_log()[-1])
|
100
|
+
await add_event_to_texthooker(get_text_log()[-1])
|
110
101
|
|
111
102
|
def reset_line_hotkey_pressed():
|
112
103
|
global current_line_time
|
@@ -119,13 +110,14 @@ def run_websocket_listener():
|
|
119
110
|
asyncio.run(listen_websocket())
|
120
111
|
|
121
112
|
|
122
|
-
def start_text_monitor():
|
113
|
+
async def start_text_monitor():
|
123
114
|
if get_config().general.use_websocket:
|
124
|
-
|
115
|
+
util.run_new_thread(run_websocket_listener)
|
125
116
|
if get_config().general.use_clipboard:
|
126
117
|
if get_config().general.use_websocket:
|
127
118
|
if get_config().general.use_both_clipboard_and_websocket:
|
128
119
|
logger.info("Listening for Text on both WebSocket and Clipboard.")
|
129
120
|
else:
|
130
121
|
logger.info("Both WebSocket and Clipboard monitoring are enabled. WebSocket will take precedence if connected.")
|
131
|
-
|
122
|
+
await monitor_clipboard()
|
123
|
+
await asyncio.sleep(1)
|