pygpt-net 2.6.34__py3-none-any.whl → 2.6.35__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. pygpt_net/CHANGELOG.txt +7 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/controller/chat/common.py +8 -2
  4. pygpt_net/controller/chat/handler/stream_worker.py +55 -43
  5. pygpt_net/controller/painter/common.py +13 -1
  6. pygpt_net/controller/painter/painter.py +11 -2
  7. pygpt_net/core/bridge/bridge.py +1 -5
  8. pygpt_net/core/bridge/context.py +81 -36
  9. pygpt_net/core/bridge/worker.py +3 -1
  10. pygpt_net/core/ctx/bag.py +4 -0
  11. pygpt_net/core/events/app.py +10 -17
  12. pygpt_net/core/events/base.py +17 -25
  13. pygpt_net/core/events/control.py +9 -17
  14. pygpt_net/core/events/event.py +9 -62
  15. pygpt_net/core/events/kernel.py +8 -17
  16. pygpt_net/core/events/realtime.py +8 -17
  17. pygpt_net/core/events/render.py +9 -17
  18. pygpt_net/core/render/web/body.py +394 -36
  19. pygpt_net/core/render/web/pid.py +39 -24
  20. pygpt_net/core/render/web/renderer.py +146 -40
  21. pygpt_net/data/config/config.json +4 -3
  22. pygpt_net/data/config/models.json +3 -3
  23. pygpt_net/data/css/web-blocks.css +3 -2
  24. pygpt_net/data/css/web-chatgpt.css +3 -1
  25. pygpt_net/data/css/web-chatgpt_wide.css +3 -1
  26. pygpt_net/data/locale/locale.de.ini +1 -0
  27. pygpt_net/data/locale/locale.en.ini +3 -2
  28. pygpt_net/data/locale/locale.es.ini +1 -0
  29. pygpt_net/data/locale/locale.fr.ini +1 -0
  30. pygpt_net/data/locale/locale.it.ini +1 -0
  31. pygpt_net/data/locale/locale.pl.ini +2 -1
  32. pygpt_net/data/locale/locale.uk.ini +1 -0
  33. pygpt_net/data/locale/locale.zh.ini +1 -0
  34. pygpt_net/provider/api/google/__init__.py +14 -5
  35. pygpt_net/provider/api/openai/__init__.py +13 -10
  36. pygpt_net/provider/core/config/patch.py +9 -0
  37. pygpt_net/ui/layout/chat/painter.py +63 -4
  38. pygpt_net/ui/widget/draw/painter.py +702 -106
  39. pygpt_net/ui/widget/textarea/web.py +2 -0
  40. {pygpt_net-2.6.34.dist-info → pygpt_net-2.6.35.dist-info}/METADATA +9 -2
  41. {pygpt_net-2.6.34.dist-info → pygpt_net-2.6.35.dist-info}/RECORD +44 -44
  42. {pygpt_net-2.6.34.dist-info → pygpt_net-2.6.35.dist-info}/LICENSE +0 -0
  43. {pygpt_net-2.6.34.dist-info → pygpt_net-2.6.35.dist-info}/WHEEL +0 -0
  44. {pygpt_net-2.6.34.dist-info → pygpt_net-2.6.35.dist-info}/entry_points.txt +0 -0
@@ -6,37 +6,52 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.19 07:00:00 #
9
+ # Updated Date: 2025.09.04 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import io
13
+ from dataclasses import dataclass, field
14
+ from typing import Any, Optional
15
+
13
16
  from pygpt_net.utils import trans
14
17
 
15
18
 
19
+ @dataclass(slots=True)
16
20
  class PidData:
