pouluo commited on
Commit
afd490e
Β·
1 Parent(s): cf52a71

Fix socket leaks on shutdown, remove hardcoded fake thinking block, and add debug logs for requests and tools

Browse files
Files changed (1) hide show
  1. gemini_web2api.py +210 -62
gemini_web2api.py CHANGED
@@ -62,6 +62,7 @@ DEFAULT_CONFIG = {
62
  # Request jitter (ms) - randomized delays to mimic human behavior
63
  "jitter_min_ms": 50,
64
  "jitter_max_ms": 300,
 
65
  }
66
 
67
  CONFIG = dict(DEFAULT_CONFIG)
@@ -403,6 +404,60 @@ class GeminiHTTPClient:
403
 
404
  return resp.read().decode("utf-8", errors="replace")
405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  def close(self):
407
  if self._session:
408
  try:
@@ -424,7 +479,7 @@ def get_http_client() -> GeminiHTTPClient:
424
 
425
  # ─── Gemini Protocol ─────────────────────────────────────────────────────────
426
 
427
- def gemini_stream_generate(prompt: str, model_info: dict) -> str:
428
  """Send prompt to Gemini StreamGenerate with retry.
429
 
430
  Uses the x-goog-ext-525001261-jspb header for model selection
@@ -497,7 +552,10 @@ def gemini_stream_generate(prompt: str, model_info: dict) -> str:
497
  time.sleep(CONFIG["retry_delay_sec"])
498
  apply_jitter()
499
 
500
- return client.post(url, data=body, headers=headers, cookies=cookies)
 
 
 
501
  except Exception as e:
502
  last_err = e
503
  if attempt < CONFIG["retry_attempts"] - 1:
@@ -509,8 +567,8 @@ def clean_gemini_text(text: str) -> str:
509
  """Remove internal code execution artifacts and image placeholders."""
510
  # Convert code execution blocks to standard markdown
511
  text = re.sub(
512
- r'```(python|javascript|text)\?code_(?:reference|stdout)&code_event_index=\d+',
513
- r'```\1', text
514
  )
515
  # Remove googleusercontent placeholder URLs (image gen/retrieval/collection)
516
  text = re.sub(
@@ -571,47 +629,64 @@ def _scan_complete_wrb_frames(buf: str) -> list:
571
  return frames
572
 
573
 
574
- def extract_response_text(raw: str, model_info: dict = None) -> str:
575
- """Parse StreamGenerate response to extract final text.
576
-
577
- Uses the upstream's bracket-depth frame scanner for robustness.
578
- """
579
- frames = _scan_complete_wrb_frames(raw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
 
581
- texts = []
582
- for elem in frames:
583
- try:
584
- if not isinstance(elem, list) or len(elem) < 3 or elem[0] != "wrb.fr":
585
- continue
586
- rp = elem[2]
587
- if not isinstance(rp, str) or len(rp) < 50:
588
- continue
589
- payload = json.loads(rp)
590
 
591
- # Extract final text
592
- if isinstance(payload, list) and len(payload) > 4 and payload[4]:
593
- for part in payload[4]:
594
- if isinstance(part, list) and len(part) > 1 and part[1]:
595
- if isinstance(part[1], list):
596
- for t in part[1]:
597
- if isinstance(t, str) and len(t) > 0:
598
- texts.append(t)
599
- except (json.JSONDecodeError, IndexError, TypeError):
600
- pass
601
-
602
- # Take the last non-empty text (final/most complete response)
603
- text = ""
604
- for t in reversed(texts):
605
- if t.strip():
606
- text = t
607
- break
608
 
609
- final_response = clean_gemini_text(text)
610
-
611
- if model_info and model_info.get("think") == 0:
612
- final_response = f"<think>\nthink_mode: 0\nwe do not really recieve thinking block from gemini.\n</think>\n\n{final_response}"
613
-
614
- return final_response
 
 
 
 
 
615
 
616
 
617
  # ─── OpenAI Format Helpers ───────────────────────────────────────────────────
@@ -794,17 +869,22 @@ class GeminiHandler(BaseHTTPRequestHandler):
794
  return None, None, err
795
  return pub_name, model_info, None
796
 
797
- def _call_gemini(self, prompt, model_info, tools):
798
- raw = gemini_stream_generate(prompt, model_info)
799
- text = extract_response_text(raw, model_info)
800
- tool_calls = None
801
- if tools and text:
802
- text, tool_calls = parse_tool_calls(text)
803
- return text or "", tool_calls
 
 
 
804
 
805
  def handle_chat(self, body: bytes):
806
  try:
807
  req = json.loads(body)
 
 
808
  except json.JSONDecodeError as e:
809
  self.send_json({"error": {"message": f"Invalid JSON payload: {e}. Body received: {body.decode('utf-8', errors='replace')}"}}, 400)
810
  return
@@ -815,47 +895,102 @@ class GeminiHandler(BaseHTTPRequestHandler):
815
  return
816
 
817
  tools = req.get("tools")
 
 
 
 
 
 
818
  prompt = messages_to_prompt(req.get("messages", []), tools)
819
  if not prompt.strip():
820
  self.send_json({"error": {"message": "empty prompt"}}, 400)
821
  return
822
 
 
 
823
  try:
824
- text, tool_calls = self._call_gemini(prompt, model_info, tools)
 
 
 
 
 
 
 
 
 
825
  except Exception as e:
826
  self.send_json({"error": {"message": f"upstream error: {e}"}}, 502)
827
  return
828
 
829
  cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
830
- msg = {"role": "assistant", "content": text or None}
831
- if tool_calls:
832
- msg["tool_calls"] = tool_calls
833
- finish = "tool_calls" if tool_calls else "stop"
834
-
835
- if req.get("stream"):
836
  self.send_response(200)
837
  self.send_header("Content-Type", "text/event-stream")
838
  self.send_header("Cache-Control", "no-cache")
839
  self.send_header("Access-Control-Allow-Origin", "*")
840
  self.end_headers()
841
- chunk = {"id": cid, "object": "chat.completion.chunk", "created": int(time.time()),
842
- "model": model_name, "choices": [{"index": 0, "delta": msg, "finish_reason": finish}]}
843
- self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  self.wfile.write(b"data: [DONE]\n\n")
845
  self.wfile.flush()
846
  else:
 
 
 
 
 
 
 
 
 
847
  self.send_json({
848
  "id": cid, "object": "chat.completion", "created": int(time.time()),
849
  "model": model_name,
850
  "choices": [{"index": 0, "message": msg, "finish_reason": finish}],
851
- "usage": {"prompt_tokens": len(prompt)//4, "completion_tokens": len(text)//4,
852
- "total_tokens": (len(prompt)+len(text))//4},
853
  })
854
 
855
  def handle_responses(self, body: bytes):
856
  """OpenAI Responses API for Codex CLI compatibility."""
857
  try:
858
  req = json.loads(body)
 
 
859
  except json.JSONDecodeError as e:
860
  self.send_json({"error": {"message": f"Invalid JSON payload: {e}. Body received: {body.decode('utf-8', errors='replace')}"}}, 400)
861
  return
@@ -867,6 +1002,12 @@ class GeminiHandler(BaseHTTPRequestHandler):
867
 
868
  input_items = req.get("input", [])
869
  tools = req.get("tools")
 
 
 
 
 
 
870
 
871
  messages = []
872
  if req.get("instructions"):
@@ -915,6 +1056,9 @@ class GeminiHandler(BaseHTTPRequestHandler):
915
 
916
  try:
917
  text, tool_calls = self._call_gemini(prompt, model_info, tools)
 
 
 
918
  except Exception as e:
919
  self.send_json({"error": {"message": f"upstream error: {e}"}}, 502)
920
  return
@@ -993,6 +1137,7 @@ def main():
993
  parser.add_argument("--config", type=str, default=None)
994
  parser.add_argument("--cookie-file", type=str, default=None, help="Path to cookie file")
995
  parser.add_argument("--proxy", type=str, default=None, help="HTTP proxy, e.g. http://127.0.0.1:7890")
 
996
  parser.add_argument("--version", action="version", version=f"gemini-web2api {__version__}")
997
  args = parser.parse_args()
998
 
@@ -1010,6 +1155,8 @@ def main():
1010
  CONFIG["cookie_file"] = args.cookie_file
1011
  if args.proxy:
1012
  CONFIG["proxy"] = args.proxy
 
 
1013
 
1014
  # Initialize HTTP client
1015
  get_http_client()
@@ -1032,13 +1179,14 @@ def main():
1032
  print(f" Proxy: {CONFIG.get('proxy') or 'none (uses system env HTTP_PROXY/HTTPS_PROXY)'}")
1033
  print(f" Retry: {CONFIG['retry_attempts']}x / {CONFIG['retry_delay_sec']}s")
1034
  print(f" Jitter: {CONFIG['jitter_min_ms']}-{CONFIG['jitter_max_ms']}ms")
 
1035
  print()
1036
  try:
1037
  server.serve_forever()
1038
  except KeyboardInterrupt:
1039
  print("\nStopped.")
1040
  get_http_client().close()
1041
- server.shutdown()
1042
 
1043
 
1044
  if __name__ == "__main__":
 
62
  # Request jitter (ms) - randomized delays to mimic human behavior
63
  "jitter_min_ms": 50,
64
  "jitter_max_ms": 300,
65
+ "debug_mode": False,
66
  }
67
 
68
  CONFIG = dict(DEFAULT_CONFIG)
 
404
 
405
  return resp.read().decode("utf-8", errors="replace")
406
 
407
+ def post_stream(self, url: str, data: bytes, headers: dict, cookies: dict = None):
408
+ """POST request that yields streaming chunks."""
409
+ if self._session:
410
+ return self._post_curl_stream(url, data, headers, cookies)
411
+ else:
412
+ return self._post_urllib_stream(url, data, headers, cookies)
413
+
414
+ def _post_curl_stream(self, url: str, data: bytes, headers: dict, cookies: dict = None):
415
+ self._session.cookies.clear()
416
+ proxy = CONFIG.get("proxy")
417
+ proxies = {"http": proxy, "https": proxy} if proxy else None
418
+
419
+ resp = self._session.post(
420
+ url,
421
+ data=data,
422
+ headers=dict(headers),
423
+ cookies=cookies or {},
424
+ proxies=proxies,
425
+ allow_redirects=True,
426
+ stream=True
427
+ )
428
+ if resp.status_code != 200:
429
+ raise Exception(f"HTTP {resp.status_code}")
430
+
431
+ for line in resp.iter_lines():
432
+ if line:
433
+ yield line.decode("utf-8", errors="replace")
434
+
435
+ def _post_urllib_stream(self, url: str, data: bytes, headers: dict, cookies: dict = None):
436
+ all_headers = dict(headers)
437
+ if cookies:
438
+ cookie_str = "; ".join(f"{k}={v}" for k, v in cookies.items())
439
+ existing = all_headers.get("Cookie", "")
440
+ if existing:
441
+ all_headers["Cookie"] = existing + "; " + cookie_str
442
+ else:
443
+ all_headers["Cookie"] = cookie_str
444
+
445
+ req = urllib.request.Request(url, data=data, headers=all_headers, method="POST")
446
+ ctx = ssl.create_default_context()
447
+ proxy = CONFIG.get("proxy")
448
+ if proxy:
449
+ opener = urllib.request.build_opener(
450
+ urllib.request.ProxyHandler({"http": proxy, "https": proxy}),
451
+ urllib.request.HTTPSHandler(context=ctx)
452
+ )
453
+ resp = opener.open(req, timeout=CONFIG["request_timeout_sec"])
454
+ else:
455
+ resp = urllib.request.urlopen(req, context=ctx, timeout=CONFIG["request_timeout_sec"])
456
+
457
+ for line in resp:
458
+ if line:
459
+ yield line.decode("utf-8", errors="replace")
460
+
461
  def close(self):
462
  if self._session:
463
  try:
 
479
 
480
  # ─── Gemini Protocol ─────────────────────────────────────────────────────────
481
 
482
+ def gemini_stream_generate(prompt: str, model_info: dict, stream: bool = False):
483
  """Send prompt to Gemini StreamGenerate with retry.
