GameSentenceMiner 2.13.15__tar.gz → 2.14.0rc1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/ai/ai_prompting.py +77 -132
  2. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/anki.py +48 -6
  3. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/config_gui.py +196 -30
  4. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/gametext.py +8 -19
  5. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/gsm.py +5 -4
  6. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/locales/en_us.json +21 -11
  7. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/locales/ja_jp.json +21 -11
  8. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/locales/zh_cn.json +9 -11
  9. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/owocr/owocr/ocr.py +20 -23
  10. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/configuration.py +241 -105
  11. gamesentenceminer-2.14.0rc1/GameSentenceMiner/util/db.py +408 -0
  12. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/ffmpeg.py +2 -10
  13. gamesentenceminer-2.14.0rc1/GameSentenceMiner/util/get_overlay_coords.py +324 -0
  14. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/model.py +8 -2
  15. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/text_log.py +1 -1
  16. gamesentenceminer-2.14.0rc1/GameSentenceMiner/web/templates/__init__.py +0 -0
  17. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/texthooking_page.py +1 -1
  18. gamesentenceminer-2.14.0rc1/GameSentenceMiner/wip/__init___.py +0 -0
  19. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner.egg-info/PKG-INFO +5 -1
  20. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner.egg-info/SOURCES.txt +7 -5
  21. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner.egg-info/requires.txt +4 -0
  22. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/PKG-INFO +5 -1
  23. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/pyproject.toml +7 -3
  24. gamesentenceminer-2.13.15/GameSentenceMiner/util/package.py +0 -37
  25. gamesentenceminer-2.13.15/GameSentenceMiner/wip/get_overlay_coords.py +0 -535
  26. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/__init__.py +0 -0
  27. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/ai/__init__.py +0 -0
  28. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/assets/__init__.py +0 -0
  29. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/assets/icon.png +0 -0
  30. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/assets/icon128.png +0 -0
  31. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/assets/icon256.png +0 -0
  32. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/assets/icon32.png +0 -0
  33. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/assets/icon512.png +0 -0
  34. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/assets/icon64.png +0 -0
  35. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/assets/pickaxe.png +0 -0
  36. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/obs.py +0 -0
  37. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/ocr/__init__.py +0 -0
  38. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/ocr/gsm_ocr_config.py +0 -0
  39. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/ocr/ocrconfig.py +0 -0
  40. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/ocr/owocr_area_selector.py +0 -0
  41. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/ocr/owocr_helper.py +0 -0
  42. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/ocr/ss_picker.py +0 -0
  43. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/owocr/owocr/__init__.py +0 -0
  44. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/owocr/owocr/__main__.py +0 -0
  45. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/owocr/owocr/config.py +0 -0
  46. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/owocr/owocr/lens_betterproto.py +0 -0
  47. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/owocr/owocr/run.py +0 -0
  48. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/owocr/owocr/screen_coordinate_picker.py +0 -0
  49. {gamesentenceminer-2.13.15/GameSentenceMiner/util → gamesentenceminer-2.14.0rc1/GameSentenceMiner/tools}/__init__.py +0 -0
  50. {gamesentenceminer-2.13.15/GameSentenceMiner/util → gamesentenceminer-2.14.0rc1/GameSentenceMiner/tools}/audio_offset_selector.py +0 -0
  51. {gamesentenceminer-2.13.15/GameSentenceMiner/util → gamesentenceminer-2.14.0rc1/GameSentenceMiner/tools}/ss_selector.py +0 -0
  52. {gamesentenceminer-2.13.15/GameSentenceMiner/util → gamesentenceminer-2.14.0rc1/GameSentenceMiner/tools}/window_transparency.py +0 -0
  53. {gamesentenceminer-2.13.15/GameSentenceMiner/util/downloader → gamesentenceminer-2.14.0rc1/GameSentenceMiner/util}/__init__.py +0 -0
  54. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/communication/__init__.py +0 -0
  55. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/communication/send.py +0 -0
  56. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/communication/websocket.py +0 -0
  57. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/downloader/Untitled_json.py +0 -0
  58. {gamesentenceminer-2.13.15/GameSentenceMiner/web → gamesentenceminer-2.14.0rc1/GameSentenceMiner/util/downloader}/__init__.py +0 -0
  59. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/downloader/download_tools.py +0 -0
  60. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/downloader/oneocr_dl.py +0 -0
  61. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/electron_config.py +0 -0
  62. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/gsm_utils.py +0 -0
  63. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/util/notification.py +0 -0
  64. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/vad.py +0 -0
  65. {gamesentenceminer-2.13.15/GameSentenceMiner/web/static → gamesentenceminer-2.14.0rc1/GameSentenceMiner/web}/__init__.py +0 -0
  66. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/service.py +0 -0
  67. {gamesentenceminer-2.13.15/GameSentenceMiner/web/templates → gamesentenceminer-2.14.0rc1/GameSentenceMiner/web/static}/__init__.py +0 -0
  68. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/static/apple-touch-icon.png +0 -0
  69. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/static/favicon-96x96.png +0 -0
  70. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/static/favicon.ico +0 -0
  71. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/static/favicon.svg +0 -0
  72. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/static/site.webmanifest +0 -0
  73. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/static/style.css +0 -0
  74. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/static/web-app-manifest-192x192.png +0 -0
  75. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/static/web-app-manifest-512x512.png +0 -0
  76. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/templates/index.html +0 -0
  77. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/templates/text_replacements.html +0 -0
  78. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner/web/templates/utility.html +0 -0
  79. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner.egg-info/dependency_links.txt +0 -0
  80. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner.egg-info/entry_points.txt +0 -0
  81. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/GameSentenceMiner.egg-info/top_level.txt +0 -0
  82. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/LICENSE +0 -0
  83. {gamesentenceminer-2.13.15 → gamesentenceminer-2.14.0rc1}/setup.cfg +0 -0
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import textwrap
3
3
  import time
