Skip to content
This repository was archived by the owner on Jun 12, 2024. It is now read-only.

Commit 163a5fd

Browse files
committed
build: 2.0.0 for functions
1 parent cd9997e commit 163a5fd

File tree

7 files changed

+124
-70
lines changed

7 files changed

+124
-70
lines changed

gemini/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Copyright 2024 Daniel Park, Antonio Cheang, MIT License
21
from os import environ
32
from .async_core import GeminiClient
43
from .core import Gemini
@@ -11,7 +10,7 @@
1110

1211
gemini_api_key = environ.get("GEMINI_COOKIES")
1312

14-
__version__ = "1.1.2"
13+
__version__ = "2.0.0"
1514
__author__ = (
1615
"daniel park <[email protected]>, antonio cheang <[email protected]>"
1716
)

gemini/src/async_core.py renamed to gemini/async_core.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Copyright 2024 Daniel Park, Antonio Cheang, MIT License
21
import os
32
import re
43
import json

gemini/core.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Copyright 2024 Daniel Park, Antonio Cheang, MIT License
21
import os
32
import re
43
import json
@@ -7,12 +6,13 @@
76
import inspect
87
import requests
98
import urllib.parse
10-
from typing import Optional, Tuple, Dict
9+
from typing import Optional, Tuple, Dict, Union
1110
from requests.exceptions import ConnectionError, RequestException
1211