484
 
485
  Uses the x-goog-ext-525001261-jspb header for model selection
 
552
  time.sleep(CONFIG["retry_delay_sec"])
553
  apply_jitter()
554
 
555
+ if stream:
556
+ return client.post_stream(url, data=body, headers=headers, cookies=cookies)
557
+ else:
558
+ return client.post(url, data=body, headers=headers, cookies=cookies)
559
  except Exception as e:
560
  last_err = e
561
  if attempt < CONFIG["retry_attempts"] - 1:
 
567
  """Remove internal code execution artifacts and image placeholders."""
568
  # Convert code execution blocks to standard markdown
569
  text = re.sub(
570
+ r'\?code_(?:reference|stdout)&code_event_index=\d+',
571
+ '', text
572
  )
573
  # Remove googleusercontent placeholder URLs (image gen/retrieval/collection)
574
  text = re.sub(
 
629
  return frames
630
 
631
 
632
+ def gemini_stream_parse(stream_generator, model_info: dict = None):
633
+ """Consume network chunks, parse wrb.fr frames, and yield text deltas incrementally."""
634
+ buf = ""
635
+ emitted_raw = ""
636
+ first_chunk = True
637
+
638
+ for chunk in stream_generator:
639
+ if not chunk: continue
640
+ buf += chunk
641
+
642
+ frames = _scan_complete_wrb_frames(buf)
643
+ if not frames: continue
644
+
645
+ # In stream context, the parser needs to extract the latest text
646
+ texts = []
647
+ for elem in frames:
648
+ try:
649
+ if not isinstance(elem, list) or len(elem) < 3 or elem[0] != "wrb.fr":
650
+ continue
651
+ rp = elem[2]
652
+ if not isinstance(rp, str) or len(rp) < 50:
653
+ continue
654
+ payload = json.loads(rp)
655
+
656
+ if isinstance(payload, list) and len(payload) > 4 and payload[4]:
657
+ for part in payload[4]:
658
+ if isinstance(part, list) and len(part) > 1 and part[1]:
659
+ if isinstance(part[1], list):
660
+ for t in part[1]:
661
+ if isinstance(t, str) and len(t) > 0:
662
+ texts.append(t)
663
+ except (json.JSONDecodeError, IndexError, TypeError):
664
+ pass
665
 
666
+ current_full_text = ""
667
+ for t in reversed(texts):
668
+ if t.strip():
669
+ current_full_text = t
670
+ break
671
+
672
+ if current_full_text == emitted_raw:
673
+ continue
 
674
 
675
+ if current_full_text.startswith(emitted_raw):
676
+ raw_delta = current_full_text[len(emitted_raw):]
677
+ emitted_raw = current_full_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
 
679
+ if raw_delta:
680
+ cleaned_delta = clean_gemini_text(raw_delta)
681
+ first_chunk = False
682
+
683
+ if cleaned_delta:
684
+ yield cleaned_delta
685
+
686
+ def extract_response_text(raw: str, model_info: dict = None) -> str:
687
+ """Parse StreamGenerate response to extract final text. (Backwards compatible)"""
688
+ gen = gemini_stream_parse([raw], model_info)
689
+ return "".join(list(gen))
690
 
691
 
692
  # ─── OpenAI Format Helpers ───────────────────────────────────────────────────
 
869
  return None, None, err
870
  return pub_name, model_info, None
871
 
872
+ def _call_gemini(self, prompt, model_info, tools, stream=False):
873
+ raw = gemini_stream_generate(prompt, model_info, stream=stream)
874
+ if stream:
875
+ return gemini_stream_parse(raw, model_info)
876
+ else:
877
+ text = extract_response_text(raw, model_info)
878
+ tool_calls = None
879
+ if tools and text:
880
+ text, tool_calls = parse_tool_calls(text)
881
+ return text or "", tool_calls
882
 
883
  def handle_chat(self, body: bytes):
884
  try:
885
  req = json.loads(body)
886
+ if CONFIG.get("debug_mode"):
887
+ log(f"DEBUG [CHAT] REQUEST: {json.dumps(req, ensure_ascii=False)[:2000]}")
888
  except json.JSONDecodeError as e:
889
  self.send_json({"error": {"message": f"Invalid JSON payload: {e}. Body received: {body.decode('utf-8', errors='replace')}"}}, 400)
890
  return
 
895
  return
896
 
897
  tools = req.get("tools")
898
+ if CONFIG.get("debug_mode"):
899
+ think_status = "Enabled" if model_info.get("think") == 0 else "Disabled"
900
+ log(f"DEBUG [CHAT] MODEL: {model_name} (Think Mode: {think_status})")
901
+ if tools:
902
+ log(f"DEBUG [CHAT] TOOLS PROVIDED: {len(tools)} tools")
903
+
904
  prompt = messages_to_prompt(req.get("messages", []), tools)
905
  if not prompt.strip():
906
  self.send_json({"error": {"message": "empty prompt"}}, 400)
907
  return
908
 
909
+ is_stream = bool(req.get("stream"))
910
+
911
  try:
912
+ # If tools are provided, we must collect full text first to parse them, so disable network streaming
913
+ if tools:
914
+ text, tool_calls = self._call_gemini(prompt, model_info, tools, stream=False)
915
+ if CONFIG.get("debug_mode"):
916
+ log(f"DEBUG [CHAT] RESPONSE TEXT: {text}")
917
+ log(f"DEBUG [CHAT] RESPONSE TOOLS: {tool_calls}")
918
+ else:
919
+ result = self._call_gemini(prompt, model_info, tools, stream=is_stream)
920
+ if not is_stream and CONFIG.get("debug_mode"):
921
+ log(f"DEBUG [CHAT] RESPONSE: {result}")
922
  except Exception as e:
923
  self.send_json({"error": {"message": f"upstream error: {e}"}}, 502)
924
  return
925
 
926
  cid = f"chatcmpl-{uuid.uuid4().hex[:12]}"
927
+
928
+ if is_stream:
 
 
 
 
929
  self.send_response(200)
930
  self.send_header("Content-Type", "text/event-stream")
931
  self.send_header("Cache-Control", "no-cache")
932
  self.send_header("Access-Control-Allow-Origin", "*")
933
  self.end_headers()
934
+
935
+ if tools:
936
+ # Tools were present, so we ran synchronously. We yield the tool calls in streaming format.
937
+ if tool_calls:
938
+ for tc in tool_calls:
939
+ chunk = {"id": cid, "object": "chat.completion.chunk", "created": int(time.time()),
940
+ "model": model_name, "choices": [{"index": 0, "delta": {"tool_calls": [tc]}}]}
941
+ self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
942
+ chunk = {"id": cid, "object": "chat.completion.chunk", "created": int(time.time()),
943
+ "model": model_name, "choices": [{"index": 0, "delta": {}, "finish_reason": "tool_calls"}]}
944
+ self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
945
+ else:
946
+ msg = {"role": "assistant", "content": text or ""}
947
+ chunk = {"id": cid, "object": "chat.completion.chunk", "created": int(time.time()),
948
+ "model": model_name, "choices": [{"index": 0, "delta": msg, "finish_reason": "stop"}]}
949
+ self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
950
+ else:
951
+ # Real streaming
952
+ first = True
953
+ for delta in result:
954
+ if CONFIG.get("debug_mode") and delta:
955
+ log(f"DEBUG [CHAT] CHUNK: {delta}")
956
+ msg = {"role": "assistant"} if first else {}
957
+ if delta: msg["content"] = delta
958
+ first = False
959
+ chunk = {"id": cid, "object": "chat.completion.chunk", "created": int(time.time()),
960
+ "model": model_name, "choices": [{"index": 0, "delta": msg, "finish_reason": None}]}
961
+ self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
962
+ self.wfile.flush()
963
+
964
+ chunk = {"id": cid, "object": "chat.completion.chunk", "created": int(time.time()),
965
+ "model": model_name, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}
966
+ self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
967
+
968
  self.wfile.write(b"data: [DONE]\n\n")
969
  self.wfile.flush()
970
  else:
971
+ if tools:
972
+ msg = {"role": "assistant", "content": text or None}
973
+ if tool_calls:
974
+ msg["tool_calls"] = tool_calls
975
+ finish = "tool_calls" if tool_calls else "stop"
976
+ else:
977
+ msg = {"role": "assistant", "content": result or None}
978
+ finish = "stop"
979
+
980
  self.send_json({
981
  "id": cid, "object": "chat.completion", "created": int(time.time()),
982
  "model": model_name,
983
  "choices": [{"index": 0, "message": msg, "finish_reason": finish}],
984
+ "usage": {"prompt_tokens": len(prompt)//4, "completion_tokens": 0,
985
+ "total_tokens": len(prompt)//4},
986
  })
987
 
988
  def handle_responses(self, body: bytes):
989
  """OpenAI Responses API for Codex CLI compatibility."""
990
  try:
991
  req = json.loads(body)
992
+ if CONFIG.get("debug_mode"):
993
+ log(f"DEBUG [RESP] REQUEST: {json.dumps(req, ensure_ascii=False)[:2000]}")
994
  except json.JSONDecodeError as e:
995
  self.send_json({"error": {"message": f"Invalid JSON payload: {e}. Body received: {body.decode('utf-8', errors='replace')}"}}, 400)
996
  return
 
1002
 
1003
  input_items = req.get("input", [])
1004
  tools = req.get("tools")
1005
+
1006
+ if CONFIG.get("debug_mode"):
1007
+ think_status = "Enabled" if model_info.get("think") == 0 else "Disabled"
1008
+ log(f"DEBUG [RESP] MODEL: {model_name} (Think Mode: {think_status})")
1009
+ if tools:
1010
+ log(f"DEBUG [RESP] TOOLS PROVIDED: {len(tools)} tools")
1011
 
1012
  messages = []
1013
  if req.get("instructions"):
 
1056
 
1057
  try:
1058
  text, tool_calls = self._call_gemini(prompt, model_info, tools)
1059
+ if CONFIG.get("debug_mode"):
1060
+ log(f"DEBUG [RESP] RESPONSE TEXT: {text}")
1061
+ log(f"DEBUG [RESP] RESPONSE TOOLS: {tool_calls}")
1062
  except Exception as e:
1063
  self.send_json({"error": {"message": f"upstream error: {e}"}}, 502)
1064
  return
 
1137
  parser.add_argument("--config", type=str, default=None)
1138
  parser.add_argument("--cookie-file", type=str, default=None, help="Path to cookie file")
1139
  parser.add_argument("--proxy", type=str, default=None, help="HTTP proxy, e.g. http://127.0.0.1:7890")
1140
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging of requests/responses")
1141
  parser.add_argument("--version", action="version", version=f"gemini-web2api {__version__}")
1142
  args = parser.parse_args()
1143
 
 
1155
  CONFIG["cookie_file"] = args.cookie_file
1156
  if args.proxy:
1157
  CONFIG["proxy"] = args.proxy
1158
+ if args.debug:
1159
+ CONFIG["debug_mode"] = True
1160
 
1161
  # Initialize HTTP client
1162
  get_http_client()
 
1179
  print(f" Proxy: {CONFIG.get('proxy') or 'none (uses system env HTTP_PROXY/HTTPS_PROXY)'}")
1180
  print(f" Retry: {CONFIG['retry_attempts']}x / {CONFIG['retry_delay_sec']}s")
1181
  print(f" Jitter: {CONFIG['jitter_min_ms']}-{CONFIG['jitter_max_ms']}ms")
1182
+ print(f" Debug: {'enabled' if CONFIG.get('debug_mode') else 'disabled'}")
1183
  print()
1184
  try:
1185
  server.serve_forever()
1186
  except KeyboardInterrupt:
1187
  print("\nStopped.")
1188
  get_http_client().close()
1189
+ server.server_close()
1190
 
1191
 
1192
  if __name__ == "__main__":