17
-
18
- def __init__(self, pid, meta=None):
19
- """Pid Data"""
20
- self.pid = pid
21
- self.meta = meta
22
- self.images_appended = []
23
- self.urls_appended = []
24
- self.files_appended = []
25
- self._buffer = io.StringIO()
26
- self._live_buffer = io.StringIO()
27
- self._html = io.StringIO()
28
- self._document = io.StringIO()
29
- self.is_cmd = False
30
- self.initialized = False
31
- self.loaded = False
32
- self.item = None
33
- self.use_buffer = False
34
- self.name_user = trans("chat.name.user")
35
- self.name_bot = trans("chat.name.bot")
36
- self.last_time_called = 0
37
- self.cooldown = 1 / 6
38
- self.throttling_min_chars = 5000
39
- self.header = None
21
+ """Pid Data"""
22
+ # Required/primary data
23
+ pid: Any
24
+ meta: Optional[Any] = None
25
+
26
+ # Collections
27
+ images_appended: list = field(default_factory=list)
28
+ urls_appended: list = field(default_factory=list)
29
+ files_appended: list = field(default_factory=list)
30
+
31
+ # Internal buffers (excluded from repr to avoid large dumps)
32
+ _buffer: io.StringIO = field(default_factory=io.StringIO, repr=False)
33
+ _live_buffer: io.StringIO = field(default_factory=io.StringIO, repr=False)
34
+ _html: io.StringIO = field(default_factory=io.StringIO, repr=False)
35
+ _document: io.StringIO = field(default_factory=io.StringIO, repr=False)
36
+
37
+ # Flags/state
38
+ is_cmd: bool = False
39
+ initialized: bool = False
40
+ loaded: bool = False
41
+ item: Optional[Any] = None
42
+ use_buffer: bool = False
43
+
44
+ # Names
45
+ name_user: str = field(default_factory=lambda: trans("chat.name.user"))
46
+ name_bot: str = field(default_factory=lambda: trans("chat.name.bot"))
47
+
48
+ # Throttling / timing
49
+ last_time_called: float = 0.0
50
+ cooldown: float = 1 / 6
51
+ throttling_min_chars: int = 5000
52
+
53
+ # Misc
54
+ header: Optional[Any] = None
40
55
 
41
56
  @property
42
57
  def buffer(self) -> str:
@@ -6,16 +6,19 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.24 02:00:00 #
9
+ # Updated Date: 2025.09.04 00:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import json
13
13
  import os
14
14
  import re
15
+ import gc
16
+ from dataclasses import dataclass, field
15
17
 
16
18
  from datetime import datetime
17
19
  from typing import Optional, List, Any
18
20
  from time import monotonic
21
+ from io import StringIO
19
22
 
20
23
  from pygpt_net.core.render.base import BaseRenderer
21
24
  from pygpt_net.core.text.utils import has_unclosed_code_tag
@@ -48,6 +51,46 @@ class Renderer(BaseRenderer):
48
51
  )
49
52
  RE_AMP_LT_GT = re.compile(r'&(lt|gt);')
50
53
 
54
+ @dataclass(slots=True)
55
+ class _AppendBuffer:
56
+ """Small, allocation-friendly buffer for throttled appends."""
57
+ _buf: StringIO = field(default_factory=StringIO, repr=False)
58
+ _size: int = 0
59
+
60
+ def append(self, s: str) -> None:
61
+ if not s:
62
+ return
63
+ self._buf.write(s)
64
+ self._size += len(s)
65
+
66
+ def is_empty(self) -> bool:
67
+ return self._size == 0
68
+
69
+ def get_and_clear(self) -> str:
70
+ """Return content and replace underlying buffer to release memory eagerly."""
71
+ if self._size == 0:
72
+ return ""
73
+ data = self._buf.getvalue()
74
+ old = self._buf
75
+ # Replace the internal buffer instance to drop capacity immediately
76
+ self._buf = StringIO()
77
+ self._size = 0
78
+ try:
79
+ old.close()
80
+ except Exception:
81
+ pass
82
+ return data
83
+
84
+ def clear(self) -> None:
85
+ """Clear content and drop buffer capacity."""
86
+ old = self._buf
87
+ self._buf = StringIO()
88
+ self._size = 0
89
+ try:
90
+ old.close()
91
+ except Exception:
92
+ pass
93
+
51
94
  def __init__(self, window=None):