1312
from .src.parser.custom_parser import ParseMethod1, ParseMethod2
1413
from .src.parser.response_parser import ResponseParser
1514
from .src.model.output import GeminiCandidate, GeminiModelOutput
15+
from .src.misc.utils import upload_image
1616
from .src.misc.constants import (
1717
HEADERS,
1818
HOST,
@@ -52,12 +52,16 @@ def __init__(
5252
auto_cookies: bool = False,
5353
timeout: int = 30,
5454
proxies: Optional[dict] = None,
55+
rcid: str = None,
5556
) -> None:
5657
"""
5758
Initializes the Gemini object with session, cookies, and other configurations.
5859
"""
5960
self._nonce = None
6061
self._sid = None
62+
self._rcid = rcid or None
63+
self._rid = None
64+
self._cid = None
6165
self.auto_cookies = auto_cookies
6266
self.cookie_fp = cookie_fp
6367
self.cookies = cookies
@@ -69,6 +73,14 @@ def __init__(
6973
self._reqid = int("".join(random.choices(string.digits, k=7)))
7074
self.parser = ResponseParser(cookies=self.cookies)
7175

76+
@property
77+
def rcid(self):
78+
return self._rcid
79+
80+
@rcid.setter
81+
def rcid(self, value):
82+
self._rcid = value
83+
7284
def _initialize_session(
7385
self,
7486
) -> requests.Session:
@@ -87,15 +99,15 @@ def _initialize_session(
8799
if self.cookies:
88100
session.cookies.update(self.cookies)
89101
elif self.cookie_fp:
90-
self._load_cookies_from_file(self.cookie_fp)
102+
self._set_cookies_from_file(self.cookie_fp)
91103
elif self.auto_cookies == True:
92104
self._set_cookies_automatically()
93105

94106
self._set_sid_and_nonce()
95107

96108
return session
97109

98-
def _load_cookies_from_file(self, file_path: str) -> None:
110+
def _set_cookies_from_file(self, file_path: str) -> None:
99111
"""Loads cookies from a file and updates the session."""
100112
try:
101113
if file_path.endswith(".json"):
@@ -120,15 +132,17 @@ def _set_sid_and_nonce(self):
120132
response = requests.get(f"{HOST}/app", cookies=self.cookies)
121133
response.raise_for_status()
122134

123-
sid_match, nonce_match = self.extract_sid_nonce(response.text)
135+
sid_match = re.search(r'"FdrFJe":"([\d-]+)"', response.text)
136+
nonce_match = re.search(r'"SNlM0e":"(.*?)"', response.text)
124137

125-
if sid_match and nonce_match:
138+
if sid_match:
126139
self._sid = sid_match.group(1)
127-
self._nonce = nonce_match.group(1)
128140
else:
129141
raise ValueError(
130142
"Failed to parse SID or SNlM0e nonce from the response.\nRefresh the Gemini web page or access Gemini in a new incognito browser to resend cookies."
131143
)
144+
if nonce_match:
145+
self._nonce = nonce_match.group(1)
132146

133147
except requests.RequestException as e:
134148
raise ConnectionError(f"Request failed: {e}")
@@ -137,12 +151,6 @@ def _set_sid_and_nonce(self):
137151
except Exception as e:
138152
raise RuntimeError(f"An unexpected error occurred: {e}")
139153

140-
@staticmethod
141-
def extract_sid_nonce(response_text):
142-
sid_match = re.search(r'"FdrFJe":"([\d-]+)"', response_text)
143-
nonce_match = re.search(r'"SNlM0e":"(.*?)"', response_text)
144-
return sid_match, nonce_match
145-
146154
def _construct_params(self, sid: str) -> str:
147155
"""
148156
Constructs URL-encoded parameters for a request.
@@ -163,7 +171,9 @@ def _construct_params(self, sid: str) -> str:
163171
}
164172
)
165173

166-
def _construct_payload(self, prompt: str, nonce: str) -> str:
174+
def _construct_payload(
175+
self, prompt: str, image: Union[bytes, str], nonce: str
176+
) -> str:
167177
"""
168178
Constructs URL-encoded payload for a request.
169179
@@ -177,15 +187,35 @@ def _construct_payload(self, prompt: str, nonce: str) -> str:
177187
return urllib.parse.urlencode(
178188
{
179189
"at": nonce,
180-
"f.req": json.dumps([None, json.dumps([[prompt], None, None])]),
181-
}
190+
"f.req": json.dumps(
191+
[
192+
None,
193+
json.dumps(
194+
[
195+
image
196+
and [
197+
prompt,
198+
0,
199+
None,
200+
[[[upload_image(image), 1]]],
201+
]
202+
or [prompt],
203+
None,
204+
[self._cid, self._rid, self._rcid],
205+
]
206+
),
207+
]
208+
),
209+
},
182210
)
183211

184-
def send_request(self, prompt: str) -> Tuple[str, int]:
212+
def send_request(
213+
self, prompt: str, image: Union[bytes, str] = None
214+
) -> Tuple[str, int]:
185215
"""Sends a request and returns the response text and status code."""
186216
try:
187217
params = self._construct_params(self._sid)
188-
data = self._construct_payload(prompt, self._nonce)
218+
data = self._construct_payload(prompt, image, self._nonce)
189219
response = self.session.post(
190220
POST_ENDPOINT,
191221
params=params,
@@ -200,7 +230,7 @@ def send_request(self, prompt: str) -> Tuple[str, int]:
200230
# self._update_cookies_from_browser()
201231
self._set_sid_and_nonce()
202232
params = self._construct_params(self._sid)
203-
data = self._construct_payload(prompt, self._nonce)
233+
data = self._construct_payload(prompt, image, self._nonce)
204234
response = self.session.post(
205235
POST_ENDPOINT,
206236
params=params,
@@ -217,9 +247,11 @@ def send_request(self, prompt: str) -> Tuple[str, int]:
217247
response.raise_for_status()
218248
return response.text, response.status_code
219249

220-
def generate_content(self, prompt: str) -> GeminiModelOutput:
250+
def generate_content(
251+
self, prompt: str, image: Union[bytes, str] = None
252+
) -> GeminiModelOutput:
221253
"""Generates content based on the prompt and returns a GeminiModelOutput object."""
222-
response_text, response_status_code = self.send_request(prompt)
254+
response_text, response_status_code = self.send_request(prompt, image)
223255
if response_status_code != 200:
224256
raise ValueError(f"Response status: {response_status_code}")
225257
else:
@@ -231,7 +263,12 @@ def generate_content(self, prompt: str) -> GeminiModelOutput:
231263
def _create_model_output(self, parsed_response: dict) -> GeminiModelOutput:
232264
candidates = self.collect_candidates(parsed_response)
233265
metadata = parsed_response.get("metadata", [])
234-
266+
try:
267+
self._cid = metadata[0]
268+
self._rid = metadata[1]
269+
# self._rcid = candidates["candidates"][0]["rcid"]
270+
except:
271+
pass
235272
return GeminiModelOutput(
236273
metadata=metadata,
237274
candidates=candidates,

gemini/src/misc/utils.py

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Copyright 2024 Daniel Park, Antonio Cheang, MIT License
22
import json
33
import requests
4-
from .constants import IMG_UPLOAD_HEADERS, REPLIT_SUPPORT_PROGRAM_LANGUAGES
4+
from typing import Union
5+
from .constants import REPLIT_SUPPORT_PROGRAM_LANGUAGES
56

67

78
def extract_code(text: str, language: str) -> str:
@@ -51,36 +52,31 @@ def extract_code(text: str, language: str) -> str:
5152
return text
5253

5354

54-
def upload_image(image: bytes, filename: str = "Photo.jpg"):
55+
def upload_image(file: Union[bytes, str]) -> str:
5556
"""
5657
Upload image into bard bucket on Google API, do not need session.
5758
5859
Returns:
5960
str: relative URL of image.
6061
"""
61-
resp = requests.options("https://content-push.googleapis.com/upload/")
62-
resp.raise_for_status()
63-
size = len(image)
64-
65-
headers = IMG_UPLOAD_HEADERS
66-
headers["size"] = str(size)
67-
headers["x-goog-upload-command"] = "start"
68-
69-
data = f"File name: {filename}"
70-
resp = requests.post(
71-
"https://content-push.googleapis.com/upload/", headers=headers, data=data
62+
if isinstance(file, str):
63+
with open(file, "rb") as f:
64+
file_data = f.read()
65+
else:
66+
file_data = file
67+
68+
response = requests.post(
69+
url="https://content-push.googleapis.com/upload/",
70+
headers={
71+
"Push-ID": "feeds/mcudyrk2a4khkz",
72+
"Content-Type": "application/octet-stream",
73+
},
74+
data=file_data,
75+
allow_redirects=True,
7276
)
73-
resp.raise_for_status()
74-
upload_url = resp.headers["X-Goog-Upload-Url"]
75-
resp = requests.options(upload_url, headers=headers)
76-
resp.raise_for_status()
77-
headers["x-goog-upload-command"] = "upload, finalize"
78-
79-
# It can be that we need to check returned offset
80-
headers["X-Goog-Upload-Offset"] = "0"
81-
resp = requests.post(upload_url, headers=headers, data=image)
82-
resp.raise_for_status()
83-
return resp.text
77+
response.raise_for_status()
78+
79+
return response.text
8480

8581

8682
def max_token(text: str, n: int) -> str:

gemini/src/model/output.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ class GeminiModelOutput(BaseModel):
2121
chosen: int = 0
2222
response_dict: Optional[dict] = None
2323

24+
def __setattr__(self, name, value):
25+
if name == "chosen":
26+
self.chosen = value
27+
else:
28+
super().__setattr__(name, value)
29+
2430
@property
2531
def rcid(self) -> str:
2632
"""The RCID (Response Choice ID) of the chosen candidate."""

gemini/src/parser/response_parser.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,65 @@ def parse(self, response_text):
1111

1212
def parse_response_text(self, response_text):
1313
body = self._extract_body(response_text)
14+
1415
if not body or not body[4]:
1516
raise ValueError(
1617
"Failed to parse response body. Data structure is invalid."
1718
)
1819

1920
candidates = self._parse_candidates(body[4])
21+
metadata = body[1]
22+
23+
if body[0] is None:
24+
prompt_class = None
25+
prompt_candidates = []
26+
else:
27+
prompt_class = body[0][0]
28+
prompt_candidates = body[0][1:] if len(body[0]) > 1 else []
29+
2030
if not candidates:
2131
raise ValueError(
2232
"Failed to generate contents. No output data found in response."
2333
)
2434

2535
return {
26-
"metadata": body[1],
36+
"metadata": metadata,
37+
"prompt_class": prompt_class,
38+
"prompt_candidates": prompt_candidates,
2739
"candidates": candidates,
2840
}
2941

3042
def _extract_body(self, response_text):
3143
try:
32-
body = json.loads(json.loads(max(response_text.split("\n"), key=len))[0][2])
44+
body = json.loads(
45+
json.loads(response_text.lstrip("')]}'\n\n").split("\n")[1])[0][2]
46+
)
3347
if not body[4]:
3448
body = json.loads(
35-
json.loads(max(response_text.split("\n"), key=len))[4][2]
49+
json.loads(response_text.lstrip("')]}'\n\n").split("\n")[1])[4][2]
3650
)
3751
return body
3852
except:
39-
body = json.loads(json.loads(response_text.text.split("\n")[2])[0][2])
40-
if not body[4]:
41-
body = json.loads(json.loads(response_text.text.split("\n")[2])[4][2])
42-
else:
43-
raise ValueError(
44-
"Failed to generate contents. Invalid response data received."
45-
)
46-
return body
53+
try:
54+
body = json.loads(json.loads(response_text.text.split("\n")[2])[0][2])
55+
if not body[4]:
56+
body = json.loads(
57+
json.loads(response_text.text.split("\n")[2])[4][2]
58+
)
59+
else:
60+
raise ValueError(
61+
"Failed to generate contents. Invalid response data received."
62+
)
63+
return body
64+
except:
65+
max_response = max(response_text.split("\n"), key=len)
66+
body = json.loads(json.loads(max_response)[0][2])
67+
if not body[4]:
68+
body = json.loads(json.loads(max_response)[4][2])
69+
return body
4770

4871
def _parse_candidates(self, candidates_data):
72+
candidates_list = []
4973
for candidate_data in candidates_data:
5074
web_images = self._parse_web_images(candidate_data[4])
5175
generated_images = self._parse_generated_images(candidate_data[12])
@@ -55,7 +79,8 @@ def _parse_candidates(self, candidates_data):
5579
"web_images": web_images,
5680
"generated_images": generated_images,
5781
}
58-
return candidate_dict
82+
candidates_list.append(candidate_dict)
83+
return candidates_list
5984

6085
def _parse_web_images(self, images_data):
6186
if not images_data:
@@ -75,7 +100,7 @@ def _parse_generated_images(self, images_data):
75100
return [
76101
{
77102
"url": image[0][3][3],
78-
"title": f"[Generated Image {image[3][6]}]",
103+
"title": f"[GeneratedImage {image[3][6]}]",
79104
"alt": image[3][5][i] if len(image[3][5]) > i else image[3][5][0],
80105
"cookies": self.cookies,
81106
}

0 commit comments

Comments
 (0)