4
+ import json
4
5
  from abc import ABC, abstractmethod
5
6
  from dataclasses import dataclass
6
7
  from enum import Enum
@@ -28,7 +29,7 @@ Translate ONLY the single line of game dialogue specified below into natural-sou
28
29
  **Output Requirements:**
29
30
  - Provide only the single, best {get_config().general.get_native_language_name()} translation.
30
31
  - Use expletives if they are natural for the context and enhance the translation's impact, but do not over-exaggerate.
31
- - Preserve or add HTML tags (e.g., `<i>`, `<b>`) if appropriate for emphasis.
32
+ - Preserve all HTML tags present in the original text on their corresponding words in the translation.
32
33
  - Do not include notes, alternatives, explanations, or any other surrounding text. Absolutely nothing but the translated line.
33
34
 
34
35
  **Line to Translate:**
@@ -45,7 +46,7 @@ Current Sentence:
45
46
  class AIType(Enum):
46
47
  GEMINI = "Gemini"
47
48
  GROQ = "Groq"
48
- LOCAL = "Local"
49
+ OPENAI = "OpenAI"
49
50
 
50
51
  @dataclass
51
52
  class AIConfig:
@@ -65,9 +66,9 @@ class GroqAiConfig(AIConfig):
65
66
  super().__init__(api_key=api_key, model=model, api_url=None, type=AIType.GROQ)
66
67
 
67
68
  @dataclass
68
- class LocalAIConfig(AIConfig):
69
- def __init__(self, model: str = "facebook/nllb-200-distilled-600M"):
70
- super().__init__(api_key="", model=model, api_url=None, type=AIType.LOCAL)
69
+ class OpenAIAIConfig(AIConfig):
70
+ def __init__(self, api_key: str, model: str = "openai/gpt-oss-20b", api_url: Optional[str] = None):
71
+ super().__init__(api_key=api_key, model=model, api_url=api_url, type=AIType.OPENAI)
71
72
 
72
73
 
73
74
  class AIManager(ABC):
@@ -123,124 +124,62 @@ class AIManager(ABC):
123
124
  return full_prompt
124
125
 