52
95
  super(Renderer, self).__init__(window)
53
96
  """
@@ -69,7 +112,7 @@ class Renderer(BaseRenderer):
69
112
  self._file_prefix = 'file:///' if self.window and self.window.core.platforms.is_windows() else 'file://'
70
113
 
71
114
  self._thr = {}
72
- self._throttle_interval = 0.01 # 10 ms delay
115
+ self._throttle_interval = 0.03 # 30 ms delay
73
116
 
74
117
  def prepare(self):
75
118
  """
@@ -328,6 +371,9 @@ class Renderer(BaseRenderer):
328
371
  except Exception:
329
372
  pass
330
373
 
374
+ # release strings
375
+ gc.collect()
376
+
331
377
  def append_context(
332
378
  self,
333
379
  meta: CtxMeta,
@@ -597,7 +643,7 @@ class Renderer(BaseRenderer):
597
643
  if self.window.core.config.get("agent.output.render.all", False):
598
644
  output = ctx.output # full agent output
599
645
  else:
600
- output = ctx.extra["output"] # final output only
646
+ output = ctx.extra["output"] # final output only
601
647
  else:
602
648
  if not output:
603
649
  return
@@ -656,6 +702,7 @@ class Renderer(BaseRenderer):
656
702
  pid = self.get_or_create_pid(meta)
657
703
  pctx = self.pids[pid]
658
704
  pctx.item = ctx
705
+
659
706
  if not text_chunk:
660
707
  if begin:
661
708
  pctx.clear()
@@ -663,13 +710,16 @@ class Renderer(BaseRenderer):
663
710
  self._throttle_reset(pid)
664
711
  return
665
712
 
666
- if begin: # prepare name and avatar header only at the beginning to avoid unnecessary checks
713
+ if begin:
714
+ # Prepare name header once per streaming session
667
715
  pctx.header = self.get_name_header(ctx, stream=True)
668
716
  self.update_names(meta, ctx)
669
717
 
670
718
  name_header_str = pctx.header
671
719
  text_chunk = text_chunk if isinstance(text_chunk, str) else str(text_chunk)
672
- text_chunk = text_chunk.translate({ord('<'): '&lt;', ord('>'): '&gt;'})
720
+ # Escape angle brackets only if present to avoid unnecessary allocations
721
+ if ('<' in text_chunk) or ('>' in text_chunk):
722
+ text_chunk = text_chunk.translate({ord('<'): '&lt;', ord('>'): '&gt;'})
673
723
 
674
724
  if begin:
675
725
  if self.is_debug():
@@ -683,42 +733,69 @@ class Renderer(BaseRenderer):
683
733
  self.clear_chunks_output(pid)
684
734
  self.prev_chunk_replace = False
685
735
 
736
+ # Append to the logical buffer (owned by pid)
686
737
  pctx.append_buffer(text_chunk)
687
-
688
738
  buffer = pctx.buffer
689
- if has_unclosed_code_tag(buffer):
690
- buffer_to_parse = "".join((buffer, "\n```"))
691
- else:
692
- buffer_to_parse = buffer
693
739
 
694
- html = self.parser.parse(buffer_to_parse)
695
- del buffer_to_parse
696
- is_code_block = html.endswith(self.ENDINGS_CODE)
697
- is_list = html.endswith(self.ENDINGS_LIST)
740
+ # Cheap detection of open code fence without full parse
741
+ open_code = has_unclosed_code_tag(buffer)
742
+
743
+ # Newline/flow state
698
744
  is_n = "\n" in text_chunk
699
- is_newline = is_n or buffer.endswith("\n") or is_code_block
700
- force_replace = False
701
- if self.prev_chunk_newline:
702
- force_replace = True
703
- if is_n:
704
- self.prev_chunk_newline = True
705
- else:
706
- self.prev_chunk_newline = False
745
+ is_newline = is_n or buffer.endswith("\n") or open_code
746
+ force_replace = self.prev_chunk_newline
747
+ self.prev_chunk_newline = bool(is_n)
707
748
 
708
749
  replace = False
709
- if is_newline or force_replace or is_list:
750
+ if is_newline or force_replace:
710
751
  replace = True
711
- if is_code_block:
712
- if not is_n:
713
- replace = False
752
+ # Do not replace for an open code block unless a newline arrived
753
+ if open_code and not is_n:
754
+ replace = False
755
+
756
+ thr = self._throttle_get(pid)
757
+ html = None
714
758
 
759
+ # Only parse when required:
760
+ # - a replace is needed now, or
761
+ # - a replace is pending in the throttle and must be refreshed, or
762
+ # - rarely: we must detect list termination without a newline
763
+ need_parse_for_pending_replace = (thr["op"] == 1)
764
+ need_parse_for_list = False
765
+
766
+ if not replace:
767
+ # Very rare case: list closing without newline. Check on a short tail only.
768
+ # This keeps behavior intact while avoiding full-buffer parse on every chunk.
769
+ tail = buffer[-4096:]
770
+ if tail:
771
+ tail_to_parse = f"{tail}\n```" if open_code else tail
772
+ tail_html = self.parser.parse(tail_to_parse)
773
+ need_parse_for_list = tail_html.endswith(self.ENDINGS_LIST)
774
+ # Tail string is short and will be collected promptly
775
+ del tail_html
776
+ if need_parse_for_list:
777
+ replace = True
778
+
779
+ if replace or need_parse_for_pending_replace:
780
+ buffer_to_parse = f"{buffer}\n```" if open_code else buffer
781
+ html = self.parser.parse(buffer_to_parse)
782
+ # Help the GC by breaking the reference as soon as possible
783
+ del buffer_to_parse
784
+
785
+ is_code_block = open_code
786
+
787
+ # Adjust output chunk formatting based on block type
715
788
  if not is_code_block:
716
789
  if is_n:
790
+ # Convert text newlines to <br/> in non-code context
717
791
  text_chunk = text_chunk.replace("\n", "<br/>")
718
792
  else:
793
+ # When previous operation replaced content and this chunk closes the fence,
794
+ # prepend a newline so the final code block renders correctly.
719
795
  if self.prev_chunk_replace and (is_code_block and not has_unclosed_code_tag(text_chunk)):
720
796
  text_chunk = "\n" + text_chunk
721
797
 
798
+ # Update replace flag for next iteration AFTER formatting decisions
722
799
  self.prev_chunk_replace = replace
723
800
 
724
801
  if begin:
@@ -727,7 +804,19 @@ class Renderer(BaseRenderer):
727
804
  except Exception:
728
805
  pass
729
806
 
730
- self._throttle_queue(pid, name_header_str or "", html, text_chunk, replace, is_code_block)
807
+ # Queue throttled emission; HTML is only provided when it is really needed
808
+ self._throttle_queue(
809
+ pid=pid,
810
+ name=name_header_str or "",
811
+ html=html if html is not None else "",
812
+ text_chunk=text_chunk,
813
+ replace=replace,
814
+ is_code_block=is_code_block,
815
+ )
816
+ # Explicitly drop local ref to large html string as early as possible
817
+ html = None
818
+
819
+ # Emit if throttle interval allows
731
820
  self._throttle_emit(pid, force=False)
732
821
 
733
822
  def next_chunk(
@@ -1276,7 +1365,7 @@ class Renderer(BaseRenderer):
1276
1365
  extra = ctx.extra["footer"]
1277
1366
  extra_style = "display:block;"
1278
1367
 
1279
- return f'<div class="msg-box msg-user" id="{msg_id}"><div class="name-header name-user">{name}</div><div class="msg">{html}<div class="msg-extra" style="{extra_style}">{extra}</div>{debug}</div></div>'
1368
+ return f'<div class="msg-box msg-user" id="{msg_id}"><div class="name-header name-user">{name}</div><div class="msg">{html}<div class="msg-extra" style="{extra_style}">{extra}</div>{debug}</div></div>'
1280
1369
 
1281
1370
  def prepare_node_output(
1282
1371
  self,
@@ -1289,7 +1378,7 @@ class Renderer(BaseRenderer):
1289
1378
  """
1290
1379
  Prepare output node
1291
1380
 
1292
- :param meta: context meta
1381
+ :param meta: CtxMeta
1293
1382
  :param ctx: CtxItem instance
1294
1383
  :param html: html text
1295
1384
  :param prev_ctx: previous context item
@@ -1399,12 +1488,12 @@ class Renderer(BaseRenderer):
1399
1488
  """
1400
1489
  try:
1401
1490
  if replace:
1402
- self.get_output_node_by_pid(pid).page().runJavaScript(
1403
- f"if (typeof window.replaceNodes !== 'undefined') replaceNodes({self.to_json(self.sanitize_html(html))});"
1491
+ self.get_output_node_by_pid(pid).page().bridge.nodeReplace.emit(
1492
+ self.sanitize_html(html)
1404
1493
  )
1405
1494
  else:
1406
- self.get_output_node_by_pid(pid).page().runJavaScript(
1407
- f"if (typeof window.appendNode !== 'undefined') appendNode({self.to_json(self.sanitize_html(html))});"
1495
+ self.get_output_node_by_pid(pid).page().bridge.node.emit(
1496
+ self.sanitize_html(html)
1408
1497
  )
1409
1498
  except Exception:
1410
1499
  pass
@@ -1772,6 +1861,9 @@ class Renderer(BaseRenderer):
1772
1861
  """
1773
1862
  if not html:
1774
1863
  return ""
1864
+ # Fast path: avoid regex work and extra allocations when not needed
1865
+ if '&amp;' not in html:
1866
+ return html
1775
1867
  return self.RE_AMP_LT_GT.sub(r'&\1;', html)
1776
1868
 
1777
1869
  def append_debug(
@@ -1819,7 +1911,14 @@ class Renderer(BaseRenderer):
1819
1911
  """
1820
1912
  thr = self._thr.get(pid)
1821
1913
  if thr is None:
1822
- thr = {"last": 0.0, "op": 0, "name": "", "replace_html": "", "append": [], "code": False}
1914
+ thr = {
1915
+ "last": 0.0,
1916
+ "op": 0,
1917
+ "name": "",
1918
+ "replace_html": "",
1919
+ "append": Renderer._AppendBuffer(),
1920
+ "code": False,
1921
+ }
1823
1922
  self._thr[pid] = thr
1824
1923
  return thr
1825
1924
 
@@ -1837,7 +1936,8 @@ class Renderer(BaseRenderer):
1837
1936
  thr["op"] = 0
1838
1937
  thr["name"] = ""
1839
1938
  thr["replace_html"] = ""
1840
- thr["append"].clear()
1939
+ # Replace append buffer instance to drop any capacity eagerly
1940
+ thr["append"] = Renderer._AppendBuffer()
1841
1941
  thr["code"] = False
1842
1942
 
1843
1943
  def _throttle_queue(
@@ -1866,10 +1966,12 @@ class Renderer(BaseRenderer):
1866
1966
  if replace:
1867
1967
  thr["op"] = 1
1868
1968
  thr["replace_html"] = html
1969
+ # Drop previous append items aggressively when a replace snapshot is available
1869
1970
  thr["append"].clear()
1870
1971
  thr["code"] = bool(is_code_block)
1871
1972
  else:
1872
1973
  if thr["op"] == 1:
1974
+ # Refresh the pending replace with the latest HTML snapshot
1873
1975
  thr["replace_html"] = html
1874
1976
  thr["code"] = bool(is_code_block)
1875
1977
  return
@@ -1895,17 +1997,21 @@ class Renderer(BaseRenderer):
1895
1997
 
1896
1998
  try:
1897
1999
  if thr["op"] == 1:
2000
+ # Replace snapshot
2001
+ replace_payload = self.sanitize_html(thr["replace_html"])
1898
2002
  node.page().bridge.chunk.emit(
1899
2003
  thr["name"],
1900
- self.sanitize_html(thr["replace_html"]),
2004
+ replace_payload,
1901
2005
  "",
1902
2006
  True,
1903
2007
  bool(thr["code"]),
1904
2008
  )
2009
+ thr["replace_html"] = "" # Cut reference ASAP
1905
2010
  thr["last"] = now
1906
2011
 
1907
- if thr["append"]:
1908
- append_str = "".join(thr["append"])
2012
+ # Append tail (if any)
2013
+ if not thr["append"].is_empty():
2014
+ append_str = thr["append"].get_and_clear()
1909
2015
  node.page().bridge.chunk.emit(
1910
2016
  thr["name"],
1911
2017
  "",
@@ -1917,8 +2023,8 @@ class Renderer(BaseRenderer):
1917
2023
 
1918
2024
  self._throttle_reset(pid)
1919
2025
 
1920
- elif thr["op"] == 2 and thr["append"]:
1921
- append_str = "".join(thr["append"])
2026
+ elif thr["op"] == 2 and not thr["append"].is_empty():
2027
+ append_str = thr["append"].get_and_clear()
1922
2028
  node.page().bridge.chunk.emit(
1923
2029
  thr["name"],
1924
2030
  "",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.6.34",
4
- "app.version": "2.6.34",
5
- "updated_at": "2025-09-03T00:00:00"
3
+ "version": "2.6.35",
4
+ "app.version": "2.6.35",
5
+ "updated_at": "2025-09-04T00:00:00"
6
6
  },
7
7
  "access.audio.event.speech": false,
8
8
  "access.audio.event.speech.disabled": [],
@@ -354,6 +354,7 @@
354
354
  "painter.brush.mode": "brush",
355
355
  "painter.brush.size": 3,
356
356
  "painter.canvas.size": "1280x720",
357
+ "painter.zoom": 100,
357
358
  "personalize.about": "",
358
359
  "personalize.modes": "chat",
359
360
  "plugins": {},
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "__meta__": {
3
- "version": "2.6.34",
4
- "app.version": "2.6.34",
5
- "updated_at": "2025-09-03T08:03:34"
3
+ "version": "2.6.35",
4
+ "app.version": "2.6.35",
5
+ "updated_at": "2025-09-04T08:03:34"
6
6
  },
7
7
  "items": {
8
8
  "SpeakLeash/bielik-11b-v2.3-instruct:Q4_K_M": {
@@ -13,7 +13,6 @@ body {{
13
13
  word-wrap: break-word;
14
14
  padding: 6px;
15
15
  line-height: 1.25;
16
- will-change: transform, opacity;
17
16
  margin: 4px;
18
17
  padding: 0;
19
18
  max-width: 100%;
@@ -26,10 +25,12 @@ body {{
26
25
  -webkit-border-radius: 1ex;
27
26
  -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
28
27
  }}
28
+ #container {{
29
+ will-change: transform, opacity;
30
+ }}
29
31
  .ts {{
30
32
  display: none;
31
33
  }}
32
-
33
34
  /* base */
34
35
  a {{
35
36
  text-decoration: none;
@@ -13,7 +13,6 @@ body {{
13
13
  word-wrap: break-word;
14
14
  padding: 6px;
15
15
  line-height: 1.25;
16
- will-change: transform, opacity;
17
16
  padding: 0;
18
17
  margin: auto;
19
18
  max-width: 720px;
@@ -27,6 +26,9 @@ body {{
27
26
  -webkit-border-radius: 1ex;
28
27
  -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
29
28
  }}
29
+ #container {{
30
+ will-change: transform, opacity;
31
+ }}
30
32
  .ts {{
31
33
  display: none;
32
34
  }}
@@ -13,7 +13,6 @@ body {{
13
13
  word-wrap: break-word;
14
14
  padding: 6px;
15
15
  line-height: 1.25;
16
- will-change: transform, opacity;
17
16
  padding: 0;
18
17
  margin: auto;
19
18
  max-width: 100%;
@@ -27,6 +26,9 @@ body {{
27
26
  -webkit-border-radius: 1ex;
28
27
  -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
29
28
  }}
29
+ #container {{
30
+ will-change: transform, opacity;
31
+ }}
30
32
  .ts {{
31
33
  display: none;
32
34
  }}
@@ -961,6 +961,7 @@ painter.btn.camera.capture = Von der Kamera
961
961
  painter.btn.capture = Bild erfassen
962
962
  painter.btn.clear = Löschen
963
963
  painter.btn.crop = Zuschneiden
964
+ painter.btn.fit = Anpassen
964
965
  painter.capture.manual.captured.success = Bild erfasst:
965
966
  painter.capture.name.prefix = Zeichnung von
966
967
  painter.mode.erase = Radieren
@@ -147,6 +147,7 @@ assistant.store.expire_days.desc = 0 = Never
147
147
  assistant.store.files.suffix = files
148
148
  assistant.store.hide_threads = Hide threads vector stores
149
149
  assistant.store.id = ID
150
+ assistant.store.menu.file.delete = Delete file
150
151
  assistant.store.name = Name
151
152
  assistant.store.status = Status
152
153
  assistant.store.thread_only = (current thread only)
@@ -250,10 +251,10 @@ confirm.assistant.import = Import all assistants from API?
250
251
  confirm.assistant.import_files = Import all files from API?
251
252
  confirm.assistant.import_files.store = Import current store files from API?
252
253
  confirm.assistant.store.clear = Clear vector stores (local only)?
254
+ confirm.assistant.store.file.delete = Delete selected file in API?
253
255
  confirm.assistant.store.import = Import all vector stores from API?
254
256
  confirm.assistant.store.refresh = Refresh all stores?
255
257
  confirm.assistant.store.truncate = Delete all vector stores in API?
256
- confirm.assistant.store.file.delete = Delete selected file in API?
257
258
  confirm.ctx.delete = Delete group?
258
259
  confirm.ctx.delete.all = Delete group and all items?
259
260
  confirm.img.delete = Delete file from disk?
@@ -963,6 +964,7 @@ painter.btn.camera.capture = From camera
963
964
  painter.btn.capture = Use image
964
965
  painter.btn.clear = Clear
965
966
  painter.btn.crop = Crop
967
+ painter.btn.fit = Fit
966
968
  painter.capture.manual.captured.success = Image captured:
967
969
  painter.capture.name.prefix = Drawing from
968
970
  painter.mode.erase = Erase
@@ -1614,4 +1616,3 @@ vision.capture.manual.captured.success = Image captured from the camera:
1614
1616
  vision.capture.name.prefix = Camera capture:
1615
1617
  vision.capture.options.title = Video capture
1616
1618
  vision.checkbox.tooltip = If checked, the vision model is active. It will be automatically activated upon image upload. You can deactivate it in real-time.
1617
- assistant.store.menu.file.delete = Delete file
@@ -962,6 +962,7 @@ painter.btn.camera.capture = De la cámara
962
962
  painter.btn.capture = Capturar imagen
963
963
  painter.btn.clear = Limpiar
964
964
  painter.btn.crop = Recortar
965
+ painter.btn.fit = Ajustar
965
966
  painter.capture.manual.captured.success = Imagen capturada:
966
967
  painter.capture.name.prefix = Dibujo de
967
968
  painter.mode.erase = Borrar
@@ -961,6 +961,7 @@ painter.btn.camera.capture = De la caméra
961
961
  painter.btn.capture = Capturer l'image
962
962
  painter.btn.clear = Effacer
963
963
  painter.btn.crop = Rogner
964
+ painter.btn.fit = Adapter
964
965
  painter.capture.manual.captured.success = Image capturée:
965
966
  painter.capture.name.prefix = Dessin de
966
967
  painter.mode.erase = Effacer
@@ -961,6 +961,7 @@ painter.btn.camera.capture = Dalla fotocamera
961
961
  painter.btn.capture = Cattura immagine
962
962
  painter.btn.clear = Pulire
963
963
  painter.btn.crop = Ritaglia
964
+ painter.btn.fit = Adatta
964
965
  painter.capture.manual.captured.success = Immagine catturata:
965
966
  painter.capture.name.prefix = Disegno da
966
967
  painter.mode.erase = Cancellare
@@ -965,6 +965,7 @@ painter.btn.camera.capture = Z kamery
965
965
  painter.btn.capture = Użyj obrazu
966
966
  painter.btn.clear = Wyczyść
967
967
  painter.btn.crop = Przytnij
968
+ painter.btn.fit = Dopasuj
968
969
  painter.capture.manual.captured.success = Image captured:
969
970
  painter.capture.name.prefix = Drawing from
970
971
  painter.mode.erase = Gumka
@@ -1403,7 +1404,7 @@ settings.vision.capture.height = Wysokość przechwytywania (w pikselach)
1403
1404
  settings.vision.capture.idx = Urządzenie Kamery
1404
1405
  settings.vision.capture.idx.desc = Wybierz urządzenie kamery do przechwytywania wideo w czasie rzeczywistym
1405
1406
  settings.vision.capture.quality = Jakość przechwytywania (%)
1406
- settings.vision.capture.width = Szerokość przechwytywania (w pikselach)
1407
+ settings.vision.capture.width = Szerokość przechwytywania (w pikselach)
1407
1408
  settings.zoom = Powiększenie okna outputu czatu
1408
1409
  speech.enable = Mowa
1409
1410
  speech.listening = Mów teraz...
@@ -961,6 +961,7 @@ painter.btn.camera.capture = З камери
961
961
  painter.btn.capture = Захопити зображення
962
962
  painter.btn.clear = Очистити
963
963
  painter.btn.crop = Обрізати
964
+ painter.btn.fit = Підігнати
964
965
  painter.capture.manual.captured.success = Зображення захоплено:
965
966
  painter.capture.name.prefix = Малюнок з
966
967
  painter.mode.erase = Стерти
@@ -961,6 +961,7 @@ painter.btn.camera.capture = 从相机
961
961
  painter.btn.capture = 使用图像
962
962
  painter.btn.clear = 清除
963
963
  painter.btn.crop = 裁剪
964
+ painter.btn.fit = 适应
964
965
  painter.capture.manual.captured.success = 图像已捕获:
965
966
  painter.capture.name.prefix = 绘制自
966
967
  painter.mode.erase = 擦除
@@ -81,10 +81,7 @@ class ApiGoogle:
81
81
  filtered["location"] = os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1")
82
82
  # filtered["http_options"] = gtypes.HttpOptions(api_version="v1")
83
83
 
84
- if self.client is None or self.last_client_args != filtered:
85
- self.client = genai.Client(**filtered)
86
- self.last_client_args = filtered
87
- return self.client
84
+ return genai.Client(**filtered)
88
85
 
89
86
  def call(
90
87
  self,
@@ -337,4 +334,16 @@ class ApiGoogle:
337
334
  # self.client.close()
338
335
  except Exception as e:
339
336
  self.window.core.debug.log(e)
340
- print("Error closing Google client:", e)
337
+ print("Error closing Google client:", e)
338
+
339
+ def safe_close(self):
340
+ """Close client"""
341
+ if self.locked:
342
+ return
343
+ if self.client is not None:
344
+ try:
345
+ self.client.close()
346
+ self.client = None
347
+ except Exception as e:
348
+ self.window.core.debug.log(e)
349
+ print("Error closing client:", e)