Gemini.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. from __future__ import annotations
  2. import os
  3. import json
  4. import random
  5. import re
  6. import base64
  7. import asyncio
  8. from aiohttp import ClientSession, BaseConnector
  9. try:
  10. import nodriver
  11. has_nodriver = True
  12. except ImportError:
  13. has_nodriver = False
  14. from ... import debug
  15. from ...typing import Messages, Cookies, ImagesType, AsyncResult, AsyncIterator
  16. from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
  17. from ..helper import format_prompt, get_cookies
  18. from ...providers.response import JsonConversation, SynthesizeData, RequestLogin, ImageResponse
  19. from ...requests.raise_for_status import raise_for_status
  20. from ...requests.aiohttp import get_connector
  21. from ...requests import get_nodriver
  22. from ...errors import MissingAuthError
  23. from ...image import to_bytes
  24. from ..helper import get_last_user_message
  25. from ... import debug
  26. REQUEST_HEADERS = {
  27. "authority": "gemini.google.com",
  28. "origin": "https://gemini.google.com",
  29. "referer": "https://gemini.google.com/",
  30. 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36',
  31. 'x-same-domain': '1',
  32. }
  33. REQUEST_BL_PARAM = "boq_assistant-bard-web-server_20240519.16_p0"
  34. REQUEST_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
  35. UPLOAD_IMAGE_URL = "https://content-push.googleapis.com/upload/"
  36. UPLOAD_IMAGE_HEADERS = {
  37. "authority": "content-push.googleapis.com",
  38. "accept": "*/*",
  39. "accept-language": "en-US,en;q=0.7",
  40. "authorization": "Basic c2F2ZXM6cyNMdGhlNmxzd2F2b0RsN3J1d1U=",
  41. "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
  42. "origin": "https://gemini.google.com",
  43. "push-id": "feeds/mcudyrk2a4khkz",
  44. "referer": "https://gemini.google.com/",
  45. "x-goog-upload-command": "start",
  46. "x-goog-upload-header-content-length": "",
  47. "x-goog-upload-protocol": "resumable",
  48. "x-tenant-id": "bard-storage",
  49. }
  50. class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
  51. label = "Google Gemini"
  52. url = "https://gemini.google.com"
  53. needs_auth = True
  54. working = True
  55. use_nodriver = True
  56. default_model = 'gemini'
  57. default_image_model = default_model
  58. default_vision_model = default_model
  59. image_models = [default_image_model]
  60. models = [default_model, "gemini-1.5-flash", "gemini-1.5-pro"]
  61. synthesize_content_type = "audio/vnd.wav"
  62. _cookies: Cookies = None
  63. _snlm0e: str = None
  64. _sid: str = None
  65. @classmethod
  66. async def nodriver_login(cls, proxy: str = None) -> AsyncIterator[str]:
  67. if not has_nodriver:
  68. if debug.logging:
  69. print("Skip nodriver login in Gemini provider")
  70. return
  71. browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="gemini")
  72. try:
  73. login_url = os.environ.get("G4F_LOGIN_URL")
  74. if login_url:
  75. yield RequestLogin(cls.label, login_url)
  76. page = await browser.get(f"{cls.url}/app")
  77. await page.select("div.ql-editor.textarea", 240)
  78. cookies = {}
  79. for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])):
  80. cookies[c.name] = c.value
  81. await page.close()
  82. cls._cookies = cookies
  83. finally:
  84. stop_browser()
  85. @classmethod
  86. async def create_async_generator(
  87. cls,
  88. model: str,
  89. messages: Messages,
  90. proxy: str = None,
  91. cookies: Cookies = None,
  92. connector: BaseConnector = None,
  93. images: ImagesType = None,
  94. return_conversation: bool = False,
  95. conversation: Conversation = None,
  96. language: str = "en",
  97. **kwargs
  98. ) -> AsyncResult:
  99. prompt = format_prompt(messages) if conversation is None else get_last_user_message(messages)
  100. cls._cookies = cookies or cls._cookies or get_cookies(".google.com", False, True)
  101. base_connector = get_connector(connector, proxy)
  102. async with ClientSession(
  103. headers=REQUEST_HEADERS,
  104. connector=base_connector
  105. ) as session:
  106. if not cls._snlm0e:
  107. await cls.fetch_snlm0e(session, cls._cookies) if cls._cookies else None
  108. if not cls._snlm0e:
  109. try:
  110. async for chunk in cls.nodriver_login(proxy):
  111. yield chunk
  112. except Exception as e:
  113. raise MissingAuthError('Missing or invalid "__Secure-1PSID" cookie', e)
  114. if not cls._snlm0e:
  115. if cls._cookies is None or "__Secure-1PSID" not in cls._cookies:
  116. raise MissingAuthError('Missing "__Secure-1PSID" cookie')
  117. await cls.fetch_snlm0e(session, cls._cookies)
  118. if not cls._snlm0e:
  119. raise RuntimeError("Invalid cookies. SNlM0e not found")
  120. yield SynthesizeData(cls.__name__, {"text": messages[-1]["content"]})
  121. images = await cls.upload_images(base_connector, images) if images else None
  122. async with ClientSession(
  123. cookies=cls._cookies,
  124. headers=REQUEST_HEADERS,
  125. connector=base_connector,
  126. ) as client:
  127. params = {
  128. 'bl': REQUEST_BL_PARAM,
  129. 'hl': language,
  130. '_reqid': random.randint(1111, 9999),
  131. 'rt': 'c',
  132. "f.sid": cls._sid,
  133. }
  134. data = {
  135. 'at': cls._snlm0e,
  136. 'f.req': json.dumps([None, json.dumps(cls.build_request(
  137. prompt,
  138. language=language,
  139. conversation=conversation,
  140. images=images
  141. ))])
  142. }
  143. async with client.post(
  144. REQUEST_URL,
  145. data=data,
  146. params=params,
  147. ) as response:
  148. await raise_for_status(response)
  149. image_prompt = response_part = None
  150. last_content = ""
  151. async for line in response.content:
  152. try:
  153. try:
  154. line = json.loads(line)
  155. except ValueError:
  156. continue
  157. if not isinstance(line, list):
  158. continue
  159. if len(line[0]) < 3 or not line[0][2]:
  160. continue
  161. response_part = json.loads(line[0][2])
  162. if not response_part[4]:
  163. continue
  164. if return_conversation:
  165. yield Conversation(response_part[1][0], response_part[1][1], response_part[4][0][0])
  166. content = response_part[4][0][1][0]
  167. except (ValueError, KeyError, TypeError, IndexError) as e:
  168. debug.log(f"{cls.__name__}:{e.__class__.__name__}:{e}")
  169. continue
  170. match = re.search(r'\[Imagen of (.*?)\]', content)
  171. if match:
  172. image_prompt = match.group(1)
  173. content = content.replace(match.group(0), '')
  174. pattern = r"http://googleusercontent.com/image_generation_content/\d+"
  175. content = re.sub(pattern, "", content)
  176. if last_content and content.startswith(last_content):
  177. yield content[len(last_content):]
  178. else:
  179. yield content
  180. last_content = content
  181. if image_prompt:
  182. try:
  183. images = [image[0][3][3] for image in response_part[4][0][12][7][0]]
  184. image_prompt = image_prompt.replace("a fake image", "")
  185. yield ImageResponse(images, image_prompt, {"cookies": cls._cookies})
  186. except (TypeError, IndexError, KeyError):
  187. pass
  188. @classmethod
  189. async def synthesize(cls, params: dict, proxy: str = None) -> AsyncIterator[bytes]:
  190. if "text" not in params:
  191. raise ValueError("Missing parameter text")
  192. async with ClientSession(
  193. cookies=cls._cookies,
  194. headers=REQUEST_HEADERS,
  195. connector=get_connector(proxy=proxy),
  196. ) as session:
  197. if not cls._snlm0e:
  198. await cls.fetch_snlm0e(session, cls._cookies) if cls._cookies else None
  199. inner_data = json.dumps([None, params["text"], "en-US", None, 2])
  200. async with session.post(
  201. "https://gemini.google.com/_/BardChatUi/data/batchexecute",
  202. data={
  203. "f.req": json.dumps([[["XqA3Ic", inner_data, None, "generic"]]]),
  204. "at": cls._snlm0e,
  205. },
  206. params={
  207. "rpcids": "XqA3Ic",
  208. "source-path": "/app/2704fb4aafcca926",
  209. "bl": "boq_assistant-bard-web-server_20241119.00_p1",
  210. "f.sid": "" if cls._sid is None else cls._sid,
  211. "hl": "de",
  212. "_reqid": random.randint(1111, 9999),
  213. "rt": "c"
  214. },
  215. ) as response:
  216. await raise_for_status(response)
  217. iter_base64_response = iter_filter_base64(response.content.iter_chunked(1024))
  218. async for chunk in iter_base64_decode(iter_base64_response):
  219. yield chunk
  220. def build_request(
  221. prompt: str,
  222. language: str,
  223. conversation: Conversation = None,
  224. images: list[list[str, str]] = None,
  225. tools: list[list[str]] = []
  226. ) -> list:
  227. image_list = [[[image_url, 1], image_name] for image_url, image_name in images] if images else []
  228. return [
  229. [prompt, 0, None, image_list, None, None, 0],
  230. [language],
  231. [
  232. None if conversation is None else conversation.conversation_id,
  233. None if conversation is None else conversation.response_id,
  234. None if conversation is None else conversation.choice_id,
  235. None,
  236. None,
  237. []
  238. ],
  239. None,
  240. None,
  241. None,
  242. [1],
  243. 0,
  244. [],
  245. tools,
  246. 1,
  247. 0,
  248. ]
  249. async def upload_images(connector: BaseConnector, images: ImagesType) -> list:
  250. async def upload_image(image: bytes, image_name: str = None):
  251. async with ClientSession(
  252. headers=UPLOAD_IMAGE_HEADERS,
  253. connector=connector
  254. ) as session:
  255. image = to_bytes(image)
  256. async with session.options(UPLOAD_IMAGE_URL) as response:
  257. await raise_for_status(response)
  258. headers = {
  259. "size": str(len(image)),
  260. "x-goog-upload-command": "start"
  261. }
  262. data = f"File name: {image_name}" if image_name else None
  263. async with session.post(
  264. UPLOAD_IMAGE_URL, headers=headers, data=data
  265. ) as response:
  266. await raise_for_status(response)
  267. upload_url = response.headers["X-Goog-Upload-Url"]
  268. async with session.options(upload_url, headers=headers) as response:
  269. await raise_for_status(response)
  270. headers["x-goog-upload-command"] = "upload, finalize"
  271. headers["X-Goog-Upload-Offset"] = "0"
  272. async with session.post(
  273. upload_url, headers=headers, data=image
  274. ) as response:
  275. await raise_for_status(response)
  276. return [await response.text(), image_name]
  277. return await asyncio.gather(*[upload_image(image, image_name) for image, image_name in images])
  278. @classmethod
  279. async def fetch_snlm0e(cls, session: ClientSession, cookies: Cookies):
  280. async with session.get(cls.url, cookies=cookies) as response:
  281. await raise_for_status(response)
  282. response_text = await response.text()
  283. match = re.search(r'SNlM0e\":\"(.*?)\"', response_text)
  284. if match:
  285. cls._snlm0e = match.group(1)
  286. sid_match = re.search(r'"FdrFJe":"([\d-]+)"', response_text)
  287. if sid_match:
  288. cls._sid = sid_match.group(1)
  289. class Conversation(JsonConversation):
  290. def __init__(self,
  291. conversation_id: str,
  292. response_id: str,
  293. choice_id: str
  294. ) -> None:
  295. self.conversation_id = conversation_id
  296. self.response_id = response_id
  297. self.choice_id = choice_id
  298. async def iter_filter_base64(chunks: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
  299. search_for = b'[["wrb.fr","XqA3Ic","[\\"'
  300. end_with = b'\\'
  301. is_started = False
  302. async for chunk in chunks:
  303. if is_started:
  304. if end_with in chunk:
  305. yield chunk.split(end_with, maxsplit=1).pop(0)
  306. break
  307. else:
  308. yield chunk
  309. elif search_for in chunk:
  310. is_started = True
  311. yield chunk.split(search_for, maxsplit=1).pop()
  312. else:
  313. raise ValueError(f"Response: {chunk}")
  314. async def iter_base64_decode(chunks: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
  315. buffer = b""
  316. rest = 0
  317. async for chunk in chunks:
  318. chunk = buffer + chunk
  319. rest = len(chunk) % 4
  320. buffer = chunk[-rest:]
  321. yield base64.b64decode(chunk[:-rest])
  322. if rest > 0:
  323. yield base64.b64decode(buffer+rest*b"=")