Shirochi commited on
Commit
4b70c44
·
verified ·
1 Parent(s): 0e9c2e0

Upload 11 files

Browse files
Files changed (4) hide show
  1. antigravity_proxy.py +130 -20
  2. app.py +186 -16
  3. splash_utils.py +3 -3
  4. unified_api_client.py +3 -5
antigravity_proxy.py CHANGED
@@ -52,6 +52,64 @@ _cancel_event = threading.Event()
52
  _proxy_process: Optional[subprocess.Popen] = None
53
  _proxy_launch_lock = threading.Lock()
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  def cancel_stream():
57
  """Signal any active Antigravity proxy stream to abort."""
@@ -138,6 +196,38 @@ def _find_npx() -> Optional[str]:
138
  return None
139
 
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  def ensure_proxy_running(log_fn=None) -> Dict[str, Any]:
142
  """Ensure the Antigravity proxy is running, auto-launching if needed.
143
 
@@ -152,6 +242,9 @@ def ensure_proxy_running(log_fn=None) -> Dict[str, Any]:
152
 
153
  _log = log_fn or (lambda msg: None)
154
 
 
 
 
155
  # Already running?
156
  health = check_proxy_health()
157
  if health.get("healthy"):
@@ -204,6 +297,15 @@ def ensure_proxy_running(log_fn=None) -> Dict[str, Any]:
204
  "stderr": subprocess.DEVNULL,
205
  "stdin": subprocess.DEVNULL,
206
  }
 
 
 
 
 
 
 
 
 
207
  if sys.platform == "win32":
208
  # CREATE_NEW_PROCESS_GROUP + DETACHED_PROCESS so it survives app close
209
  CREATE_NEW_PROCESS_GROUP = 0x00000200
@@ -359,6 +461,20 @@ def send_message(
359
  "The model may need more time for long translations."
360
  )
361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  if resp.status_code != 200:
363
  error_body = resp.text
364
  try:
@@ -366,17 +482,6 @@ def send_message(
366
  error_msg = error_json.get("error", {}).get("message", error_body)
367
  except Exception:
368
  error_msg = error_body
369
-
370
- # Auto-open browser for authentication if proxy returns 401/403
371
- if resp.status_code in (401, 403):
372
- auth_url = proxy_url # e.g. http://localhost:8080
373
- if log_fn:
374
- log_fn(f"🔐 Antigravity: Authentication required – opening {auth_url} in your browser...")
375
- try:
376
- webbrowser.open(auth_url)
377
- except Exception:
378
- pass
379
-
380
  raise RuntimeError(
381
  f"Antigravity: {resp.status_code} - {error_msg}"
382
  )
@@ -477,16 +582,21 @@ def send_message_stream(
477
  f"Antigravity proxy streaming request timed out after {timeout}s."
478
  )
479
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  if resp.status_code != 200:
481
- # Auto-open browser for authentication if proxy returns 401/403
482
- if resp.status_code in (401, 403):
483
- auth_url = proxy_url
484
- if log_fn:
485
- log_fn(f"🔐 Antigravity: Authentication required – opening {auth_url} in your browser...")
486
- try:
487
- webbrowser.open(auth_url)
488
- except Exception:
489
- pass
490
  raise RuntimeError(f"Antigravity: {resp.status_code} - {resp.text[:500]}")
491
 
492
  # Collect SSE events
 
52
  _proxy_process: Optional[subprocess.Popen] = None
53
  _proxy_launch_lock = threading.Lock()
54
 
55
+ # Auth browser tracking — only open the browser once per session
56
+ _auth_browser_opened = False
57
+ _auth_browser_lock = threading.Lock()
58
+
59
+
60
+ def _open_auth_browser_once(proxy_url: str, log_fn=None) -> bool:
61
+ """Open the proxy auth URL in the browser, but only once per session.
62
+
63
+ Returns True if the browser was opened (first call), False if already opened.
64
+ """
65
+ global _auth_browser_opened
66
+ with _auth_browser_lock:
67
+ if _auth_browser_opened:
68
+ return False
69
+ _auth_browser_opened = True
70
+ _log = log_fn or (lambda msg: None)
71
+ _log(f"🔐 Antigravity: Authentication required – opening {proxy_url} in your browser...")
72
+ _log(f" Please link your Google account. Glossarion will continue automatically once done.")
73
+ try:
74
+ webbrowser.open(proxy_url)
75
+ except Exception:
76
+ pass
77
+ return True
78
+
79
+
80
+ def _wait_for_auth(
81
+ url: str,
82
+ payload: dict,
83
+ headers: dict,
84
+ proxy_url: str,
85
+ log_fn=None,
86
+ max_wait: int = 120,
87
+ poll_interval: int = 5,
88
+ stream: bool = False,
89
+ ):
90
+ """Open browser once and poll until authentication succeeds or timeout.
91
+
92
+ Returns the successful requests.Response, or None if timed out.
93
+ """
94
+ _open_auth_browser_once(proxy_url, log_fn)
95
+ _log = log_fn or (lambda msg: None)
96
+ elapsed = 0
97
+ while elapsed < max_wait:
98
+ time.sleep(poll_interval)
99
+ elapsed += poll_interval
100
+ if _cancel_event.is_set():
101
+ return None
102
+ _log(f"⏳ Waiting for authentication... ({elapsed}s / {max_wait}s)")
103
+ try:
104
+ retry_resp = requests.post(
105
+ url, json=payload, headers=headers, timeout=30, stream=stream
106
+ )
107
+ if retry_resp.status_code not in (401, 403):
108
+ return retry_resp
109
+ except Exception:
110
+ continue
111
+ return None
112
+
113
 
114
  def cancel_stream():
115
  """Signal any active Antigravity proxy stream to abort."""
 
196
  return None
197
 
198
 
199
+ def _ensure_proxy_config():
200
+ """Ensure the proxy config disables API key auth.
201
+
202
+ The antigravity-claude-proxy validates an ``apiKey`` on every ``/v1/*``
203
+ request. Glossarion talks to the proxy on localhost, so there is no
204
+ need for this gate. We write ``{"apiKey": ""}`` (skip validation) into
205
+ the proxy's config file so that any dummy token we send is accepted.
206
+ Existing settings in the file are preserved; only ``apiKey`` is touched.
207
+ """
208
+ try:
209
+ config_dir = os.path.join(os.path.expanduser("~"), ".config", "antigravity-proxy")
210
+ os.makedirs(config_dir, exist_ok=True)
211
+ config_path = os.path.join(config_dir, "config.json")
212
+
213
+ # Read existing config if present, otherwise start fresh
214
+ existing: Dict[str, Any] = {}
215
+ if os.path.isfile(config_path):
216
+ try:
217
+ with open(config_path, "r", encoding="utf-8") as f:
218
+ existing = json.loads(f.read())
219
+ except Exception:
220
+ existing = {}
221
+
222
+ # Only write if apiKey is not already empty
223
+ if existing.get("apiKey", None) != "":
224
+ existing["apiKey"] = ""
225
+ with open(config_path, "w", encoding="utf-8") as f:
226
+ f.write(json.dumps(existing, indent=2))
227
+ except Exception:
228
+ pass # Non-critical – proxy may still work without this
229
+
230
+
231
  def ensure_proxy_running(log_fn=None) -> Dict[str, Any]:
232
  """Ensure the Antigravity proxy is running, auto-launching if needed.
233
 
 
242
 
243
  _log = log_fn or (lambda msg: None)
244
 
245
+ # Ensure proxy config disables API key auth (localhost doesn't need it)
246
+ _ensure_proxy_config()
247
+
248
  # Already running?
249
  health = check_proxy_health()
250
  if health.get("healthy"):
 
297
  "stderr": subprocess.DEVNULL,
298
  "stdin": subprocess.DEVNULL,
299
  }
300
+
301
+ # Ensure node.exe's directory is on PATH for the subprocess
302
+ # (npx.cmd invokes "node" and needs it resolvable)
303
+ npx_dir = os.path.dirname(npx_path)
304
+ env = os.environ.copy()
305
+ if npx_dir not in env.get("PATH", ""):
306
+ env["PATH"] = npx_dir + os.pathsep + env.get("PATH", "")
307
+ kwargs["env"] = env
308
+
309
  if sys.platform == "win32":
310
  # CREATE_NEW_PROCESS_GROUP + DETACHED_PROCESS so it survives app close
311
  CREATE_NEW_PROCESS_GROUP = 0x00000200
 
461
  "The model may need more time for long translations."
462
  )
463
 
464
+ # Handle auth failure: open browser once and wait for user to authenticate
465
+ if resp.status_code in (401, 403):
466
+ auth_resp = _wait_for_auth(
467
+ url, payload, headers, proxy_url, log_fn, stream=False
468
+ )
469
+ if auth_resp is not None and auth_resp.status_code == 200:
470
+ resp = auth_resp # auth succeeded, continue with this response
471
+ else:
472
+ raise RuntimeError(
473
+ f"Antigravity: Authentication timed out.\n"
474
+ f"Open {proxy_url} in your browser and link your Google account,\n"
475
+ f"then try again."
476
+ )
477
+
478
  if resp.status_code != 200:
479
  error_body = resp.text
480
  try:
 
482
  error_msg = error_json.get("error", {}).get("message", error_body)
483
  except Exception:
484
  error_msg = error_body
 
 
 
 
 
 
 
 
 
 
 
485
  raise RuntimeError(
486
  f"Antigravity: {resp.status_code} - {error_msg}"
487
  )
 
582
  f"Antigravity proxy streaming request timed out after {timeout}s."
583
  )
584
 
585
+ # Handle auth failure: open browser once and wait for user to authenticate
586
+ if resp.status_code in (401, 403):
587
+ auth_resp = _wait_for_auth(
588
+ url, payload, headers, proxy_url, log_fn, stream=True
589
+ )
590
+ if auth_resp is not None and auth_resp.status_code == 200:
591
+ resp = auth_resp # auth succeeded, continue with this streaming response
592
+ else:
593
+ raise RuntimeError(
594
+ f"Antigravity: Authentication timed out.\n"
595
+ f"Open {proxy_url} in your browser and link your Google account,\n"
596
+ f"then try again."
597
+ )
598
+
599
  if resp.status_code != 200:
 
 
 
 
 
 
 
 
 
600
  raise RuntimeError(f"Antigravity: {resp.status_code} - {resp.text[:500]}")
601
 
602
  # Collect SSE events
app.py CHANGED
@@ -10,6 +10,8 @@ import sys
10
  import json
11
  import tempfile
12
  import base64
 
 
13
  from pathlib import Path
14
 
15
  # CRITICAL: Set API delay IMMEDIATELY at module level before any other imports
@@ -82,7 +84,7 @@ except ImportError as e:
82
 
83
 
84
  # Models that do not require an API key
85
- _NO_API_KEY_PREFIXES = ('vertex/', 'authgpt/', 'google-translate', 'deepl')
86
 
87
  def _model_needs_no_api_key(model: str) -> bool:
88
  """Return True if the given model name does not require a user-supplied API key."""
@@ -92,6 +94,72 @@ def _model_needs_no_api_key(model: str) -> bool:
92
  return any(m.startswith(p) for p in _NO_API_KEY_PREFIXES)
93
 
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  class GlossarionWeb:
96
  """Web interface for Glossarion translator"""
97
 
@@ -211,14 +279,20 @@ class GlossarionWeb:
211
  print(f"🤖 Loaded {len(self.models)} models: {self.models[:5]}{'...' if len(self.models) > 5 else ''}")
212
 
213
  # Translation state management
214
- import threading
 
 
215
  self.is_translating = False
216
  self.stop_flag = threading.Event()
217
  self.translation_thread = None
218
  self.current_unified_client = None # Track active client to allow cancellation
219
  self.current_translator = None # Track active translator to allow shutdown
220
 
221
- # Add stop flags for different translation types
 
 
 
 
222
  self.epub_translation_stop = False
223
  self.epub_translation_thread = None
224
  self.glossary_extraction_stop = False
@@ -830,9 +904,14 @@ class GlossarionWeb:
830
  yield None, None, None, "❌ Please select a translation profile", None, "Error", 0
831
  return
832
 
833
- # Initialize logs list
834
  translation_logs = []
835
 
 
 
 
 
 
836
  try:
837
  # Initial status
838
  input_path = epub_file.name if hasattr(epub_file, 'name') else epub_file
@@ -967,28 +1046,46 @@ class GlossarionWeb:
967
 
968
  # Create a thread-safe queue for capturing logs
969
  import queue
970
- import threading
971
  import time
972
  log_queue = queue.Queue()
973
  translation_complete = threading.Event()
974
  translation_error = [None]
975
 
 
 
 
 
 
 
 
 
 
976
  def log_callback(msg):
977
- """Capture log messages"""
978
  if msg and msg.strip():
979
  log_queue.put(msg.strip())
980
 
 
 
 
 
 
981
  # Run translation in a separate thread
982
  def run_translation():
 
 
 
983
  try:
984
  result = TransateKRtoEN.main(
985
  log_callback=log_callback,
986
- stop_callback=None
987
  )
988
  translation_error[0] = None
989
  except Exception as e:
990
  translation_error[0] = e
991
  finally:
 
 
992
  translation_complete.set()
993
 
994
  translation_thread = threading.Thread(target=run_translation, daemon=True)
@@ -999,10 +1096,16 @@ class GlossarionWeb:
999
  progress_percent = 10
1000
 
1001
  while not translation_complete.is_set() or not log_queue.empty():
1002
- # Check if stop was requested
1003
- if self.epub_translation_stop:
1004
  translation_logs.append("⚠️ Stopping translation...")
1005
- # Try to stop the translation thread
 
 
 
 
 
 
1006
  translation_complete.set()
1007
  break
1008
 
@@ -1367,6 +1470,9 @@ class GlossarionWeb:
1367
  error_msg = f"❌ Error during translation:\n{str(e)}\n\n{traceback.format_exc()}"
1368
  translation_logs.append(error_msg)
1369
  yield None, None, gr.update(visible=False), "\n".join(translation_logs), gr.update(visible=True), "Error occurred", 0
 
 
 
1370
 
1371
  def translate_epub_with_stop(self, *args):
1372
  """Wrapper for translate_epub that includes button visibility control"""
@@ -1387,6 +1493,17 @@ class GlossarionWeb:
1387
  def stop_epub_translation(self):
1388
  """Stop the ongoing EPUB translation"""
1389
  self.epub_translation_stop = True
 
 
 
 
 
 
 
 
 
 
 
1390
  if self.epub_translation_thread and self.epub_translation_thread.is_alive():
1391
  # The thread will check the stop flag
1392
  pass
@@ -1485,31 +1602,46 @@ class GlossarionWeb:
1485
 
1486
  # Create a thread-safe queue for capturing logs
1487
  import queue
1488
- import threading
1489
  import time
1490
  log_queue = queue.Queue()
1491
  extraction_complete = threading.Event()
1492
  extraction_error = [None]
1493
  extraction_result = [None]
1494
 
 
 
 
 
 
 
 
 
1495
  def log_callback(msg):
1496
- """Capture log messages"""
1497
  if msg and msg.strip():
1498
  log_queue.put(msg.strip())
1499
 
 
 
 
 
1500
  # Run extraction in a separate thread
1501
  def run_extraction():
 
 
1502
  try:
1503
  result = extract_glossary_from_epub.main(
1504
  log_callback=log_callback,
1505
- stop_callback=None
1506
  )
1507
  extraction_result[0] = result
1508
  extraction_error[0] = None
1509
  except Exception as e:
1510
  extraction_error[0] = e
1511
  finally:
 
1512
  extraction_complete.set()
 
1513
 
1514
  extraction_thread = threading.Thread(target=run_extraction, daemon=True)
1515
  extraction_thread.start()
@@ -1519,10 +1651,10 @@ class GlossarionWeb:
1519
  progress_percent = 40
1520
 
1521
  while not extraction_complete.is_set() or not log_queue.empty():
1522
- # Check if stop was requested
1523
- if self.glossary_extraction_stop:
1524
  extraction_logs.append("⚠️ Stopping extraction...")
1525
- # Try to stop the extraction thread
1526
  extraction_complete.set()
1527
  break
1528
 
@@ -1615,6 +1747,11 @@ class GlossarionWeb:
1615
  def stop_glossary_extraction(self):
1616
  """Stop the ongoing glossary extraction"""
1617
  self.glossary_extraction_stop = True
 
 
 
 
 
1618
  if self.glossary_extraction_thread and self.glossary_extraction_thread.is_alive():
1619
  # The thread will check the stop flag
1620
  pass
@@ -1973,11 +2110,44 @@ class GlossarionWeb:
1973
  else:
1974
  print("DEBUG: stop_translation called but not translating")
1975
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1976
  def _reset_translation_flags(self):
1977
  """Reset all translation flags for new translation"""
1978
  self.is_translating = False
1979
  self.stop_flag.clear()
1980
 
 
 
 
 
 
 
 
 
 
 
 
 
1981
  # Reset global cancellation flags
1982
  try:
1983
  if MANGA_TRANSLATION_AVAILABLE:
 
10
  import json
11
  import tempfile
12
  import base64
13
+ import threading
14
+ import uuid
15
  from pathlib import Path
16
 
17
  # CRITICAL: Set API delay IMMEDIATELY at module level before any other imports
 
84
 
85
 
86
  # Models that do not require an API key
87
+ _NO_API_KEY_PREFIXES = ('vertex/', 'authgpt/', 'antigravity/', 'google-translate', 'deepl')
88
 
89
  def _model_needs_no_api_key(model: str) -> bool:
90
  """Return True if the given model name does not require a user-supplied API key."""
 
94
  return any(m.startswith(p) for p in _NO_API_KEY_PREFIXES)
95
 
96
 
97
+ # ---------------------------------------------------------------------------
98
+ # Thread-local stdout writer — isolates log output per translation thread
99
+ # so User A's logs never leak into User B's log_callback.
100
+ # ---------------------------------------------------------------------------
101
+ _thread_local = threading.local()
102
+
103
+ class _ThreadLocalStdoutWriter:
104
+ """sys.stdout replacement that routes write() to a per-thread callback.
105
+
106
+ If the current thread has a callback registered in _thread_local.log_cb,
107
+ output goes there. Otherwise it falls through to the original stdout so
108
+ server-side console logs still work.
109
+ """
110
+ def __init__(self, original_stdout):
111
+ self._original = original_stdout
112
+
113
+ # --- required file-like API ---
114
+ def write(self, text):
115
+ cb = getattr(_thread_local, 'log_cb', None)
116
+ if cb is not None:
117
+ # Only forward non-empty, non-whitespace-only text
118
+ stripped = text.strip()
119
+ if stripped:
120
+ try:
121
+ cb(stripped)
122
+ except Exception:
123
+ pass
124
+ return
125
+ # No per-thread callback → use original stdout
126
+ if self._original is not None:
127
+ try:
128
+ self._original.write(text)
129
+ except Exception:
130
+ pass
131
+
132
+ def flush(self):
133
+ if self._original is not None:
134
+ try:
135
+ self._original.flush()
136
+ except Exception:
137
+ pass
138
+
139
+ def fileno(self):
140
+ if self._original is not None:
141
+ return self._original.fileno()
142
+ raise OSError("no underlying fileno")
143
+
144
+ @property
145
+ def encoding(self):
146
+ return getattr(self._original, 'encoding', 'utf-8')
147
+
148
+ def isatty(self):
149
+ return False
150
+
151
+ def readable(self):
152
+ return False
153
+
154
+ def writable(self):
155
+ return True
156
+
157
+ # Install the thread-local writer ONCE at module load so *all* print()
158
+ # calls are routed through it. The original stdout is preserved inside.
159
+ if not isinstance(sys.stdout, _ThreadLocalStdoutWriter):
160
+ sys.stdout = _ThreadLocalStdoutWriter(sys.stdout)
161
+
162
+
163
  class GlossarionWeb:
164
  """Web interface for Glossarion translator"""
165
 
 
279
  print(f"🤖 Loaded {len(self.models)} models: {self.models[:5]}{'...' if len(self.models) > 5 else ''}")
280
 
281
  # Translation state management
282
+ # NOTE: These are per-instance defaults. For a public web app each
283
+ # concurrent request needs its own stop flag — we use a dict keyed by
284
+ # a unique request_id so one user's stop doesn't affect another.
285
  self.is_translating = False
286
  self.stop_flag = threading.Event()
287
  self.translation_thread = None
288
  self.current_unified_client = None # Track active client to allow cancellation
289
  self.current_translator = None # Track active translator to allow shutdown
290
 
291
+ # Per-request stop flags (keyed by request_id uuid)
292
+ self._stop_flags: dict[str, bool] = {} # request_id -> True means "stop"
293
+ self._stop_lock = threading.Lock()
294
+
295
+ # Legacy flags (kept for manga which still uses the old pattern)
296
  self.epub_translation_stop = False
297
  self.epub_translation_thread = None
298
  self.glossary_extraction_stop = False
 
904
  yield None, None, None, "❌ Please select a translation profile", None, "Error", 0
905
  return
906
 
907
+ # Initialize logs list (per-request, NOT shared)
908
  translation_logs = []
909
 
910
+ # Create a unique request id for per-session stop tracking
911
+ request_id = self._new_request_id()
912
+ # Also reset legacy flag
913
+ self.epub_translation_stop = False
914
+
915
  try:
916
  # Initial status
917
  input_path = epub_file.name if hasattr(epub_file, 'name') else epub_file
 
1046
 
1047
  # Create a thread-safe queue for capturing logs
1048
  import queue
 
1049
  import time
1050
  log_queue = queue.Queue()
1051
  translation_complete = threading.Event()
1052
  translation_error = [None]
1053
 
1054
+ # CRITICAL: Reset stop env vars so stale flags don't interfere
1055
+ os.environ['GRACEFUL_STOP'] = '0'
1056
+ os.environ['TRANSLATION_CANCELLED'] = '0'
1057
+ try:
1058
+ from TransateKRtoEN import set_stop_flag as _set_stop
1059
+ _set_stop(False)
1060
+ except ImportError:
1061
+ pass
1062
+
1063
  def log_callback(msg):
1064
+ """Capture log messages — per-request, thread-safe"""
1065
  if msg and msg.strip():
1066
  log_queue.put(msg.strip())
1067
 
1068
+ # Stop callback that TransateKRtoEN.main() will poll
1069
+ _rid = request_id # capture in closure
1070
+ def stop_callback():
1071
+ return self._is_request_stopped(_rid) or self.epub_translation_stop
1072
+
1073
  # Run translation in a separate thread
1074
  def run_translation():
1075
+ # Install per-thread log routing so print() inside
1076
+ # TransateKRtoEN goes to THIS request's queue only.
1077
+ _thread_local.log_cb = log_callback
1078
  try:
1079
  result = TransateKRtoEN.main(
1080
  log_callback=log_callback,
1081
+ stop_callback=stop_callback
1082
  )
1083
  translation_error[0] = None
1084
  except Exception as e:
1085
  translation_error[0] = e
1086
  finally:
1087
+ # Remove per-thread redirect
1088
+ _thread_local.log_cb = None
1089
  translation_complete.set()
1090
 
1091
  translation_thread = threading.Thread(target=run_translation, daemon=True)
 
1096
  progress_percent = 10
1097
 
1098
  while not translation_complete.is_set() or not log_queue.empty():
1099
+ # Check if stop was requested (per-request OR legacy flag)
1100
+ if self._is_request_stopped(request_id) or self.epub_translation_stop:
1101
  translation_logs.append("⚠️ Stopping translation...")
1102
+ # Signal the translation engine to stop as well
1103
+ try:
1104
+ from TransateKRtoEN import set_stop_flag as _set_stop
1105
+ _set_stop(True)
1106
+ except ImportError:
1107
+ pass
1108
+ os.environ['TRANSLATION_CANCELLED'] = '1'
1109
  translation_complete.set()
1110
  break
1111
 
 
1470
  error_msg = f"❌ Error during translation:\n{str(e)}\n\n{traceback.format_exc()}"
1471
  translation_logs.append(error_msg)
1472
  yield None, None, gr.update(visible=False), "\n".join(translation_logs), gr.update(visible=True), "Error occurred", 0
1473
+ finally:
1474
+ # Always clean up the per-request stop flag
1475
+ self._cleanup_request(request_id)
1476
 
1477
  def translate_epub_with_stop(self, *args):
1478
  """Wrapper for translate_epub that includes button visibility control"""
 
1493
  def stop_epub_translation(self):
1494
  """Stop the ongoing EPUB translation"""
1495
  self.epub_translation_stop = True
1496
+ # Also signal the translation engine's global stop flag
1497
+ try:
1498
+ from TransateKRtoEN import set_stop_flag as _set_stop
1499
+ _set_stop(True)
1500
+ except ImportError:
1501
+ pass
1502
+ os.environ['TRANSLATION_CANCELLED'] = '1'
1503
+ # Signal all active per-request stop flags for epub
1504
+ with self._stop_lock:
1505
+ for rid in self._stop_flags:
1506
+ self._stop_flags[rid] = True
1507
  if self.epub_translation_thread and self.epub_translation_thread.is_alive():
1508
  # The thread will check the stop flag
1509
  pass
 
1602
 
1603
  # Create a thread-safe queue for capturing logs
1604
  import queue
 
1605
  import time
1606
  log_queue = queue.Queue()
1607
  extraction_complete = threading.Event()
1608
  extraction_error = [None]
1609
  extraction_result = [None]
1610
 
1611
+ # Per-request stop tracking for glossary extraction
1612
+ gloss_request_id = self._new_request_id()
1613
+ self.glossary_extraction_stop = False
1614
+
1615
+ # CRITICAL: Reset stop env vars
1616
+ os.environ['GRACEFUL_STOP'] = '0'
1617
+ os.environ['TRANSLATION_CANCELLED'] = '0'
1618
+
1619
  def log_callback(msg):
1620
+ """Capture log messages — per-request, thread-safe"""
1621
  if msg and msg.strip():
1622
  log_queue.put(msg.strip())
1623
 
1624
+ _grid = gloss_request_id # capture in closure
1625
+ def stop_callback():
1626
+ return self._is_request_stopped(_grid) or self.glossary_extraction_stop
1627
+
1628
  # Run extraction in a separate thread
1629
  def run_extraction():
1630
+ # Install per-thread log routing
1631
+ _thread_local.log_cb = log_callback
1632
  try:
1633
  result = extract_glossary_from_epub.main(
1634
  log_callback=log_callback,
1635
+ stop_callback=stop_callback
1636
  )
1637
  extraction_result[0] = result
1638
  extraction_error[0] = None
1639
  except Exception as e:
1640
  extraction_error[0] = e
1641
  finally:
1642
+ _thread_local.log_cb = None
1643
  extraction_complete.set()
1644
+ self._cleanup_request(_grid)
1645
 
1646
  extraction_thread = threading.Thread(target=run_extraction, daemon=True)
1647
  extraction_thread.start()
 
1651
  progress_percent = 40
1652
 
1653
  while not extraction_complete.is_set() or not log_queue.empty():
1654
+ # Check if stop was requested (per-request OR legacy flag)
1655
+ if self._is_request_stopped(gloss_request_id) or self.glossary_extraction_stop:
1656
  extraction_logs.append("⚠️ Stopping extraction...")
1657
+ os.environ['TRANSLATION_CANCELLED'] = '1'
1658
  extraction_complete.set()
1659
  break
1660
 
 
1747
  def stop_glossary_extraction(self):
1748
  """Stop the ongoing glossary extraction"""
1749
  self.glossary_extraction_stop = True
1750
+ os.environ['TRANSLATION_CANCELLED'] = '1'
1751
+ # Signal all active per-request stop flags
1752
+ with self._stop_lock:
1753
+ for rid in self._stop_flags:
1754
+ self._stop_flags[rid] = True
1755
  if self.glossary_extraction_thread and self.glossary_extraction_thread.is_alive():
1756
  # The thread will check the stop flag
1757
  pass
 
2110
  else:
2111
  print("DEBUG: stop_translation called but not translating")
2112
 
2113
+ # -- per-request stop helpers --------------------------------------------------
2114
+ def _new_request_id(self) -> str:
2115
+ """Create a fresh request ID and register its stop flag."""
2116
+ rid = str(uuid.uuid4())
2117
+ with self._stop_lock:
2118
+ self._stop_flags[rid] = False
2119
+ return rid
2120
+
2121
+ def _request_stop(self, rid: str):
2122
+ """Signal stop for a given request."""
2123
+ with self._stop_lock:
2124
+ self._stop_flags[rid] = True
2125
+
2126
+ def _is_request_stopped(self, rid: str) -> bool:
2127
+ with self._stop_lock:
2128
+ return self._stop_flags.get(rid, False)
2129
+
2130
+ def _cleanup_request(self, rid: str):
2131
+ with self._stop_lock:
2132
+ self._stop_flags.pop(rid, None)
2133
+
2134
  def _reset_translation_flags(self):
2135
  """Reset all translation flags for new translation"""
2136
  self.is_translating = False
2137
  self.stop_flag.clear()
2138
 
2139
+ # CRITICAL: Reset stop / graceful-stop env vars so stale state
2140
+ # from a previous run does not bleed into the next one.
2141
+ os.environ['GRACEFUL_STOP'] = '0'
2142
+ os.environ['TRANSLATION_CANCELLED'] = '0'
2143
+
2144
+ # Reset global stop flags in the translation engine
2145
+ try:
2146
+ from TransateKRtoEN import set_stop_flag as _set_stop
2147
+ _set_stop(False)
2148
+ except ImportError:
2149
+ pass
2150
+
2151
  # Reset global cancellation flags
2152
  try:
2153
  if MANGA_TRANSLATION_AVAILABLE:
splash_utils.py CHANGED
@@ -55,7 +55,7 @@ class SplashManager(QObject):
55
 
56
  if os.path.isfile(ico_path):
57
  # Set app user model ID immediately
58
- ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('Glossarion.Translator.7.8.0')
59
 
60
  # Set app-level icon
61
  if self.app:
@@ -162,7 +162,7 @@ class SplashManager(QObject):
162
  self._load_icon(layout)
163
 
164
  # Title
165
- title_label = QLabel("Glossarion v7.8.0")
166
  title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
167
  title_font = QFont("Arial", int(max(14, 20 * self._ui_scale)), QFont.Weight.Bold)
168
  title_label.setFont(title_font)
@@ -320,7 +320,7 @@ class SplashManager(QObject):
320
  import platform
321
  if platform.system() == 'Windows':
322
  # Set app user model ID
323
- ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('Glossarion.Translator.7.8.0')
324
 
325
  # Load icon from file and set it on the window
326
  hwnd = int(self.splash_window.winId())
 
55
 
56
  if os.path.isfile(ico_path):
57
  # Set app user model ID immediately
58
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('Glossarion.Translator.7.8.1')
59
 
60
  # Set app-level icon
61
  if self.app:
 
162
  self._load_icon(layout)
163
 
164
  # Title
165
+ title_label = QLabel("Glossarion v7.8.1")
166
  title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
167
  title_font = QFont("Arial", int(max(14, 20 * self._ui_scale)), QFont.Weight.Bold)
168
  title_label.setFont(title_font)
 
320
  import platform
321
  if platform.system() == 'Windows':
322
  # Set app user model ID
323
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('Glossarion.Translator.7.8.1')
324
 
325
  # Load icon from file and set it on the window
326
  hwnd = int(self.splash_window.winId())
unified_api_client.py CHANGED
@@ -14882,12 +14882,10 @@ class UnifiedClient:
14882
  error_type="config_error"
14883
  )
14884
 
14885
- # Authentication erroruser needs to link Google account (don't retry)
14886
- if "401" in error_str or "403" in error_str or "api key" in error_str.lower():
14887
  raise UnifiedClientError(
14888
- f"Antigravity proxy authentication failed.\n"
14889
- f"Open http://localhost:8080 in your browser and link your Google account,\n"
14890
- f"then try again.",
14891
  error_type="config_error"
14892
  )
14893
 
 
14882
  error_type="config_error"
14883
  )
14884
 
14885
+ # Authentication timed out proxy handled the browser+polling already
14886
+ if "authentication timed out" in error_str.lower():
14887
  raise UnifiedClientError(
14888
+ str(exc),
 
 
14889
  error_type="config_error"
14890
  )
14891