125
126
 
126
- class LocalAIManager(AIManager):
127
- def __init__(self, model, logger: Optional[logging.Logger] = None):
128
- super().__init__(LocalAIConfig(model=model), logger)
127
+ class OpenAIManager(AIManager):
128
+ def __init__(self, model, api_url, api_key, logger: Optional[logging.Logger] = None):
129
+ super().__init__(OpenAIAIConfig(api_key=api_key, model=model, api_url=api_url), logger)
129
130
  try:
130
- import torch
131
- from transformers import AutoTokenizer, AutoModelForCausalLM, AutoModelForSeq2SeqLM, pipeline
132
-
133
- self.transformers_available = True
134
- except (ImportError, OSError):
135
- self.transformers_available = False
136
- self.model_name = self.ai_config.model
137
- if MANUAL_MODEL_OVERRIDE:
138
- self.model_name = MANUAL_MODEL_OVERRIDE
139
- self.logger.warning(f"MANUAL MODEL OVERRIDE ENABLED! Using model: {self.model_name}")
140
- self.model = None
141
- self.pipe = None
142
- self.tokenizer = None
143
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
144
- self.is_encoder_decoder = False
145
- self.is_nllb = "nllb" in self.model_name.lower()
146
-
147
- if not self.transformers_available:
148
- self.logger.error("Local AI dependencies not found. Please run: pip install torch transformers sentencepiece")
149
- return
150
-
151
- if not self.model_name:
152
- self.logger.error("No local model name provided in configuration.")
153
- return
154
-
155
- try:
156
- self.logger.info(f"Loading local model: {self.model_name}")
157
- self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
158
-
159
- # Try to load as a Causal LM first. If it fails, assume it's a Seq2Seq model.
160
- # This is a heuristic to fix the original code's bug of using Seq2Seq for all models.
161
- try:
162
- self.model = AutoModelForCausalLM.from_pretrained(
163
- self.model_name,
164
- torch_dtype=torch.bfloat16,
165
- )
166
- # self.pipe = pipeline(
167
- # "text-generation",
168
- # model=self.model_name,
169
- # torch_dtype=torch.bfloat16,
170
- # device=self.device
171
- # )
172
- # print(self.pipe("Translate this sentence to English: お前は何をしている!?", return_full_text=False))
173
- self.is_encoder_decoder = False
174
- self.logger.info(f"Loaded {self.model_name} as a CausalLM.")
175
- except (ValueError, TypeError, OSError, KeyError) as e:
176
- print(e)
177
- self.model = AutoModelForSeq2SeqLM.from_pretrained(
178
- self.model_name,
179
- torch_dtype=torch.bfloat16,
131
+ import openai
132
+ self.client = openai.OpenAI(
133
+ base_url=api_url,
134
+ api_key=api_key
180
135
  )
181
- self.is_encoder_decoder = True
182
- self.logger.info(f"Loaded {self.model_name} as a Seq2SeqLM.")
183
- if self.device == "cuda":
184
- self.model.to(self.device)
185
-
186
-
187
- self.logger.info(f"Local model '{self.model_name}' loaded on {self.device}.")
136
+ self.model_name = model
137
+ if MANUAL_MODEL_OVERRIDE:
138
+ self.model_name = MANUAL_MODEL_OVERRIDE
139
+ self.logger.warning(f"MANUAL MODEL OVERRIDE ENABLED! Using model: {self.model_name}")
140
+ self.logger.debug(f"OpenAIManager initialized with model: {self.model_name}")
188
141
  except Exception as e:
189
- self.logger.error(f"Failed to load local model '{self.model_name}': {e}", exc_info=True)
190
- self.model = None
191
- self.tokenizer = None
192
-
193
- # if self.is_nllb:
194
- # self.tokenizer = NllbTokenizer().from_pretrained(self.model_name)
195
-
142
+ self.logger.error(f"Failed to initialize OpenAI API: {e}")
143
+ self.openai = None
144
+ self.model_name = None
145
+
196
146
  def _build_prompt(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str) -> str:
197
- return super()._build_prompt(lines, sentence, current_line, game_title)
198
-
147
+ prompt = super()._build_prompt(lines, sentence, current_line, game_title)
148
+ return prompt
149
+
199
150
  def process(self, lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "") -> str:
200
- if (not self.model or not self.tokenizer) and not self.pipe:
201
- return "Processing failed: Local AI model not initialized."
151
+ if self.client is None:
152
+ return "Processing failed: OpenAI client not initialized."
202
153
 
203
- text_to_process = self._build_prompt(lines, sentence, current_line, game_title)
204
- self.logger.debug(f"Generated prompt for local model:\n{text_to_process}")
154
+ if not lines or not current_line:
155
+ self.logger.warning(f"Invalid input for process: lines={len(lines)}, current_line={current_line.index}")
156
+ return "Invalid input."
205
157
 
206
158
  try:
207
- if self.is_encoder_decoder:
208
- if self.is_nllb:
209
- # NLLB-specific handling for translation
210
- self.tokenizer.src_lang = "jpn_Jpan"
211
- inputs = self.tokenizer(current_line.text, return_tensors="pt").to(self.device)
212
- generated_tokens = self.model.generate(
213
- **inputs,
214
- forced_bos_token_id=self.tokenizer.convert_tokens_to_ids("eng_Latn"),
215
- max_new_tokens=256
216
- )
217
- result = self.tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
218
- else:
219
- # Generic Seq2Seq
220
- inputs = self.tokenizer(text_to_process, return_tensors="pt").to(self.device)
221
- outputs = self.model.generate(**inputs, max_new_tokens=256)
222
- result = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
223
- else:
224
- # Causal LM with chat template
225
- messages = [
226
- # {"role": "system", "content": "You are a helpful assistant that accurately translates Japanese game dialogue into natural, context-aware English."},
227
- {"role": "user", "content": text_to_process}
228
- ]
229
- tokenized_chat = self.tokenizer.apply_chat_template(
230
- messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
231
- ).to(self.device)
232
- outputs = self.model.generate(tokenized_chat, max_new_tokens=256)
233
- result = self.tokenizer.decode(outputs[0][tokenized_chat.shape[-1]:], skip_special_tokens=True)
234
- # result = self.pipe(messages, max_new_tokens=50)
235
- print(result)
236
- # result = result[0]['generated_text']
237
- result = result.strip()
238
-
239
- result = result.strip()
240
- self.logger.debug(f"Received response from local model:\n{result}")
241
- return result
159
+ prompt = self._build_prompt(lines, sentence, current_line, game_title)
160
+ self.logger.debug(f"Generated prompt:\n{prompt}")
161
+ response = self.client.chat.completions.create(
162
+ model=self.model_name,
163
+ messages=[
164
+ {"role": "system", "content": "You are a helpful assistant that translates game dialogue. Provide output in the form of json with a single key 'output'."},
165
+ {"role": "user", "content": prompt}
166
+ ],
167
+ temperature=0.3,
168
+ max_tokens=4096,
169
+ top_p=1,
170
+ n=1,
171
+ stop=None,
172
+ )
173
+ if response.choices and response.choices[0].message.content:
174
+ text_output = response.choices[0].message.content.strip()
175
+ # get the json at the end of the message
176
+ if "{" in text_output and "}" in text_output:
177
+ json_output = text_output[text_output.find("{"):text_output.rfind("}")+1]
178
+ text_output = json.loads(json_output)['output']
179
+ self.logger.debug(f"Received response:\n{text_output}")
180
+ return text_output
242
181
  except Exception as e:
243
- self.logger.error(f"Local model processing failed: {e}", exc_info=True)
182
+ self.logger.error(f"OpenAI processing failed: {e}")
244
183
  return f"Processing failed: {e}"
245
184
 
246
185
 
@@ -341,9 +280,9 @@ class GroqAI(AIManager):
341
280
  completion = self.client.chat.completions.create(
342
281
  model=self.model_name,
343
282
  messages=[{"role": "user", "content": prompt}],
344
- temperature=.5,
283
+ temperature=0,
345
284
  max_completion_tokens=1024,
346
- top_p=1,
285
+ top_p=.9,
347
286
  stream=False,
348
287
  stop=None,
349
288
  )
@@ -361,7 +300,7 @@ current_ai_config: Ai | None = None
361
300
  def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: GameLine, game_title: str = "", force_refresh: bool = False) -> str:
362
301
  global ai_manager, current_ai_config
363
302
  try:
364
- is_local_provider = get_config().ai.provider == AIType.LOCAL.value
303
+ is_local_provider = get_config().ai.provider == AIType.OPENAI.value
365
304
  if not is_local_provider and not is_connected():
366
305
  logger.error("No internet connection. Unable to proceed with AI prompt.")
367
306
  return ""
@@ -380,12 +319,12 @@ def get_ai_prompt_result(lines: List[GameLine], sentence: str, current_line: Gam
380
319
  logger.info(f"Reusing existing Groq AI Manager for model: {get_config().ai.groq_model}")
381
320
  else:
382
321
  ai_manager = GroqAI(model=get_config().ai.groq_model, api_key=get_config().ai.groq_api_key, logger=logger)
383
- elif provider == AIType.LOCAL.value:
384
- if get_config().ai.local_model in ai_managers:
385
- ai_manager = ai_managers[get_config().ai.local_model]
386
- logger.info(f"Reusing existing Local AI Manager for model: {get_config().ai.local_model}")
322
+ elif provider == AIType.OPENAI.value:
323
+ if get_config().ai.open_ai_model in ai_managers:
324
+ ai_manager = ai_managers[get_config().ai.open_ai_model]
325
+ logger.info(f"Reusing existing OpenAI AI Manager for model: {get_config().ai.open_ai_model}")
387
326
  else:
388
- ai_manager = LocalAIManager(model=get_config().ai.local_model, logger=logger)
327
+ ai_manager = OpenAIManager(model=get_config().ai.open_ai_model, api_key=get_config().ai.open_ai_api_key, api_url=get_config().ai.open_ai_url, logger=logger)
389
328
  else:
390
329
  ai_manager = None
391
330
  if ai_manager:
@@ -410,7 +349,7 @@ def ai_config_changed(config, current):
410
349
  return True
411
350
  if config.provider == AIType.GROQ.value and (config.groq_api_key != current.groq_api_key or config.groq_model != current.groq_model):
412
351
  return True
413
- if config.provider == AIType.LOCAL.value and config.gemini_model != current.gemini_model:
352
+ if config.provider == AIType.OPENAI.value and config.gemini_model != current.gemini_model:
414
353
  return True
415
354
  if config.custom_prompt != current.custom_prompt:
416
355
  return True
@@ -422,11 +361,11 @@ def ai_config_changed(config, current):
422
361
 
423
362
 
424
363
  if __name__ == '__main__':
425
- # logger.setLevel(logging.DEBUG)
426
- # console_handler = logging.StreamHandler()
427
- # console_handler.setLevel(logging.DEBUG)
428
- # logger.addHandler(console_handler)
429
- # logging.basicConfig(level=logging.DEBUG)
364
+ logger.setLevel(logging.DEBUG)
365
+ console_handler = logging.StreamHandler()
366
+ console_handler.setLevel(logging.DEBUG)
367
+ logger.addHandler(console_handler)
368
+ logging.basicConfig(level=logging.DEBUG)
430
369
  lines = [
431
370
  # Sexual/Explicit Japanese words and phrases
432
371
  GameLine(index=0, text="ねぇ、あたしのおっぱい、揉んでみない?", id=None, time=None, prev=None, next=None),
@@ -474,11 +413,14 @@ if __name__ == '__main__':
474
413
  current_line = lines[6]
475
414
  game_title = "Corrupted Reality"
476
415
 
477
- get_config().ai.provider = "Local"
416
+ get_config().ai.provider = AIType.OPENAI.value
478
417
  models = [
418
+ # 'openai/gpt-oss-20b',
419
+ # 'meta-llama-3.1-8b-instruct',
420
+ 'google/gemma-3n-e4b',
479
421
  # 'google/gemma-2-2b-it',
480
422
  # 'google/gemma-2b-it',
481
- 'facebook/nllb-200-distilled-600M',
423
+ # 'facebook/nllb-200-distilled-600M',
482
424
  # 'meta-llama/Llama-3.2-1B-Instruct',
483
425
  # 'facebook/nllb-200-1.3B'
484
426
  ]
@@ -492,9 +434,12 @@ if __name__ == '__main__':
492
434
  # results.append({"model": model,"response": result, "time": time.time() - start_time, "iteration": 1})
493
435
 
494
436
  # Second Time after Already Loaded
495
- for i in range(1, 500):
437
+
438
+ get_config().ai.open_ai_url = "http://127.0.0.1:1234/v1"
439
+ get_config().ai.open_ai_api_key = "lm-studio"
440
+ for i in range(1, 10):
496
441
  for model in models:
497
- get_config().ai.local_model = model
442
+ get_config().ai.open_ai_model = model
498
443
  start_time = time.time()
499
444
  result = get_ai_prompt_result(lines, sentence, current_line, game_title, True)
500
445
  print(result)
@@ -11,6 +11,7 @@ from requests import post
11
11
 
12
12
  from GameSentenceMiner import obs
13
13
  from GameSentenceMiner.ai.ai_prompting import get_ai_prompt_result
14
+ from GameSentenceMiner.util.db import GameLinesTable
14
15
  from GameSentenceMiner.util.gsm_utils import make_unique, sanitize_filename, wait_for_stable_file, remove_html_and_cloze_tags, combine_dialogue, \
15
16
  run_new_thread, open_audio_in_external
16
17
  from GameSentenceMiner.util import ffmpeg, notification
@@ -69,10 +70,19 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
69
70
  prev_screenshot_html = f"<img src=\"{prev_screenshot_in_anki}\">"
70
71
 
71
72
 
73
+ # Vars for DB update
74
+ new_audio_path = ''
75
+ new_screenshot_path = ''
76
+ new_prev_screenshot_path = ''
77
+ new_video_path = ''
78
+ translation = ''
79
+ anki_audio_path = ''
80
+ anki_screenshot_path = ''
72
81
  # Move files to output folder if configured
73
82
  if get_config().paths.output_folder and get_config().paths.copy_temp_files_to_output_folder:
74
83
  word_path = os.path.join(get_config().paths.output_folder, sanitize_filename(tango))
75
84
  os.makedirs(word_path, exist_ok=True)
85
+
76
86
  if audio_path:
77
87
  audio_filename = Path(audio_path).name
78
88
  new_audio_path = os.path.join(word_path, audio_filename)
@@ -94,6 +104,10 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
94
104
  new_video_path = os.path.join(word_path, Path(trimmed_video).name)
95
105
  if os.path.exists(trimmed_video):
96
106
  shutil.copy(trimmed_video, new_video_path)
107
+
108
+ if get_config().audio.anki_media_collection:
109
+ anki_audio_path = os.path.join(get_config().audio.anki_media_collection, audio_in_anki)
110
+ anki_screenshot_path = os.path.join(get_config().audio.anki_media_collection, screenshot_in_anki)
97
111
 
98
112
  # Open to word_path if configured
99
113
  if get_config().paths.open_output_folder_on_card_creation:
@@ -146,7 +160,8 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
146
160
  if tags:
147
161
  tag_string = " ".join(tags)
148
162
  invoke("addTags", tags=tag_string, notes=[last_note.noteId])
149
-
163
+
164
+ # Update GameLine in DB
150
165
 
151
166
  logger.info(f"Adding {game_line.id} to Anki Results Dict...")
152
167
  if not reuse_audio:
@@ -160,6 +175,7 @@ def update_anki_card(last_note: AnkiCard, note=None, audio_path='', video_path='
160
175
  )
161
176
 
162
177
  run_new_thread(lambda: check_and_update_note(last_note, note, tags))
178
+ GameLinesTable.update(line_id=game_line.id, screenshot_path=new_screenshot_path, audio_path=new_audio_path, replay_path=new_video_path, audio_in_anki=anki_audio_path, screenshot_in_anki=anki_screenshot_path, translation=translation)
163
179
 
164
180
  def check_and_update_note(last_note, note, tags=[]):
165
181
  selected_notes = invoke("guiSelectedNotes")
@@ -196,18 +212,43 @@ def add_image_to_card(last_note: AnkiCard, image_path):
196
212
  run_new_thread(lambda: check_and_update_note(last_note, note))
197
213
 
198
214
  logger.info(f"UPDATED IMAGE FOR ANKI CARD {last_note.noteId}")
215
+
216
+
217
+
218
+ # Go through every field in the note and fix whitespace issues
219
+ # the issues being, a ton of new lines randomly, nothing else
220
+ def fix_overlay_whitespace(last_note: AnkiCard, note, lines=None):
221
+ for field in last_note.fields:
222
+ if not last_note.fields[field]:
223
+ continue
224
+ text = last_note.get_field(field)
225
+ # Count occurrences of excessive whitespace patterns using regex
226
+ whitespace_patterns = [
227
+ r'(\r?\n){3,}', # 3 or more consecutive newlines
228
+ r'(\r){3,}', # 3 or more consecutive carriage returns
229
+ r'(\n\r\n){2,}', # 2 or more consecutive \n\r\n
230
+ r'(\r\n\r){2,}', # 2 or more consecutive \r\n\r
231
+ r'(<br>\s*){3,}' # 3 or more consecutive <br>
232
+ ]
233
+ for pattern in whitespace_patterns:
234
+ if re.search(pattern, text):
235
+ fixed_text = re.sub(pattern, '', text)
236
+ note['fields'][field] = fixed_text
237
+ last_note.fields[field].value = fixed_text
238
+ break
239
+ return note, last_note
199
240
 
200
241
 
201
242
  def get_initial_card_info(last_note: AnkiCard, selected_lines):
202
243
  note = {'id': last_note.noteId, 'fields': {}}
203
244
  if not last_note:
204
- return note
245
+ return note, last_note
246
+ note, last_note = fix_overlay_whitespace(last_note, note, selected_lines)
205
247
  game_line = get_text_event(last_note)
206
248
  sentences = []
207
249
  sentences_text = ''
208
250
 
209
- # TODO: REMOVE THIS, I DON'T THINK IT'S NEEDED
210
- if get_config().wip.overlay_websocket_send:
251
+ if get_config().overlay.websocket_port and texthooking_page.overlay_server_thread.has_clients():
211
252
  sentence_in_anki = last_note.get_field(get_config().anki.sentence_field).replace("\n", "").replace("\r", "").strip()
212
253
  if lines_match(game_line.text, remove_html_and_cloze_tags(sentence_in_anki)):
213
254
  logger.info("Found matching line in Anki, Preserving HTML and fix spacing!")
@@ -274,7 +315,7 @@ def get_initial_card_info(last_note: AnkiCard, selected_lines):
274
315
  note['fields'][get_config().anki.previous_sentence_field] = selected_lines[0].prev.text
275
316
  else:
276
317
  note['fields'][get_config().anki.previous_sentence_field] = game_line.prev.text
277
- return note
318
+ return note, last_note
278
319
 
279
320
 
280
321
  def store_media_file(path):
@@ -404,7 +445,8 @@ def update_card_from_same_sentence(last_card, lines, game_line):
404
445
  time.sleep(0.5)
405
446
  anki_result = anki_results[game_line.id]
406
447
  if anki_result.success:
407
- update_anki_card(last_card, note=get_initial_card_info(last_card, lines),
448
+ note, last_card = get_initial_card_info(last_card, lines)
449
+ update_anki_card(last_card, note=note,
408
450
  game_line=get_mined_line(last_card, lines), reuse_audio=True)
409
451
  else:
410
452
  logger.error(f"Anki update failed for card {last_card.noteId}")