webscout 8.2.2__py3-none-any.whl → 2026.1.19__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 (483) hide show
  1. webscout/AIauto.py +524 -143
  2. webscout/AIbase.py +247 -123
  3. webscout/AIutel.py +68 -132
  4. webscout/Bard.py +1072 -535
  5. webscout/Extra/GitToolkit/__init__.py +2 -2
  6. webscout/Extra/GitToolkit/gitapi/__init__.py +20 -12
  7. webscout/Extra/GitToolkit/gitapi/gist.py +142 -0
  8. webscout/Extra/GitToolkit/gitapi/organization.py +91 -0
  9. webscout/Extra/GitToolkit/gitapi/repository.py +308 -195
  10. webscout/Extra/GitToolkit/gitapi/search.py +162 -0
  11. webscout/Extra/GitToolkit/gitapi/trending.py +236 -0
  12. webscout/Extra/GitToolkit/gitapi/user.py +128 -96
  13. webscout/Extra/GitToolkit/gitapi/utils.py +82 -62
  14. webscout/Extra/YTToolkit/README.md +443 -0
  15. webscout/Extra/YTToolkit/YTdownloader.py +953 -957
  16. webscout/Extra/YTToolkit/__init__.py +3 -3
  17. webscout/Extra/YTToolkit/transcriber.py +595 -476
  18. webscout/Extra/YTToolkit/ytapi/README.md +230 -0
  19. webscout/Extra/YTToolkit/ytapi/__init__.py +22 -6
  20. webscout/Extra/YTToolkit/ytapi/captions.py +190 -0
  21. webscout/Extra/YTToolkit/ytapi/channel.py +302 -307
  22. webscout/Extra/YTToolkit/ytapi/errors.py +13 -13
  23. webscout/Extra/YTToolkit/ytapi/extras.py +178 -45
  24. webscout/Extra/YTToolkit/ytapi/hashtag.py +120 -0
  25. webscout/Extra/YTToolkit/ytapi/https.py +89 -88
  26. webscout/Extra/YTToolkit/ytapi/patterns.py +61 -61
  27. webscout/Extra/YTToolkit/ytapi/playlist.py +59 -59
  28. webscout/Extra/YTToolkit/ytapi/pool.py +8 -8
  29. webscout/Extra/YTToolkit/ytapi/query.py +143 -40
  30. webscout/Extra/YTToolkit/ytapi/shorts.py +122 -0
  31. webscout/Extra/YTToolkit/ytapi/stream.py +68 -63
  32. webscout/Extra/YTToolkit/ytapi/suggestions.py +97 -0
  33. webscout/Extra/YTToolkit/ytapi/utils.py +66 -62
  34. webscout/Extra/YTToolkit/ytapi/video.py +189 -18
  35. webscout/Extra/__init__.py +2 -3
  36. webscout/Extra/gguf.py +1298 -682
  37. webscout/Extra/tempmail/README.md +488 -0
  38. webscout/Extra/tempmail/__init__.py +28 -28
  39. webscout/Extra/tempmail/async_utils.py +143 -141
  40. webscout/Extra/tempmail/base.py +172 -161
  41. webscout/Extra/tempmail/cli.py +191 -187
  42. webscout/Extra/tempmail/emailnator.py +88 -84
  43. webscout/Extra/tempmail/mail_tm.py +378 -361
  44. webscout/Extra/tempmail/temp_mail_io.py +304 -292
  45. webscout/Extra/weather.py +196 -194
  46. webscout/Extra/weather_ascii.py +17 -15
  47. webscout/Provider/AISEARCH/PERPLEXED_search.py +175 -0
  48. webscout/Provider/AISEARCH/Perplexity.py +237 -304
  49. webscout/Provider/AISEARCH/README.md +106 -0
  50. webscout/Provider/AISEARCH/__init__.py +16 -10
  51. webscout/Provider/AISEARCH/brave_search.py +298 -0
  52. webscout/Provider/AISEARCH/iask_search.py +130 -209
  53. webscout/Provider/AISEARCH/monica_search.py +200 -246
  54. webscout/Provider/AISEARCH/webpilotai_search.py +242 -281
  55. webscout/Provider/Algion.py +413 -0
  56. webscout/Provider/Andi.py +74 -69
  57. webscout/Provider/Apriel.py +313 -0
  58. webscout/Provider/Ayle.py +323 -0
  59. webscout/Provider/ChatSandbox.py +329 -0
  60. webscout/Provider/ClaudeOnline.py +365 -0
  61. webscout/Provider/Cohere.py +232 -208
  62. webscout/Provider/DeepAI.py +367 -0
  63. webscout/Provider/Deepinfra.py +343 -173
  64. webscout/Provider/EssentialAI.py +217 -0
  65. webscout/Provider/ExaAI.py +274 -261
  66. webscout/Provider/Gemini.py +60 -54
  67. webscout/Provider/GithubChat.py +385 -367
  68. webscout/Provider/Gradient.py +286 -0
  69. webscout/Provider/Groq.py +556 -670
  70. webscout/Provider/HadadXYZ.py +323 -0
  71. webscout/Provider/HeckAI.py +392 -233
  72. webscout/Provider/HuggingFace.py +387 -0
  73. webscout/Provider/IBM.py +340 -0
  74. webscout/Provider/Jadve.py +317 -266
  75. webscout/Provider/K2Think.py +306 -0
  76. webscout/Provider/Koboldai.py +221 -381
  77. webscout/Provider/Netwrck.py +273 -228
  78. webscout/Provider/Nvidia.py +310 -0
  79. webscout/Provider/OPENAI/DeepAI.py +489 -0
  80. webscout/Provider/OPENAI/K2Think.py +423 -0
  81. webscout/Provider/OPENAI/PI.py +463 -0
  82. webscout/Provider/OPENAI/README.md +890 -0
  83. webscout/Provider/OPENAI/TogetherAI.py +405 -0
  84. webscout/Provider/OPENAI/TwoAI.py +255 -0
  85. webscout/Provider/OPENAI/__init__.py +148 -25
  86. webscout/Provider/OPENAI/ai4chat.py +348 -0
  87. webscout/Provider/OPENAI/akashgpt.py +436 -0
  88. webscout/Provider/OPENAI/algion.py +303 -0
  89. webscout/Provider/OPENAI/ayle.py +365 -0
  90. webscout/Provider/OPENAI/base.py +253 -46
  91. webscout/Provider/OPENAI/cerebras.py +296 -0
  92. webscout/Provider/OPENAI/chatgpt.py +514 -193
  93. webscout/Provider/OPENAI/chatsandbox.py +233 -0
  94. webscout/Provider/OPENAI/deepinfra.py +403 -272
  95. webscout/Provider/OPENAI/e2b.py +2370 -1350
  96. webscout/Provider/OPENAI/elmo.py +278 -0
  97. webscout/Provider/OPENAI/exaai.py +186 -138
  98. webscout/Provider/OPENAI/freeassist.py +446 -0
  99. webscout/Provider/OPENAI/gradient.py +448 -0
  100. webscout/Provider/OPENAI/groq.py +380 -0
  101. webscout/Provider/OPENAI/hadadxyz.py +292 -0
  102. webscout/Provider/OPENAI/heckai.py +100 -104
  103. webscout/Provider/OPENAI/huggingface.py +321 -0
  104. webscout/Provider/OPENAI/ibm.py +425 -0
  105. webscout/Provider/OPENAI/llmchat.py +253 -0
  106. webscout/Provider/OPENAI/llmchatco.py +378 -327
  107. webscout/Provider/OPENAI/meta.py +541 -0
  108. webscout/Provider/OPENAI/netwrck.py +110 -84
  109. webscout/Provider/OPENAI/nvidia.py +317 -0
  110. webscout/Provider/OPENAI/oivscode.py +348 -0
  111. webscout/Provider/OPENAI/openrouter.py +328 -0
  112. webscout/Provider/OPENAI/pydantic_imports.py +1 -0
  113. webscout/Provider/OPENAI/sambanova.py +397 -0
  114. webscout/Provider/OPENAI/sonus.py +126 -115
  115. webscout/Provider/OPENAI/textpollinations.py +218 -133
  116. webscout/Provider/OPENAI/toolbaz.py +136 -166
  117. webscout/Provider/OPENAI/typefully.py +419 -0
  118. webscout/Provider/OPENAI/typliai.py +279 -0
  119. webscout/Provider/OPENAI/utils.py +314 -211
  120. webscout/Provider/OPENAI/wisecat.py +103 -125
  121. webscout/Provider/OPENAI/writecream.py +185 -156
  122. webscout/Provider/OPENAI/x0gpt.py +227 -136
  123. webscout/Provider/OPENAI/zenmux.py +380 -0
  124. webscout/Provider/OpenRouter.py +386 -0
  125. webscout/Provider/Openai.py +337 -496
  126. webscout/Provider/PI.py +443 -344
  127. webscout/Provider/QwenLM.py +346 -254
  128. webscout/Provider/STT/__init__.py +28 -0
  129. webscout/Provider/STT/base.py +303 -0
  130. webscout/Provider/STT/elevenlabs.py +264 -0
  131. webscout/Provider/Sambanova.py +317 -0
  132. webscout/Provider/TTI/README.md +69 -0
  133. webscout/Provider/TTI/__init__.py +37 -12
  134. webscout/Provider/TTI/base.py +147 -0
  135. webscout/Provider/TTI/claudeonline.py +393 -0
  136. webscout/Provider/TTI/magicstudio.py +292 -0
  137. webscout/Provider/TTI/miragic.py +180 -0
  138. webscout/Provider/TTI/pollinations.py +331 -0
  139. webscout/Provider/TTI/together.py +334 -0
  140. webscout/Provider/TTI/utils.py +14 -0
  141. webscout/Provider/TTS/README.md +186 -0
  142. webscout/Provider/TTS/__init__.py +43 -7
  143. webscout/Provider/TTS/base.py +523 -0
  144. webscout/Provider/TTS/deepgram.py +286 -156
  145. webscout/Provider/TTS/elevenlabs.py +189 -111
  146. webscout/Provider/TTS/freetts.py +218 -0
  147. webscout/Provider/TTS/murfai.py +288 -113
  148. webscout/Provider/TTS/openai_fm.py +364 -0
  149. webscout/Provider/TTS/parler.py +203 -111
  150. webscout/Provider/TTS/qwen.py +334 -0
  151. webscout/Provider/TTS/sherpa.py +286 -0
  152. webscout/Provider/TTS/speechma.py +693 -180
  153. webscout/Provider/TTS/streamElements.py +275 -333
  154. webscout/Provider/TTS/utils.py +280 -280
  155. webscout/Provider/TextPollinationsAI.py +221 -121
  156. webscout/Provider/TogetherAI.py +450 -0
  157. webscout/Provider/TwoAI.py +309 -199
  158. webscout/Provider/TypliAI.py +311 -0
  159. webscout/Provider/UNFINISHED/ChatHub.py +219 -0
  160. webscout/Provider/{OPENAI/glider.py → UNFINISHED/ChutesAI.py} +160 -145
  161. webscout/Provider/UNFINISHED/GizAI.py +300 -0
  162. webscout/Provider/UNFINISHED/Marcus.py +218 -0
  163. webscout/Provider/UNFINISHED/Qodo.py +481 -0
  164. webscout/Provider/UNFINISHED/XenAI.py +330 -0
  165. webscout/Provider/{Youchat.py → UNFINISHED/Youchat.py} +64 -47
  166. webscout/Provider/UNFINISHED/aihumanizer.py +41 -0
  167. webscout/Provider/UNFINISHED/grammerchecker.py +37 -0
  168. webscout/Provider/UNFINISHED/liner.py +342 -0
  169. webscout/Provider/UNFINISHED/liner_api_request.py +246 -0
  170. webscout/Provider/UNFINISHED/samurai.py +231 -0
  171. webscout/Provider/WiseCat.py +256 -196
  172. webscout/Provider/WrDoChat.py +390 -0
  173. webscout/Provider/__init__.py +115 -198
  174. webscout/Provider/ai4chat.py +181 -202
  175. webscout/Provider/akashgpt.py +330 -342
  176. webscout/Provider/cerebras.py +397 -242
  177. webscout/Provider/cleeai.py +236 -213
  178. webscout/Provider/elmo.py +291 -234
  179. webscout/Provider/geminiapi.py +343 -208
  180. webscout/Provider/julius.py +245 -223
  181. webscout/Provider/learnfastai.py +333 -266
  182. webscout/Provider/llama3mitril.py +230 -180
  183. webscout/Provider/llmchat.py +308 -213
  184. webscout/Provider/llmchatco.py +321 -311
  185. webscout/Provider/meta.py +996 -794
  186. webscout/Provider/oivscode.py +332 -0
  187. webscout/Provider/searchchat.py +316 -293
  188. webscout/Provider/sonus.py +264 -208
  189. webscout/Provider/toolbaz.py +359 -320
  190. webscout/Provider/turboseek.py +332 -219
  191. webscout/Provider/typefully.py +262 -280
  192. webscout/Provider/x0gpt.py +332 -256
  193. webscout/__init__.py +31 -38
  194. webscout/__main__.py +5 -5
  195. webscout/cli.py +585 -293
  196. webscout/client.py +1497 -0
  197. webscout/conversation.py +140 -565
  198. webscout/exceptions.py +383 -339
  199. webscout/litagent/__init__.py +29 -29
  200. webscout/litagent/agent.py +492 -455
  201. webscout/litagent/constants.py +60 -60
  202. webscout/models.py +505 -181
  203. webscout/optimizers.py +32 -378
  204. webscout/prompt_manager.py +376 -274
  205. webscout/sanitize.py +1514 -0
  206. webscout/scout/README.md +452 -0
  207. webscout/scout/__init__.py +8 -8
  208. webscout/scout/core/__init__.py +7 -7
  209. webscout/scout/core/crawler.py +330 -140
  210. webscout/scout/core/scout.py +800 -568
  211. webscout/scout/core/search_result.py +51 -96
  212. webscout/scout/core/text_analyzer.py +64 -63
  213. webscout/scout/core/text_utils.py +412 -277
  214. webscout/scout/core/web_analyzer.py +54 -52
  215. webscout/scout/element.py +872 -460
  216. webscout/scout/parsers/__init__.py +70 -69
  217. webscout/scout/parsers/html5lib_parser.py +182 -172
  218. webscout/scout/parsers/html_parser.py +238 -236
  219. webscout/scout/parsers/lxml_parser.py +203 -178
  220. webscout/scout/utils.py +38 -37
  221. webscout/search/__init__.py +47 -0
  222. webscout/search/base.py +201 -0
  223. webscout/search/bing_main.py +45 -0
  224. webscout/search/brave_main.py +92 -0
  225. webscout/search/duckduckgo_main.py +57 -0
  226. webscout/search/engines/__init__.py +127 -0
  227. webscout/search/engines/bing/__init__.py +15 -0
  228. webscout/search/engines/bing/base.py +35 -0
  229. webscout/search/engines/bing/images.py +114 -0
  230. webscout/search/engines/bing/news.py +96 -0
  231. webscout/search/engines/bing/suggestions.py +36 -0
  232. webscout/search/engines/bing/text.py +109 -0
  233. webscout/search/engines/brave/__init__.py +19 -0
  234. webscout/search/engines/brave/base.py +47 -0
  235. webscout/search/engines/brave/images.py +213 -0
  236. webscout/search/engines/brave/news.py +353 -0
  237. webscout/search/engines/brave/suggestions.py +318 -0
  238. webscout/search/engines/brave/text.py +167 -0
  239. webscout/search/engines/brave/videos.py +364 -0
  240. webscout/search/engines/duckduckgo/__init__.py +25 -0
  241. webscout/search/engines/duckduckgo/answers.py +80 -0
  242. webscout/search/engines/duckduckgo/base.py +189 -0
  243. webscout/search/engines/duckduckgo/images.py +100 -0
  244. webscout/search/engines/duckduckgo/maps.py +183 -0
  245. webscout/search/engines/duckduckgo/news.py +70 -0
  246. webscout/search/engines/duckduckgo/suggestions.py +22 -0
  247. webscout/search/engines/duckduckgo/text.py +221 -0
  248. webscout/search/engines/duckduckgo/translate.py +48 -0
  249. webscout/search/engines/duckduckgo/videos.py +80 -0
  250. webscout/search/engines/duckduckgo/weather.py +84 -0
  251. webscout/search/engines/mojeek.py +61 -0
  252. webscout/search/engines/wikipedia.py +77 -0
  253. webscout/search/engines/yahoo/__init__.py +41 -0
  254. webscout/search/engines/yahoo/answers.py +19 -0
  255. webscout/search/engines/yahoo/base.py +34 -0
  256. webscout/search/engines/yahoo/images.py +323 -0
  257. webscout/search/engines/yahoo/maps.py +19 -0
  258. webscout/search/engines/yahoo/news.py +258 -0
  259. webscout/search/engines/yahoo/suggestions.py +140 -0
  260. webscout/search/engines/yahoo/text.py +273 -0
  261. webscout/search/engines/yahoo/translate.py +19 -0
  262. webscout/search/engines/yahoo/videos.py +302 -0
  263. webscout/search/engines/yahoo/weather.py +220 -0
  264. webscout/search/engines/yandex.py +67 -0
  265. webscout/search/engines/yep/__init__.py +13 -0
  266. webscout/search/engines/yep/base.py +34 -0
  267. webscout/search/engines/yep/images.py +101 -0
  268. webscout/search/engines/yep/suggestions.py +38 -0
  269. webscout/search/engines/yep/text.py +99 -0
  270. webscout/search/http_client.py +172 -0
  271. webscout/search/results.py +141 -0
  272. webscout/search/yahoo_main.py +57 -0
  273. webscout/search/yep_main.py +48 -0
  274. webscout/server/__init__.py +48 -0
  275. webscout/server/config.py +78 -0
  276. webscout/server/exceptions.py +69 -0
  277. webscout/server/providers.py +286 -0
  278. webscout/server/request_models.py +131 -0
  279. webscout/server/request_processing.py +404 -0
  280. webscout/server/routes.py +642 -0
  281. webscout/server/server.py +351 -0
  282. webscout/server/ui_templates.py +1171 -0
  283. webscout/swiftcli/__init__.py +79 -809
  284. webscout/swiftcli/core/__init__.py +7 -0
  285. webscout/swiftcli/core/cli.py +574 -0
  286. webscout/swiftcli/core/context.py +98 -0
  287. webscout/swiftcli/core/group.py +268 -0
  288. webscout/swiftcli/decorators/__init__.py +28 -0
  289. webscout/swiftcli/decorators/command.py +243 -0
  290. webscout/swiftcli/decorators/options.py +247 -0
  291. webscout/swiftcli/decorators/output.py +392 -0
  292. webscout/swiftcli/exceptions.py +21 -0
  293. webscout/swiftcli/plugins/__init__.py +9 -0
  294. webscout/swiftcli/plugins/base.py +134 -0
  295. webscout/swiftcli/plugins/manager.py +269 -0
  296. webscout/swiftcli/utils/__init__.py +58 -0
  297. webscout/swiftcli/utils/formatting.py +251 -0
  298. webscout/swiftcli/utils/parsing.py +368 -0
  299. webscout/update_checker.py +280 -136
  300. webscout/utils.py +28 -14
  301. webscout/version.py +2 -1
  302. webscout/version.py.bak +3 -0
  303. webscout/zeroart/__init__.py +218 -55
  304. webscout/zeroart/base.py +70 -60
  305. webscout/zeroart/effects.py +155 -99
  306. webscout/zeroart/fonts.py +1799 -816
  307. webscout-2026.1.19.dist-info/METADATA +638 -0
  308. webscout-2026.1.19.dist-info/RECORD +312 -0
  309. {webscout-8.2.2.dist-info → webscout-2026.1.19.dist-info}/WHEEL +1 -1
  310. webscout-2026.1.19.dist-info/entry_points.txt +4 -0
  311. webscout-2026.1.19.dist-info/top_level.txt +1 -0
  312. inferno/__init__.py +0 -6
  313. inferno/__main__.py +0 -9
  314. inferno/cli.py +0 -6
  315. webscout/DWEBS.py +0 -477
  316. webscout/Extra/autocoder/__init__.py +0 -9
  317. webscout/Extra/autocoder/autocoder.py +0 -849
  318. webscout/Extra/autocoder/autocoder_utiles.py +0 -332
  319. webscout/LLM.py +0 -442
  320. webscout/Litlogger/__init__.py +0 -67
  321. webscout/Litlogger/core/__init__.py +0 -6
  322. webscout/Litlogger/core/level.py +0 -23
  323. webscout/Litlogger/core/logger.py +0 -165
  324. webscout/Litlogger/handlers/__init__.py +0 -12
  325. webscout/Litlogger/handlers/console.py +0 -33
  326. webscout/Litlogger/handlers/file.py +0 -143
  327. webscout/Litlogger/handlers/network.py +0 -173
  328. webscout/Litlogger/styles/__init__.py +0 -7
  329. webscout/Litlogger/styles/colors.py +0 -249
  330. webscout/Litlogger/styles/formats.py +0 -458
  331. webscout/Litlogger/styles/text.py +0 -87
  332. webscout/Litlogger/utils/__init__.py +0 -6
  333. webscout/Litlogger/utils/detectors.py +0 -153
  334. webscout/Litlogger/utils/formatters.py +0 -200
  335. webscout/Local/__init__.py +0 -12
  336. webscout/Local/__main__.py +0 -9
  337. webscout/Local/api.py +0 -576
  338. webscout/Local/cli.py +0 -516
  339. webscout/Local/config.py +0 -75
  340. webscout/Local/llm.py +0 -287
  341. webscout/Local/model_manager.py +0 -253
  342. webscout/Local/server.py +0 -721
  343. webscout/Local/utils.py +0 -93
  344. webscout/Provider/AI21.py +0 -177
  345. webscout/Provider/AISEARCH/DeepFind.py +0 -250
  346. webscout/Provider/AISEARCH/ISou.py +0 -256
  347. webscout/Provider/AISEARCH/felo_search.py +0 -228
  348. webscout/Provider/AISEARCH/genspark_search.py +0 -208
  349. webscout/Provider/AISEARCH/hika_search.py +0 -194
  350. webscout/Provider/AISEARCH/scira_search.py +0 -324
  351. webscout/Provider/Aitopia.py +0 -292
  352. webscout/Provider/AllenAI.py +0 -413
  353. webscout/Provider/Blackboxai.py +0 -229
  354. webscout/Provider/C4ai.py +0 -432
  355. webscout/Provider/ChatGPTClone.py +0 -226
  356. webscout/Provider/ChatGPTES.py +0 -237
  357. webscout/Provider/ChatGPTGratis.py +0 -194
  358. webscout/Provider/Chatify.py +0 -175
  359. webscout/Provider/Cloudflare.py +0 -273
  360. webscout/Provider/DeepSeek.py +0 -196
  361. webscout/Provider/ElectronHub.py +0 -709
  362. webscout/Provider/ExaChat.py +0 -342
  363. webscout/Provider/Free2GPT.py +0 -241
  364. webscout/Provider/GPTWeb.py +0 -193
  365. webscout/Provider/Glider.py +0 -211
  366. webscout/Provider/HF_space/__init__.py +0 -0
  367. webscout/Provider/HF_space/qwen_qwen2.py +0 -206
  368. webscout/Provider/HuggingFaceChat.py +0 -462
  369. webscout/Provider/Hunyuan.py +0 -272
  370. webscout/Provider/LambdaChat.py +0 -392
  371. webscout/Provider/Llama.py +0 -200
  372. webscout/Provider/Llama3.py +0 -204
  373. webscout/Provider/Marcus.py +0 -148
  374. webscout/Provider/OLLAMA.py +0 -396
  375. webscout/Provider/OPENAI/c4ai.py +0 -367
  376. webscout/Provider/OPENAI/chatgptclone.py +0 -460
  377. webscout/Provider/OPENAI/exachat.py +0 -433
  378. webscout/Provider/OPENAI/freeaichat.py +0 -352
  379. webscout/Provider/OPENAI/opkfc.py +0 -488
  380. webscout/Provider/OPENAI/scirachat.py +0 -463
  381. webscout/Provider/OPENAI/standardinput.py +0 -425
  382. webscout/Provider/OPENAI/typegpt.py +0 -346
  383. webscout/Provider/OPENAI/uncovrAI.py +0 -455
  384. webscout/Provider/OPENAI/venice.py +0 -413
  385. webscout/Provider/OPENAI/yep.py +0 -327
  386. webscout/Provider/OpenGPT.py +0 -199
  387. webscout/Provider/Perplexitylabs.py +0 -415
  388. webscout/Provider/Phind.py +0 -535
  389. webscout/Provider/PizzaGPT.py +0 -198
  390. webscout/Provider/Reka.py +0 -214
  391. webscout/Provider/StandardInput.py +0 -278
  392. webscout/Provider/TTI/AiForce/__init__.py +0 -22
  393. webscout/Provider/TTI/AiForce/async_aiforce.py +0 -224
  394. webscout/Provider/TTI/AiForce/sync_aiforce.py +0 -245
  395. webscout/Provider/TTI/FreeAIPlayground/__init__.py +0 -9
  396. webscout/Provider/TTI/FreeAIPlayground/async_freeaiplayground.py +0 -181
  397. webscout/Provider/TTI/FreeAIPlayground/sync_freeaiplayground.py +0 -180
  398. webscout/Provider/TTI/ImgSys/__init__.py +0 -23
  399. webscout/Provider/TTI/ImgSys/async_imgsys.py +0 -202
  400. webscout/Provider/TTI/ImgSys/sync_imgsys.py +0 -195
  401. webscout/Provider/TTI/MagicStudio/__init__.py +0 -2
  402. webscout/Provider/TTI/MagicStudio/async_magicstudio.py +0 -111
  403. webscout/Provider/TTI/MagicStudio/sync_magicstudio.py +0 -109
  404. webscout/Provider/TTI/Nexra/__init__.py +0 -22
  405. webscout/Provider/TTI/Nexra/async_nexra.py +0 -286
  406. webscout/Provider/TTI/Nexra/sync_nexra.py +0 -258
  407. webscout/Provider/TTI/PollinationsAI/__init__.py +0 -23
  408. webscout/Provider/TTI/PollinationsAI/async_pollinations.py +0 -311
  409. webscout/Provider/TTI/PollinationsAI/sync_pollinations.py +0 -265
  410. webscout/Provider/TTI/aiarta/__init__.py +0 -2
  411. webscout/Provider/TTI/aiarta/async_aiarta.py +0 -482
  412. webscout/Provider/TTI/aiarta/sync_aiarta.py +0 -440
  413. webscout/Provider/TTI/artbit/__init__.py +0 -22
  414. webscout/Provider/TTI/artbit/async_artbit.py +0 -155
  415. webscout/Provider/TTI/artbit/sync_artbit.py +0 -148
  416. webscout/Provider/TTI/fastflux/__init__.py +0 -22
  417. webscout/Provider/TTI/fastflux/async_fastflux.py +0 -261
  418. webscout/Provider/TTI/fastflux/sync_fastflux.py +0 -252
  419. webscout/Provider/TTI/huggingface/__init__.py +0 -22
  420. webscout/Provider/TTI/huggingface/async_huggingface.py +0 -199
  421. webscout/Provider/TTI/huggingface/sync_huggingface.py +0 -195
  422. webscout/Provider/TTI/piclumen/__init__.py +0 -23
  423. webscout/Provider/TTI/piclumen/async_piclumen.py +0 -268
  424. webscout/Provider/TTI/piclumen/sync_piclumen.py +0 -233
  425. webscout/Provider/TTI/pixelmuse/__init__.py +0 -4
  426. webscout/Provider/TTI/pixelmuse/async_pixelmuse.py +0 -249
  427. webscout/Provider/TTI/pixelmuse/sync_pixelmuse.py +0 -182
  428. webscout/Provider/TTI/talkai/__init__.py +0 -4
  429. webscout/Provider/TTI/talkai/async_talkai.py +0 -229
  430. webscout/Provider/TTI/talkai/sync_talkai.py +0 -207
  431. webscout/Provider/TTS/gesserit.py +0 -127
  432. webscout/Provider/TeachAnything.py +0 -187
  433. webscout/Provider/Venice.py +0 -219
  434. webscout/Provider/VercelAI.py +0 -234
  435. webscout/Provider/WebSim.py +0 -228
  436. webscout/Provider/Writecream.py +0 -211
  437. webscout/Provider/WritingMate.py +0 -197
  438. webscout/Provider/aimathgpt.py +0 -189
  439. webscout/Provider/askmyai.py +0 -158
  440. webscout/Provider/asksteve.py +0 -203
  441. webscout/Provider/bagoodex.py +0 -145
  442. webscout/Provider/chatglm.py +0 -205
  443. webscout/Provider/copilot.py +0 -428
  444. webscout/Provider/freeaichat.py +0 -271
  445. webscout/Provider/gaurish.py +0 -244
  446. webscout/Provider/geminiprorealtime.py +0 -160
  447. webscout/Provider/granite.py +0 -187
  448. webscout/Provider/hermes.py +0 -219
  449. webscout/Provider/koala.py +0 -268
  450. webscout/Provider/labyrinth.py +0 -340
  451. webscout/Provider/lepton.py +0 -194
  452. webscout/Provider/llamatutor.py +0 -192
  453. webscout/Provider/multichat.py +0 -325
  454. webscout/Provider/promptrefine.py +0 -193
  455. webscout/Provider/scira_chat.py +0 -277
  456. webscout/Provider/scnet.py +0 -187
  457. webscout/Provider/talkai.py +0 -194
  458. webscout/Provider/tutorai.py +0 -252
  459. webscout/Provider/typegpt.py +0 -232
  460. webscout/Provider/uncovr.py +0 -312
  461. webscout/Provider/yep.py +0 -376
  462. webscout/litprinter/__init__.py +0 -59
  463. webscout/scout/core.py +0 -881
  464. webscout/tempid.py +0 -128
  465. webscout/webscout_search.py +0 -1346
  466. webscout/webscout_search_async.py +0 -877
  467. webscout/yep_search.py +0 -297
  468. webscout-8.2.2.dist-info/METADATA +0 -734
  469. webscout-8.2.2.dist-info/RECORD +0 -309
  470. webscout-8.2.2.dist-info/entry_points.txt +0 -5
  471. webscout-8.2.2.dist-info/top_level.txt +0 -3
  472. webstoken/__init__.py +0 -30
  473. webstoken/classifier.py +0 -189
  474. webstoken/keywords.py +0 -216
  475. webstoken/language.py +0 -128
  476. webstoken/ner.py +0 -164
  477. webstoken/normalizer.py +0 -35
  478. webstoken/processor.py +0 -77
  479. webstoken/sentiment.py +0 -206
  480. webstoken/stemmer.py +0 -73
  481. webstoken/tagger.py +0 -60
  482. webstoken/tokenizer.py +0 -158
  483. {webscout-8.2.2.dist-info → webscout-2026.1.19.dist-info/licenses}/LICENSE.md +0 -0
webscout/Bard.py CHANGED
@@ -1,535 +1,1072 @@
1
- #########################################
2
- # Code Generated by o3-mini-high
3
- #########################################
4
- import asyncio
5
- import json
6
- import os
7
- import random
8
- import re
9
- import string
10
- from enum import Enum
11
- from pathlib import Path
12
- from datetime import datetime
13
- from typing import Dict, List, Tuple, Union, Optional
14
-
15
- import httpx
16
- from httpx import AsyncClient, HTTPStatusError
17
-
18
- # For image models using validation. Adjust based on organization internal pydantic.
19
- from pydantic import BaseModel, validator
20
-
21
- # Rich is retained for logging within image methods.
22
- from rich.console import Console
23
- from rich.markdown import Markdown
24
-
25
- console = Console()
26
-
27
- #########################################
28
- # New Enums and functions for endpoints,
29
- # headers, models, file upload and images.
30
- #########################################
31
-
32
- class Endpoint(Enum):
33
- INIT = "https://gemini.google.com/app"
34
- GENERATE = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
35
- ROTATE_COOKIES = "https://accounts.google.com/RotateCookies"
36
- UPLOAD = "https://content-push.googleapis.com/upload"
37
-
38
- class Headers(Enum):
39
- GEMINI = {
40
- "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
41
- "Host": "gemini.google.com",
42
- "Origin": "https://gemini.google.com",
43
- "Referer": "https://gemini.google.com/",
44
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
45
- "X-Same-Domain": "1",
46
- }
47
- ROTATE_COOKIES = {
48
- "Content-Type": "application/json",
49
- }
50
- UPLOAD = {"Push-ID": "feeds/mcudyrk2a4khkz"}
51
-
52
- class Model(Enum):
53
- UNSPECIFIED = ("unspecified", {}, False)
54
- G_2_0_FLASH = (
55
- "gemini-2.0-flash",
56
- {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f299729663a2343f"]'},
57
- False,
58
- )
59
- G_2_0_FLASH_THINKING = (
60
- "gemini-2.0-flash-thinking",
61
- {"x-goog-ext-525001261-jspb": '[null,null,null,null,"7ca48d02d802f20a"]'},
62
- False,
63
- )
64
- G_2_5_PRO = (
65
- "gemini-2.5-pro",
66
- {"x-goog-ext-525001261-jspb": '[null,null,null,null,"2525e3954d185b3c"]'},
67
- False,
68
- )
69
- G_2_0_EXP_ADVANCED = (
70
- "gemini-2.0-exp-advanced",
71
- {"x-goog-ext-525001261-jspb": '[null,null,null,null,"b1e46a6037e6aa9f"]'},
72
- True,
73
- )
74
- G_2_5_EXP_ADVANCED = (
75
- "gemini-2.5-exp-advanced",
76
- {"x-goog-ext-525001261-jspb": '[null,null,null,null,"203e6bb81620bcfe"]'},
77
- True,
78
- )
79
- G_2_5_FLASH = (
80
- "gemini-2.5-flash",
81
- {"x-goog-ext-525001261-jspb": '[1,null,null,null,"35609594dbe934d8"]'},
82
- False,
83
- )
84
-
85
- def __init__(self, name, header, advanced_only):
86
- self.model_name = name
87
- self.model_header = header
88
- self.advanced_only = advanced_only
89
-
90
- @classmethod
91
- def from_name(cls, name: str):
92
- for model in cls:
93
- if model.model_name == name:
94
- return model
95
- raise ValueError(
96
- f"Unknown model name: {name}. Available models: {', '.join([model.model_name for model in cls])}"
97
- )
98
-
99
- async def upload_file(file: Union[bytes, str, Path], proxy: Optional[str] = None) -> str:
100
- """
101
- Upload a file to Google's server and return its identifier.
102
-
103
- Parameters:
104
- file: bytes | str | Path
105
- File data in bytes, or path to the file to be uploaded.
106
- proxy: str, optional
107
- Proxy URL.
108
-
109
- Returns:
110
- str: Identifier of the uploaded file.
111
- Raises:
112
- httpx.HTTPStatusError: If the upload request failed.
113
- """
114
- if not isinstance(file, bytes):
115
- with open(file, "rb") as f:
116
- file = f.read()
117
-
118
- async with AsyncClient(http2=True, proxies=proxy) as client:
119
- response = await client.post(
120
- url=Endpoint.UPLOAD.value,
121
- headers=Headers.UPLOAD.value,
122
- files={"file": file},
123
- follow_redirects=True,
124
- )
125
- response.raise_for_status()
126
- return response.text
127
-
128
- #########################################
129
- # Cookie loading and Chatbot classes
130
- #########################################
131
-
132
- def load_cookies(cookie_path: str) -> Tuple[str, str]:
133
- """Loads cookies from the provided JSON file."""
134
- try:
135
- with open(cookie_path, 'r') as file:
136
- cookies = json.load(file)
137
- session_auth1 = next(item['value'] for item in cookies if item['name'] == '__Secure-1PSID')
138
- session_auth2 = next(item['value'] for item in cookies if item['name'] == '__Secure-1PSIDTS')
139
- return session_auth1, session_auth2
140
- except FileNotFoundError:
141
- raise Exception(f"Cookie file not found at path: {cookie_path}")
142
- except json.JSONDecodeError:
143
- raise Exception("Invalid JSON format in the cookie file.")
144
- except StopIteration:
145
- raise Exception("Required cookies not found in the cookie file.")
146
-
147
- class Chatbot:
148
- """
149
- Synchronous wrapper for the AsyncChatbot class.
150
- """
151
- def __init__(
152
- self,
153
- cookie_path: str,
154
- proxy: dict = None,
155
- timeout: int = 20,
156
- model: Model = Model.UNSPECIFIED
157
- ):
158
- self.loop = asyncio.get_event_loop()
159
- self.secure_1psid, self.secure_1psidts = load_cookies(cookie_path)
160
- self.async_chatbot = self.loop.run_until_complete(
161
- AsyncChatbot.create(self.secure_1psid, self.secure_1psidts, proxy, timeout, model)
162
- )
163
-
164
- def save_conversation(self, file_path: str, conversation_name: str):
165
- return self.loop.run_until_complete(
166
- self.async_chatbot.save_conversation(file_path, conversation_name)
167
- )
168
-
169
- def load_conversations(self, file_path: str) -> List[Dict]:
170
- return self.loop.run_until_complete(
171
- self.async_chatbot.load_conversations(file_path)
172
- )
173
-
174
- def load_conversation(self, file_path: str, conversation_name: str) -> bool:
175
- return self.loop.run_until_complete(
176
- self.async_chatbot.load_conversation(file_path, conversation_name)
177
- )
178
-
179
- def ask(self, message: str) -> dict:
180
- return self.loop.run_until_complete(self.async_chatbot.ask(message))
181
-
182
- class AsyncChatbot:
183
- """
184
- A class to interact with Google Gemini.
185
- Parameters:
186
- secure_1psid: str
187
- The __Secure-1PSID cookie.
188
- secure_1psidts: str
189
- The __Secure-1PSIDTS cookie.
190
- proxy: dict
191
- Http request proxy.
192
- timeout: int
193
- Request timeout in seconds.
194
- model: Model
195
- Selected model for the session.
196
- """
197
- __slots__ = [
198
- "headers",
199
- "_reqid",
200
- "SNlM0e",
201
- "conversation_id",
202
- "response_id",
203
- "choice_id",
204
- "proxy",
205
- "secure_1psidts",
206
- "secure_1psid",
207
- "session",
208
- "timeout",
209
- "model",
210
- ]
211
-
212
- def __init__(
213
- self,
214
- secure_1psid: str,
215
- secure_1psidts: str,
216
- proxy: dict = None,
217
- timeout: int = 20,
218
- model: Model = Model.UNSPECIFIED,
219
- ):
220
- headers = Headers.GEMINI.value.copy()
221
- if model != Model.UNSPECIFIED:
222
- headers.update(model.model_header)
223
- self._reqid = int("".join(random.choices(string.digits, k=4)))
224
- self.proxy = proxy
225
- self.conversation_id = ""
226
- self.response_id = ""
227
- self.choice_id = ""
228
- self.secure_1psid = secure_1psid
229
- self.secure_1psidts = secure_1psidts
230
- self.session = httpx.AsyncClient(proxies=self.proxy)
231
- self.session.headers = headers
232
- self.session.cookies.set("__Secure-1PSID", secure_1psid)
233
- self.session.cookies.set("__Secure-1PSIDTS", secure_1psidts)
234
- self.timeout = timeout
235
- self.model = model
236
-
237
- @classmethod
238
- async def create(
239
- cls,
240
- secure_1psid: str,
241
- secure_1psidts: str,
242
- proxy: dict = None,
243
- timeout: int = 20,
244
- model: Model = Model.UNSPECIFIED,
245
- ) -> "AsyncChatbot":
246
- instance = cls(secure_1psid, secure_1psidts, proxy, timeout, model)
247
- instance.SNlM0e = await instance.__get_snlm0e()
248
- return instance
249
-
250
- async def save_conversation(self, file_path: str, conversation_name: str) -> None:
251
- conversations = await self.load_conversations(file_path)
252
- conversation_exists = False
253
- for conversation in conversations:
254
- if conversation["conversation_name"] == conversation_name:
255
- conversation["conversation_name"] = conversation_name
256
- conversation["_reqid"] = self._reqid
257
- conversation["conversation_id"] = self.conversation_id
258
- conversation["response_id"] = self.response_id
259
- conversation["choice_id"] = self.choice_id
260
- conversation["SNlM0e"] = self.SNlM0e
261
- conversation_exists = True
262
- if not conversation_exists:
263
- conversation = {
264
- "conversation_name": conversation_name,
265
- "_reqid": self._reqid,
266
- "conversation_id": self.conversation_id,
267
- "response_id": self.response_id,
268
- "choice_id": self.choice_id,
269
- "SNlM0e": self.SNlM0e,
270
- }
271
- conversations.append(conversation)
272
- with open(file_path, "w", encoding="utf-8") as f:
273
- json.dump(conversations, f, indent=4)
274
-
275
- async def load_conversations(self, file_path: str) -> List[Dict]:
276
- if not os.path.isfile(file_path):
277
- return []
278
- with open(file_path, encoding="utf-8") as f:
279
- return json.load(f)
280
-
281
- async def load_conversation(self, file_path: str, conversation_name: str) -> bool:
282
- conversations = await self.load_conversations(file_path)
283
- for conversation in conversations:
284
- if conversation["conversation_name"] == conversation_name:
285
- self._reqid = conversation["_reqid"]
286
- self.conversation_id = conversation["conversation_id"]
287
- self.response_id = conversation["response_id"]
288
- self.choice_id = conversation["choice_id"]
289
- self.SNlM0e = conversation["SNlM0e"]
290
- return True
291
- return False
292
-
293
- async def __get_snlm0e(self):
294
- if not (self.secure_1psid and self.secure_1psidts) or self.secure_1psid[:2] != "g.":
295
- raise Exception("Enter correct __Secure_1PSID and __Secure_1PSIDTS value. __Secure_1PSID value must start with a g.")
296
- resp = await self.session.get(Endpoint.INIT.value, timeout=10, follow_redirects=True)
297
- if resp.status_code != 200:
298
- raise Exception(f"Response code not 200. Response Status is {resp.status_code}")
299
- snlm0e_match = re.search(r'"SNlM0e":"(.*?)"', resp.text)
300
- if not snlm0e_match:
301
- raise Exception("SNlM0e value not found in response. Check __Secure_1PSID value."
302
- "\nNOTE: The cookies expire after a short period; ensure you update them frequently."
303
- f" Failed with status {resp.status_code} - {resp.reason_phrase}")
304
- return snlm0e_match.group(1)
305
-
306
- async def ask(self, message: str) -> dict:
307
- params = {
308
- "bl": "boq_assistant-bard-web-server_20230713.13_p0",
309
- "_reqid": str(self._reqid),
310
- "rt": "c",
311
- }
312
- message_struct = [
313
- [message],
314
- None,
315
- [self.conversation_id, self.response_id, self.choice_id],
316
- ]
317
- data = {
318
- "f.req": json.dumps([None, json.dumps(message_struct)]),
319
- "at": self.SNlM0e,
320
- }
321
- resp = await self.session.post(
322
- Endpoint.GENERATE.value,
323
- params=params,
324
- data=data,
325
- timeout=self.timeout,
326
- )
327
- try:
328
- chat_data_line = resp.content.splitlines()[3]
329
- chat_data = json.loads(chat_data_line)[0][2]
330
- except (IndexError, json.JSONDecodeError):
331
- return {"content": f"Gemini encountered an error: {resp.content}."}
332
- if not chat_data:
333
- return {"content": f"Gemini returned empty response: {resp.content}."}
334
- json_chat_data = json.loads(chat_data)
335
- images = []
336
- if len(json_chat_data) >= 3:
337
- if len(json_chat_data[4][0]) >= 4 and json_chat_data[4][0][4]:
338
- for img in json_chat_data[4][0][4]:
339
- images.append(img[0][0][0])
340
- results = {
341
- "content": json_chat_data[4][0][1][0],
342
- "conversation_id": json_chat_data[1][0],
343
- "response_id": json_chat_data[1][1],
344
- "factualityQueries": json_chat_data[3],
345
- "textQuery": json_chat_data[2][0] if json_chat_data[2] is not None else "",
346
- "choices": [{"id": i[0], "content": i[1]} for i in json_chat_data[4]],
347
- "images": images,
348
- }
349
- self.conversation_id = results["conversation_id"]
350
- self.response_id = results["response_id"]
351
- self.choice_id = results["choices"][0]["id"]
352
- self._reqid += 100000
353
- return results
354
-
355
- #########################################
356
- # New Image classes
357
- #########################################
358
-
359
- class Image(BaseModel):
360
- """
361
- A single image object returned from Gemini.
362
- Parameters:
363
- url: str
364
- URL of the image.
365
- title: str, optional
366
- Title of the image (default: "[Image]").
367
- alt: str, optional
368
- Optional description.
369
- proxy: str, optional
370
- Proxy used when saving the image.
371
- """
372
- url: str
373
- title: str = "[Image]"
374
- alt: str = ""
375
- proxy: Optional[str] = None
376
-
377
- def __str__(self):
378
- return f"{self.title}({self.url}) - {self.alt}"
379
-
380
- def __repr__(self):
381
- short_url = self.url if len(self.url) <= 20 else self.url[:8] + "..." + self.url[-12:]
382
- return f"Image(title='{self.title}', url='{short_url}', alt='{self.alt}')"
383
-
384
- async def save(
385
- self,
386
- path: str = "temp",
387
- filename: Optional[str] = None,
388
- cookies: Optional[dict] = None,
389
- verbose: bool = False,
390
- skip_invalid_filename: bool = False,
391
- ) -> Optional[str]:
392
- """
393
- Save the image to disk.
394
- Parameters:
395
- path: str, optional
396
- Directory to save the image (default "./temp").
397
- filename: str, optional
398
- Filename to use; if not provided, inferred from URL.
399
- cookies: dict, optional
400
- Cookies used for the image request.
401
- verbose: bool, optional
402
- If True, outputs status messages (default False).
403
- skip_invalid_filename: bool, optional
404
- If True, skips saving if the filename is invalid.
405
- Returns:
406
- Absolute path of the saved image if successful; None if skipped.
407
- Raises:
408
- httpx.HTTPError if the network request fails.
409
- """
410
- filename = filename or self.url.split("/")[-1].split("?")[0]
411
- try:
412
- filename = re.search(r"^(.*\.\w+)", filename).group()
413
- except AttributeError:
414
- if verbose:
415
- console.log(f"Invalid filename: {filename}")
416
- if skip_invalid_filename:
417
- return None
418
- async with AsyncClient(http2=True, follow_redirects=True, cookies=cookies, proxies=self.proxy) as client:
419
- response = await client.get(self.url)
420
- if response.status_code == 200:
421
- content_type = response.headers.get("content-type")
422
- if content_type and "image" not in content_type:
423
- console.log(f"Warning: Content type of {filename} is {content_type}, not an image.")
424
- dest_path = Path(path)
425
- dest_path.mkdir(parents=True, exist_ok=True)
426
- dest = dest_path / filename
427
- dest.write_bytes(response.content)
428
- if verbose:
429
- console.log(f"Image saved as {dest.resolve()}")
430
- return str(dest.resolve())
431
- else:
432
- raise HTTPStatusError(
433
- f"Error downloading image: {response.status_code} {response.reason_phrase}",
434
- request=response.request,
435
- response=response,
436
- )
437
-
438
- class WebImage(Image):
439
- """
440
- Image retrieved from web.
441
- Returned when asking Gemini to "SEND an image of [something]".
442
- """
443
- pass
444
-
445
- class GeneratedImage(Image):
446
- """
447
- Image generated by ImageFX (Google's AI image generator).
448
- Parameters:
449
- cookies: dict[str, str]
450
- Cookies used from the GeminiClient.
451
- """
452
- cookies: Dict[str, str]
453
-
454
- @validator("cookies")
455
- def validate_cookies(cls, v):
456
- if not v:
457
- raise ValueError("GeneratedImage requires cookies from GeminiClient.")
458
- return v
459
-
460
- async def save(self, **kwargs) -> Optional[str]:
461
- """
462
- Save the generated image to disk.
463
- Parameters:
464
- filename: str, optional
465
- Filename to use; generated images are in .png format.
466
- Additional arguments are passed to Image.save.
467
- Returns:
468
- Absolute path of the saved image if successful.
469
- """
470
- filename = kwargs.pop("filename", None) or f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{self.url[-10:]}.png"
471
- return await super().save(filename=filename, cookies=self.cookies, **kwargs)
472
-
473
- #########################################
474
- # Main usage demonstration
475
- #########################################
476
-
477
- if __name__ == "__main__":
478
- """
479
- Usage demonstration:
480
- - Reads cookies from 'cookies.json'
481
- - Initializes the synchronous Chatbot wrapper.
482
- - Performs a text query.
483
- - Performs an image generation query and downloads the generated image.
484
- - Demonstrates saving a conversation.
485
- """
486
- # Define the path to cookies file
487
- cookies_file = r"cookies.json"
488
-
489
- # Create Chatbot instance with a chosen model
490
- try:
491
- bot = Chatbot(cookie_path=cookies_file, model=Model.G_2_0_FLASH_THINKING_WITH_APPS)
492
- except Exception as e:
493
- console.log(f"[red]Error initializing Chatbot: {e}[/red]")
494
- exit(1)
495
-
496
- # Sample text query
497
- text_message = "How many r's in word strawberry?"
498
- console.log("[green]Sending text query to Gemini...[/green]")
499
- try:
500
- response_text = bot.ask(text_message)
501
- console.log("[blue]Text Response:[/blue]")
502
- console.print(Markdown(response_text.get("content", "No content received.")))
503
- except Exception as e:
504
- console.log(f"[red]Error sending text query: {e}[/red]")
505
-
506
- # Image generation query
507
- image_message = "Generate an image of a scenic view."
508
- console.log("[green]Requesting image generation from Gemini...[/green]")
509
- try:
510
- response_image = bot.ask(image_message)
511
- # Check if any image URL is returned in the response
512
- image_urls = response_image.get("images", [])
513
- if not image_urls:
514
- console.log("[red]No image URLs returned in response.[/red]")
515
- else:
516
- image_url = image_urls[0]
517
- console.log(f"[blue]Image URL received: {image_url}[/blue]")
518
- # Use GeneratedImage class to download the generated image
519
- generated_img = GeneratedImage(
520
- url=image_url,
521
- cookies={"__Secure-1PSID": bot.secure_1psid, "__Secure-1PSIDTS": bot.secure_1psidts}
522
- )
523
- saved_path = asyncio.run(generated_img.save(path="downloaded_images", verbose=True))
524
- console.log(f"[blue]Generated image saved at: {saved_path}[/blue]")
525
- except Exception as e:
526
- console.log(f"[red]Error processing image generation: {e}[/red]")
527
-
528
- # Demonstrate saving a conversation
529
- conversation_file = "conversations.json"
530
- conversation_name = "Sample Conversation"
531
- try:
532
- bot.save_conversation(conversation_file, conversation_name)
533
- console.log(f"[green]Conversation saved to {conversation_file} under the name '{conversation_name}'.[/green]")
534
- except Exception as e:
535
- console.log(f"[red]Error saving conversation: {e}[/red]")
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import random
5
+ import re
6
+ import string
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union, cast
11
+
12
+ try:
13
+ import trio # type: ignore # noqa: F401
14
+ except ImportError:
15
+ pass
16
+ from curl_cffi import CurlError
17
+ from curl_cffi.requests import AsyncSession
18
+ from pydantic import BaseModel, field_validator
19
+ from requests.exceptions import HTTPError, RequestException, Timeout
20
+ from rich.console import Console
21
+
22
+ console = Console()
23
+
24
+
25
+ class AskResponse(TypedDict):
26
+ content: str
27
+ conversation_id: str
28
+ response_id: str
29
+ factualityQueries: Optional[List[Any]]
30
+ textQuery: str
31
+ choices: List[Dict[str, Union[str, List[str]]]]
32
+ images: List[Dict[str, str]]
33
+ error: bool
34
+
35
+
36
+ class Endpoint(Enum):
37
+ """
38
+ Enum for Google Gemini API endpoints.
39
+
40
+ Attributes:
41
+ INIT (str): URL for initializing the Gemini session.
42
+ GENERATE (str): URL for generating chat responses.
43
+ ROTATE_COOKIES (str): URL for rotating authentication cookies.
44
+ UPLOAD (str): URL for uploading files/images.
45
+ """
46
+
47
+ INIT = "https://gemini.google.com/app"
48
+ GENERATE = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
49
+ ROTATE_COOKIES = "https://accounts.google.com/RotateCookies"
50
+ UPLOAD = "https://content-push.googleapis.com/upload"
51
+
52
+
53
+ class Headers(Enum):
54
+ """
55
+ Enum for HTTP headers used in Gemini API requests.
56
+
57
+ Attributes:
58
+ GEMINI (dict): Headers for Gemini chat requests.
59
+ ROTATE_COOKIES (dict): Headers for rotating cookies.
60
+ UPLOAD (dict): Headers for file/image upload.
61
+ """
62
+
63
+ GEMINI = {
64
+ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
65
+ "Host": "gemini.google.com",
66
+ "Origin": "https://gemini.google.com",
67
+ "Referer": "https://gemini.google.com/",
68
+ "X-Same-Domain": "1",
69
+ }
70
+ ROTATE_COOKIES = {
71
+ "Content-Type": "application/json",
72
+ }
73
+ UPLOAD = {"Push-ID": "feeds/mcudyrk2a4khkz"}
74
+
75
+
76
+ class Model(Enum):
77
+ """
78
+ Enum for available Gemini model configurations.
79
+
80
+ Attributes:
81
+ model_name (str): Name of the model.
82
+ model_header (dict): Additional headers required for the model.
83
+ advanced_only (bool): Whether the model is available only for advanced users.
84
+ """
85
+
86
+ UNSPECIFIED = ("unspecified", {}, False)
87
+ GEMINI_3_0_PRO = (
88
+ "gemini-3.0-pro",
89
+ {
90
+ "x-goog-ext-525001261-jspb": '[1,null,null,null,"e6fa609c3fa255c0",null,null,0,[4],null,null,2]'
91
+ },
92
+ False,
93
+ )
94
+ GEMINI_3_0_FLASH = (
95
+ "gemini-3.0-flash",
96
+ {
97
+ "x-goog-ext-525001261-jspb": '[1,null,null,null,"56fdd199312815e2",null,null,0,[4],null,null,2]'
98
+ },
99
+ False,
100
+ )
101
+ GEMINI_3_0_FLASH_THINKING = (
102
+ "gemini-3.0-flash-thinking",
103
+ {
104
+ "x-goog-ext-525001261-jspb": '[1,null,null,null,"e051ce1aa80aa576",null,null,0,[4],null,null,2]'
105
+ },
106
+ False,
107
+ )
108
+
109
+ def __init__(self, name: str, header: Dict[str, str], advanced_only: bool):
110
+ """
111
+ Initialize a Model enum member.
112
+
113
+ Args:
114
+ name (str): Model name.
115
+ header (dict): Model-specific headers.
116
+ advanced_only (bool): If True, model is for advanced users only.
117
+ """
118
+ self.model_name = name
119
+ self.model_header = header
120
+ self.advanced_only = advanced_only
121
+
122
+ @classmethod
123
+ def from_name(cls, name: str):
124
+ """
125
+ Get a Model enum member by its model name.
126
+
127
+ Args:
128
+ name (str): Name of the model.
129
+
130
+ Returns:
131
+ Model: Corresponding Model enum member.
132
+
133
+ Raises:
134
+ ValueError: If the model name is not found.
135
+ """
136
+ for model in cls:
137
+ if model.model_name == name:
138
+ return model
139
+ raise ValueError(
140
+ f"Unknown model name: {name}. Available models: {', '.join([model.model_name for model in cls])}"
141
+ )
142
+
143
+
144
+ async def upload_file(
145
+ file: Union[bytes, str, Path],
146
+ proxy: Optional[Union[str, Dict[str, str]]] = None,
147
+ impersonate: str = "chrome110",
148
+ ) -> str:
149
+ """
150
+ Uploads a file to Google's Gemini server using curl_cffi and returns its identifier.
151
+
152
+ Args:
153
+ file (bytes | str | Path): File data in bytes or path to the file to be uploaded.
154
+ proxy (str | dict, optional): Proxy URL or dictionary for the request.
155
+ impersonate (str, optional): Browser profile for curl_cffi to impersonate. Defaults to "chrome110".
156
+
157
+ Returns:
158
+ str: Identifier of the uploaded file.
159
+
160
+ Raises:
161
+ HTTPError: If the upload request fails.
162
+ RequestException: For other network-related errors.
163
+ FileNotFoundError: If the file path does not exist.
164
+ """
165
+ if not isinstance(file, bytes):
166
+ file_path = Path(file)
167
+ if not file_path.is_file():
168
+ raise FileNotFoundError(f"File not found at path: {file}")
169
+ with open(file_path, "rb") as f:
170
+ file_content = f.read()
171
+ else:
172
+ file_content = file
173
+
174
+ proxies_dict = None
175
+ if isinstance(proxy, str):
176
+ proxies_dict = {"http": proxy, "https": proxy}
177
+ elif isinstance(proxy, dict):
178
+ proxies_dict = proxy
179
+
180
+ try:
181
+ async with AsyncSession(
182
+ proxies=cast(Any, proxies_dict),
183
+ impersonate=cast(Any, impersonate),
184
+ headers=Headers.UPLOAD.value,
185
+ ) as client:
186
+ response = await client.post(
187
+ url=Endpoint.UPLOAD.value,
188
+ files={"file": file_content},
189
+ )
190
+ response.raise_for_status()
191
+ return response.text
192
+ except HTTPError as e:
193
+ console.log(f"[red]HTTP error during file upload: {e.response.status_code} {e}[/red]")
194
+ raise
195
+ except (RequestException, CurlError) as e:
196
+ console.log(f"[red]Network error during file upload: {e}[/red]")
197
+ raise
198
+
199
+
200
+ def load_cookies(cookie_path: str) -> Tuple[str, str]:
201
+ """
202
+ Loads authentication cookies from a JSON file.
203
+
204
+ Args:
205
+ cookie_path (str): Path to the JSON file containing cookies.
206
+
207
+ Returns:
208
+ tuple[str, str]: Tuple containing __Secure-1PSID and __Secure-1PSIDTS cookie values.
209
+
210
+ Raises:
211
+ Exception: If the file is not found, invalid, or required cookies are missing.
212
+ """
213
+ try:
214
+ with open(cookie_path, "r", encoding="utf-8") as file:
215
+ cookies = json.load(file)
216
+ session_auth1 = next(
217
+ (item["value"] for item in cookies if item["name"].upper() == "__SECURE-1PSID"), None
218
+ )
219
+ session_auth2 = next(
220
+ (item["value"] for item in cookies if item["name"].upper() == "__SECURE-1PSIDTS"), None
221
+ )
222
+
223
+ if not session_auth1 or not session_auth2:
224
+ raise ValueError("Required cookies (__Secure-1PSID or __Secure-1PSIDTS) not found.")
225
+
226
+ return session_auth1, session_auth2
227
+ except FileNotFoundError:
228
+ raise Exception(f"Cookie file not found at path: {cookie_path}")
229
+ except json.JSONDecodeError:
230
+ raise Exception("Invalid JSON format in the cookie file.")
231
+ except StopIteration as e:
232
+ raise Exception(f"{e} Check the cookie file format and content.")
233
+ except Exception as e:
234
+ raise Exception(f"An unexpected error occurred while loading cookies: {e}")
235
+
236
+
237
+ class Chatbot:
238
+ """
239
+ Synchronous wrapper for the AsyncChatbot class.
240
+
241
+ This class provides a synchronous interface to interact with Google Gemini,
242
+ handling authentication, conversation management, and message sending.
243
+
244
+ Attributes:
245
+ loop (asyncio.AbstractEventLoop): Event loop for running async tasks.
246
+ secure_1psid (str): Authentication cookie.
247
+ secure_1psidts (str): Authentication cookie.
248
+ async_chatbot (AsyncChatbot): Underlying asynchronous chatbot instance.
249
+ """
250
+
251
+ def __init__(
252
+ self,
253
+ cookie_path: str,
254
+ proxy: Optional[Union[str, Dict[str, str]]] = None,
255
+ timeout: int = 20,
256
+ model: Model = Model.UNSPECIFIED,
257
+ impersonate: str = "chrome110",
258
+ ):
259
+ try:
260
+ self.loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
261
+ except RuntimeError:
262
+ self.loop = asyncio.new_event_loop()
263
+ asyncio.set_event_loop(self.loop)
264
+
265
+ self.secure_1psid, self.secure_1psidts = load_cookies(cookie_path)
266
+ self.async_chatbot = self.loop.run_until_complete(
267
+ AsyncChatbot.create(
268
+ self.secure_1psid, self.secure_1psidts, proxy, timeout, model, impersonate
269
+ )
270
+ )
271
+
272
+ def save_conversation(self, file_path: str, conversation_name: str):
273
+ return self.loop.run_until_complete(
274
+ self.async_chatbot.save_conversation(file_path, conversation_name)
275
+ )
276
+
277
+ def load_conversations(self, file_path: str) -> List[Dict]:
278
+ return self.loop.run_until_complete(self.async_chatbot.load_conversations(file_path))
279
+
280
+ def load_conversation(self, file_path: str, conversation_name: str) -> bool:
281
+ return self.loop.run_until_complete(
282
+ self.async_chatbot.load_conversation(file_path, conversation_name)
283
+ )
284
+
285
+ def ask(self, message: str, image: Optional[Union[bytes, str, Path]] = None) -> AskResponse:
286
+ return self.loop.run_until_complete(self.async_chatbot.ask(message, image=image))
287
+
288
+
289
+ class AsyncChatbot:
290
+ """
291
+ Asynchronous chatbot client for interacting with Google Gemini using curl_cffi.
292
+
293
+ This class manages authentication, session state, conversation history,
294
+ and sending/receiving messages (including images) asynchronously.
295
+
296
+ Attributes:
297
+ headers (dict): HTTP headers for requests.
298
+ _reqid (int): Request identifier for Gemini API.
299
+ SNlM0e (str): Session token required for API requests.
300
+ conversation_id (str): Current conversation ID.
301
+ response_id (str): Current response ID.
302
+ choice_id (str): Current choice ID.
303
+ proxy (str | dict | None): Proxy configuration.
304
+ proxies_dict (dict | None): Proxy dictionary for curl_cffi.
305
+ secure_1psid (str): Authentication cookie.
306
+ secure_1psidts (str): Authentication cookie.
307
+ session (AsyncSession): curl_cffi session for HTTP requests.
308
+ timeout (int): Request timeout in seconds.
309
+ model (Model): Selected Gemini model.
310
+ impersonate (str): Browser profile for curl_cffi to impersonate.
311
+ """
312
+
313
+ __slots__ = [
314
+ "headers",
315
+ "_reqid",
316
+ "SNlM0e",
317
+ "conversation_id",
318
+ "response_id",
319
+ "choice_id",
320
+ "proxy",
321
+ "proxies_dict",
322
+ "secure_1psidts",
323
+ "secure_1psid",
324
+ "session",
325
+ "timeout",
326
+ "model",
327
+ "impersonate",
328
+ ]
329
+
330
+ def __init__(
331
+ self,
332
+ secure_1psid: str,
333
+ secure_1psidts: str,
334
+ proxy: Optional[Union[str, Dict[str, str]]] = None,
335
+ timeout: int = 20,
336
+ model: Model = Model.UNSPECIFIED,
337
+ impersonate: str = "chrome110",
338
+ ):
339
+ headers = Headers.GEMINI.value.copy()
340
+ if model != Model.UNSPECIFIED:
341
+ headers.update(model.model_header)
342
+ self._reqid = int("".join(random.choices(string.digits, k=7)))
343
+ self.proxy = proxy
344
+ self.impersonate = impersonate
345
+
346
+ self.proxies_dict = None
347
+ if isinstance(proxy, str):
348
+ self.proxies_dict = {"http": proxy, "https": proxy}
349
+ elif isinstance(proxy, dict):
350
+ self.proxies_dict = proxy
351
+
352
+ self.conversation_id = ""
353
+ self.response_id = ""
354
+ self.choice_id = ""
355
+ self.secure_1psid = secure_1psid
356
+ self.secure_1psidts = secure_1psidts
357
+
358
+ self.session: AsyncSession = AsyncSession(
359
+ headers=headers,
360
+ cookies={"__Secure-1PSID": secure_1psid, "__Secure-1PSIDTS": secure_1psidts},
361
+ proxies=cast(Any, self.proxies_dict if self.proxies_dict else None),
362
+ timeout=timeout,
363
+ impersonate=cast(Any, self.impersonate if self.impersonate else None),
364
+ )
365
+
366
+ self.timeout = timeout
367
+ self.model = model
368
+ self.SNlM0e = None
369
+
370
+ @classmethod
371
+ async def create(
372
+ cls,
373
+ secure_1psid: str,
374
+ secure_1psidts: str,
375
+ proxy: Optional[Union[str, Dict[str, str]]] = None,
376
+ timeout: int = 20,
377
+ model: Model = Model.UNSPECIFIED,
378
+ impersonate: str = "chrome110",
379
+ ) -> "AsyncChatbot":
380
+ """
381
+ Factory method to create and initialize an AsyncChatbot instance.
382
+ Fetches the necessary SNlM0e value asynchronously.
383
+ """
384
+ instance = cls(secure_1psid, secure_1psidts, proxy, timeout, model, impersonate)
385
+ try:
386
+ instance.SNlM0e = await instance.__get_snlm0e()
387
+ except Exception as e:
388
+ console.log(
389
+ f"[red]Error during AsyncChatbot initialization (__get_snlm0e): {e}[/red]",
390
+ style="bold red",
391
+ )
392
+ await instance.session.close()
393
+ raise
394
+ return instance
395
+
396
+ def _error_response(self, message: str) -> AskResponse:
397
+ """Helper to create a consistent error response."""
398
+ return {
399
+ "content": message,
400
+ "conversation_id": getattr(self, "conversation_id", ""),
401
+ "response_id": getattr(self, "response_id", ""),
402
+ "factualityQueries": [],
403
+ "textQuery": "",
404
+ "choices": [],
405
+ "images": [],
406
+ "error": True,
407
+ }
408
+
409
+ async def save_conversation(self, file_path: str, conversation_name: str) -> None:
410
+ conversations = await self.load_conversations(file_path)
411
+ conversation_data = {
412
+ "conversation_name": conversation_name,
413
+ "_reqid": self._reqid,
414
+ "conversation_id": self.conversation_id,
415
+ "response_id": self.response_id,
416
+ "choice_id": self.choice_id,
417
+ "SNlM0e": self.SNlM0e,
418
+ "model_name": self.model.model_name,
419
+ "timestamp": datetime.now().isoformat(),
420
+ }
421
+
422
+ found = False
423
+ for i, conv in enumerate(conversations):
424
+ if conv.get("conversation_name") == conversation_name:
425
+ conversations[i] = conversation_data
426
+ found = True
427
+ break
428
+ if not found:
429
+ conversations.append(conversation_data)
430
+
431
+ try:
432
+ Path(file_path).parent.mkdir(parents=True, exist_ok=True)
433
+ with open(file_path, "w", encoding="utf-8") as f:
434
+ json.dump(conversations, f, indent=4, ensure_ascii=False)
435
+ except IOError as e:
436
+ console.log(f"[red]Error saving conversation to {file_path}: {e}[/red]")
437
+ raise
438
+
439
+ async def load_conversations(self, file_path: str) -> List[Dict]:
440
+ if not os.path.isfile(file_path):
441
+ return []
442
+ try:
443
+ with open(file_path, "r", encoding="utf-8") as f:
444
+ return json.load(f)
445
+ except (json.JSONDecodeError, IOError) as e:
446
+ console.log(f"[red]Error loading conversations from {file_path}: {e}[/red]")
447
+ return []
448
+
449
+ async def load_conversation(self, file_path: str, conversation_name: str) -> bool:
450
+ conversations = await self.load_conversations(file_path)
451
+ for conversation in conversations:
452
+ if conversation.get("conversation_name") == conversation_name:
453
+ try:
454
+ self._reqid = conversation["_reqid"]
455
+ self.conversation_id = conversation["conversation_id"]
456
+ self.response_id = conversation["response_id"]
457
+ self.choice_id = conversation["choice_id"]
458
+ self.SNlM0e = conversation["SNlM0e"]
459
+ if "model_name" in conversation:
460
+ try:
461
+ self.model = Model.from_name(conversation["model_name"])
462
+ self.session.headers.update(self.model.model_header)
463
+ except ValueError as e:
464
+ console.log(
465
+ f"[yellow]Warning: Model '{conversation['model_name']}' from saved conversation not found. Using current model '{self.model.model_name}'. Error: {e}[/yellow]"
466
+ )
467
+
468
+ console.log(f"Loaded conversation '{conversation_name}'")
469
+ return True
470
+ except KeyError as e:
471
+ console.log(
472
+ f"[red]Error loading conversation '{conversation_name}': Missing key {e}[/red]"
473
+ )
474
+ return False
475
+ console.log(f"[yellow]Conversation '{conversation_name}' not found in {file_path}[/yellow]")
476
+ return False
477
+
478
+ async def __get_snlm0e(self) -> str:
479
+ """Fetches the SNlM0e value required for API requests using curl_cffi."""
480
+ if not self.secure_1psid:
481
+ raise ValueError("__Secure-1PSID cookie is required.")
482
+
483
+ try:
484
+ resp = await self.session.get(Endpoint.INIT.value, timeout=self.timeout)
485
+ resp.raise_for_status()
486
+
487
+ if "Sign in to continue" in resp.text or "accounts.google.com" in str(resp.url):
488
+ raise PermissionError(
489
+ "Authentication failed. Cookies might be invalid or expired. Please update them."
490
+ )
491
+
492
+ snlm0e_match = re.search(r'["\']SNlM0e["\']\s*:\s*["\'](.*?)["\']', resp.text)
493
+ if not snlm0e_match:
494
+ error_message = "SNlM0e value not found in response."
495
+ if resp.status_code == 429:
496
+ error_message += " Rate limit likely exceeded."
497
+ else:
498
+ error_message += (
499
+ f" Response status: {resp.status_code}. Check cookie validity and network."
500
+ )
501
+ raise ValueError(error_message)
502
+
503
+ if not self.secure_1psidts and "PSIDTS" not in self.session.cookies:
504
+ try:
505
+ await self.__rotate_cookies()
506
+ except Exception as e:
507
+ console.log(f"[yellow]Warning: Could not refresh PSIDTS cookie: {e}[/yellow]")
508
+
509
+ return snlm0e_match.group(1)
510
+
511
+ except Timeout as e:
512
+ raise TimeoutError(f"Request timed out while fetching SNlM0e: {e}") from e
513
+ except (RequestException, CurlError) as e:
514
+ raise ConnectionError(f"Network error while fetching SNlM0e: {e}") from e
515
+ except Exception as e:
516
+ if isinstance(e, HTTPError) and (
517
+ e.response.status_code == 401 or e.response.status_code == 403
518
+ ):
519
+ raise PermissionError(
520
+ f"Authentication failed (status {e.response.status_code}). Check cookies. {e}"
521
+ ) from e
522
+ else:
523
+ raise Exception(f"Error while fetching SNlM0e: {e}") from e
524
+
525
+ async def __rotate_cookies(self) -> Optional[str]:
526
+ """Rotates the __Secure-1PSIDTS cookie."""
527
+ try:
528
+ response = await self.session.post(
529
+ Endpoint.ROTATE_COOKIES.value,
530
+ headers=Headers.ROTATE_COOKIES.value,
531
+ data='[000,"-0000000000000000000"]',
532
+ timeout=self.timeout,
533
+ )
534
+ response.raise_for_status()
535
+
536
+ if new_1psidts := response.cookies.get("__Secure-1PSIDTS"):
537
+ self.secure_1psidts = new_1psidts
538
+ self.session.cookies.set("__Secure-1PSIDTS", new_1psidts)
539
+ return new_1psidts
540
+ except Exception as e:
541
+ console.log(f"[yellow]Cookie rotation failed: {e}[/yellow]")
542
+ raise
543
+
544
+ async def ask(
545
+ self, message: str, image: Optional[Union[bytes, str, Path]] = None
546
+ ) -> AskResponse:
547
+ """
548
+ Sends a message to Google Gemini and returns the response using curl_cffi.
549
+
550
+ Parameters:
551
+ message: str
552
+ The message to send.
553
+ image: Optional[Union[bytes, str, Path]]
554
+ Optional image data (bytes) or path to an image file to include.
555
+
556
+ Returns:
557
+ dict: A dictionary containing the response content and metadata.
558
+ """
559
+ if self.SNlM0e is None:
560
+ raise RuntimeError("AsyncChatbot not properly initialized. Call AsyncChatbot.create()")
561
+
562
+ params = {
563
+ "bl": "boq_assistant-bard-web-server_20240625.13_p0",
564
+ "_reqid": str(self._reqid),
565
+ "rt": "c",
566
+ }
567
+
568
+ image_upload_id = None
569
+ if image:
570
+ try:
571
+ image_upload_id = await upload_file(
572
+ image, proxy=self.proxies_dict, impersonate=self.impersonate
573
+ )
574
+ console.log(f"Image uploaded successfully. ID: {image_upload_id}")
575
+ except Exception as e:
576
+ console.log(f"[red]Error uploading image: {e}[/red]")
577
+ return self._error_response(f"Error uploading image: {e}")
578
+
579
+ if image_upload_id:
580
+ message_struct = [
581
+ [message],
582
+ [[[image_upload_id, 1]]],
583
+ [self.conversation_id, self.response_id, self.choice_id],
584
+ ]
585
+ else:
586
+ message_struct = [
587
+ [message],
588
+ None,
589
+ [self.conversation_id, self.response_id, self.choice_id],
590
+ ]
591
+
592
+ data = {
593
+ "f.req": json.dumps(
594
+ [None, json.dumps(message_struct, ensure_ascii=False)], ensure_ascii=False
595
+ ),
596
+ "at": self.SNlM0e,
597
+ }
598
+
599
+ resp = None
600
+ try:
601
+ resp = await self.session.post(
602
+ Endpoint.GENERATE.value,
603
+ params=params,
604
+ data=data,
605
+ timeout=self.timeout,
606
+ )
607
+ resp.raise_for_status()
608
+
609
+ if resp is None:
610
+ raise ValueError("Failed to get response from Gemini API")
611
+
612
+ lines = resp.text.splitlines()
613
+ if len(lines) < 3:
614
+ raise ValueError(
615
+ f"Unexpected response format. Status: {resp.status_code}. Content: {resp.text[:200]}..."
616
+ )
617
+
618
+ chat_data_line = None
619
+ for line in lines:
620
+ if line.startswith(")]}'"):
621
+ chat_data_line = line[4:].strip()
622
+ break
623
+ elif line.startswith("["):
624
+ chat_data_line = line
625
+ break
626
+
627
+ if not chat_data_line:
628
+ chat_data_line = lines[3] if len(lines) > 3 else lines[-1]
629
+ if chat_data_line.startswith(")]}'"):
630
+ chat_data_line = chat_data_line[4:].strip()
631
+
632
+ response_json = json.loads(chat_data_line)
633
+
634
+ body = None
635
+ body_index = 0
636
+
637
+ for part_index, part in enumerate(response_json):
638
+ try:
639
+ if isinstance(part, list) and len(part) > 2:
640
+ main_part = json.loads(part[2])
641
+ if main_part and len(main_part) > 4 and main_part[4]:
642
+ body = main_part
643
+ body_index = part_index
644
+ break
645
+ except (IndexError, TypeError, json.JSONDecodeError):
646
+ continue
647
+
648
+ if not body:
649
+ return self._error_response("Failed to parse response body. No valid data found.")
650
+
651
+ try:
652
+ content = ""
653
+ if len(body) > 4 and len(body[4]) > 0 and len(body[4][0]) > 1:
654
+ content = body[4][0][1][0] if len(body[4][0][1]) > 0 else ""
655
+
656
+ conversation_id = (
657
+ body[1][0] if len(body) > 1 and len(body[1]) > 0 else self.conversation_id
658
+ )
659
+ response_id = body[1][1] if len(body) > 1 and len(body[1]) > 1 else self.response_id
660
+
661
+ factualityQueries = body[3] if len(body) > 3 else None
662
+ textQuery = body[2][0] if len(body) > 2 and body[2] else ""
663
+
664
+ choices = []
665
+ if len(body) > 4:
666
+ for candidate in body[4]:
667
+ if (
668
+ len(candidate) > 1
669
+ and isinstance(candidate[1], list)
670
+ and len(candidate[1]) > 0
671
+ ):
672
+ choices.append({"id": candidate[0], "content": candidate[1][0]})
673
+
674
+ choice_id = choices[0]["id"] if choices else self.choice_id
675
+
676
+ images = []
677
+
678
+ if len(body) > 4 and len(body[4]) > 0 and len(body[4][0]) > 4 and body[4][0][4]:
679
+ for img_data in body[4][0][4]:
680
+ try:
681
+ img_url = img_data[0][0][0]
682
+ img_alt = img_data[2] if len(img_data) > 2 else ""
683
+ img_title = img_data[1] if len(img_data) > 1 else "[Image]"
684
+ images.append({"url": img_url, "alt": img_alt, "title": img_title})
685
+ except (IndexError, TypeError):
686
+ console.log(
687
+ "[yellow]Warning: Could not parse image data structure (format 1).[/yellow]"
688
+ )
689
+ continue
690
+
691
+ generated_images = []
692
+ if len(body) > 4 and len(body[4]) > 0 and len(body[4][0]) > 12 and body[4][0][12]:
693
+ try:
694
+ if body[4][0][12][7] and body[4][0][12][7][0]:
695
+ for img_index, img_data in enumerate(body[4][0][12][7][0]):
696
+ try:
697
+ img_url = img_data[0][3][3]
698
+ img_title = f"[Generated Image {img_index + 1}]"
699
+ img_alt = (
700
+ img_data[3][5][0]
701
+ if len(img_data[3]) > 5 and len(img_data[3][5]) > 0
702
+ else ""
703
+ )
704
+ generated_images.append(
705
+ {"url": img_url, "alt": img_alt, "title": img_title}
706
+ )
707
+ except (IndexError, TypeError):
708
+ continue
709
+
710
+ if not generated_images:
711
+ for part_index, part in enumerate(response_json):
712
+ if part_index <= body_index:
713
+ continue
714
+ try:
715
+ img_part = json.loads(part[2])
716
+ if img_part[4][0][12][7][0]:
717
+ for img_index, img_data in enumerate(
718
+ img_part[4][0][12][7][0]
719
+ ):
720
+ try:
721
+ img_url = img_data[0][3][3]
722
+ img_title = f"[Generated Image {img_index + 1}]"
723
+ img_alt = (
724
+ img_data[3][5][0]
725
+ if len(img_data[3]) > 5
726
+ and len(img_data[3][5]) > 0
727
+ else ""
728
+ )
729
+ generated_images.append(
730
+ {
731
+ "url": img_url,
732
+ "alt": img_alt,
733
+ "title": img_title,
734
+ }
735
+ )
736
+ except (IndexError, TypeError):
737
+ continue
738
+ break
739
+ except (IndexError, TypeError, json.JSONDecodeError):
740
+ continue
741
+ except (IndexError, TypeError):
742
+ pass
743
+
744
+ if len(generated_images) == 0 and len(body) > 4 and len(body[4]) > 0:
745
+ try:
746
+ candidate = body[4][0]
747
+ if len(candidate) > 22 and candidate[22]:
748
+ import re
749
+
750
+ content = (
751
+ candidate[22][0]
752
+ if isinstance(candidate[22], list) and len(candidate[22]) > 0
753
+ else str(candidate[22])
754
+ )
755
+ urls = re.findall(r"https?://[^\s]+", content)
756
+ for i, url in enumerate(urls):
757
+ if url[-1] in [".", ",", ")", "]", "}", '"', "'"]:
758
+ url = url[:-1]
759
+ generated_images.append(
760
+ {"url": url, "title": f"[Generated Image {i + 1}]", "alt": ""}
761
+ )
762
+ except (IndexError, TypeError) as e:
763
+ console.log(
764
+ f"[yellow]Warning: Could not parse alternative image structure: {e}[/yellow]"
765
+ )
766
+
767
+ if len(images) == 0 and len(generated_images) == 0 and content:
768
+ try:
769
+ import re
770
+
771
+ urls = re.findall(
772
+ r"(https?://[^\s]+\.(jpg|jpeg|png|gif|webp))", content.lower()
773
+ )
774
+
775
+ google_urls = re.findall(
776
+ r"(https?://lh\d+\.googleusercontent\.com/[^\s]+)", content
777
+ )
778
+
779
+ general_urls = re.findall(r"(https?://[^\s]+)", content)
780
+
781
+ all_urls = []
782
+ if urls:
783
+ all_urls.extend([url_tuple[0] for url_tuple in urls])
784
+ if google_urls:
785
+ all_urls.extend(google_urls)
786
+
787
+ if not all_urls and general_urls:
788
+ all_urls = general_urls
789
+
790
+ if all_urls:
791
+ for i, url in enumerate(all_urls):
792
+ if url[-1] in [".", ",", ")", "]", "}", '"', "'"]:
793
+ url = url[:-1]
794
+ images.append(
795
+ {"url": url, "title": f"[Image in Content {i + 1}]", "alt": ""}
796
+ )
797
+ console.log(
798
+ f"[green]Found {len(all_urls)} potential image URLs in content.[/green]"
799
+ )
800
+ except Exception as e:
801
+ console.log(
802
+ f"[yellow]Warning: Error extracting URLs from content: {e}[/yellow]"
803
+ )
804
+
805
+ all_images = images + generated_images
806
+
807
+ results: AskResponse = {
808
+ "content": content,
809
+ "conversation_id": conversation_id,
810
+ "response_id": response_id,
811
+ "factualityQueries": factualityQueries,
812
+ "textQuery": textQuery,
813
+ "choices": choices,
814
+ "images": all_images,
815
+ "error": False,
816
+ }
817
+
818
+ self.conversation_id = conversation_id
819
+ self.response_id = response_id
820
+ self.choice_id = choice_id
821
+ self._reqid += random.randint(1000, 9000)
822
+
823
+ return results
824
+
825
+ except (IndexError, TypeError) as e:
826
+ console.log(f"[red]Error extracting data from response: {e}[/red]")
827
+ return self._error_response(f"Error extracting data from response: {e}")
828
+
829
+ except json.JSONDecodeError as e:
830
+ console.log(f"[red]Error parsing JSON response: {e}[/red]")
831
+ resp_text = resp.text[:200] if resp else "No response"
832
+ return self._error_response(
833
+ f"Error parsing JSON response: {e}. Response: {resp_text}..."
834
+ )
835
+ except Timeout as e:
836
+ console.log(f"[red]Request timed out: {e}[/red]")
837
+ return self._error_response(f"Request timed out: {e}")
838
+ except HTTPError as e:
839
+ console.log(f"[red]HTTP error {e.response.status_code}: {e}[/red]")
840
+ return self._error_response(f"HTTP error {e.response.status_code}: {e}")
841
+ except (RequestException, CurlError) as e:
842
+ console.log(f"[red]Network error: {e}[/red]")
843
+ return self._error_response(f"Network error: {e}")
844
+ except Exception as e:
845
+ console.log(
846
+ f"[red]An unexpected error occurred during ask: {e}[/red]", style="bold red"
847
+ )
848
+ return self._error_response(f"An unexpected error occurred: {e}")
849
+
850
+
851
+ class Image(BaseModel):
852
+ """
853
+ Represents a single image object returned from Gemini.
854
+
855
+ Attributes:
856
+ url (str): URL of the image.
857
+ title (str): Title of the image (default: "[Image]").
858
+ alt (str): Optional description of the image.
859
+ proxy (str | dict | None): Proxy used when saving the image.
860
+ impersonate (str): Browser profile for curl_cffi to impersonate.
861
+ """
862
+
863
+ url: str
864
+ title: str = "[Image]"
865
+ alt: str = ""
866
+ proxy: Optional[Union[str, Dict[str, str]]] = None
867
+ impersonate: str = "chrome110"
868
+
869
+ def __str__(self) -> str:
870
+ return f"{self.title}({self.url}) - {self.alt}"
871
+
872
+ def __repr__(self) -> Any:
873
+ short_url = self.url if len(self.url) <= 50 else self.url[:20] + "..." + self.url[-20:]
874
+ short_alt = self.alt[:30] + "..." if len(self.alt) > 30 else self.alt
875
+ return f"Image(title='{self.title}', url='{short_url}', alt='{short_alt}')"
876
+
877
+ async def save(
878
+ self,
879
+ path: str = "downloaded_images",
880
+ filename: Optional[str] = None,
881
+ cookies: Optional[Dict[str, str]] = None,
882
+ verbose: bool = False,
883
+ skip_invalid_filename: bool = True,
884
+ ) -> Optional[str]:
885
+ """
886
+ Save the image to disk using curl_cffi.
887
+ Parameters:
888
+ path: str, optional
889
+ Directory to save the image (default "downloaded_images").
890
+ filename: str, optional
891
+ Filename to use; if not provided, inferred from URL.
892
+ cookies: dict, optional
893
+ Cookies used for the image request.
894
+ verbose: bool, optional
895
+ If True, outputs status messages (default False).
896
+ skip_invalid_filename: bool, optional
897
+ If True, skips saving if the filename is invalid.
898
+ Returns:
899
+ Absolute path of the saved image if successful; None if skipped.
900
+ Raises:
901
+ HTTPError if the network request fails.
902
+ RequestException/CurlError for other network errors.
903
+ IOError if file writing fails.
904
+ """
905
+ if not filename:
906
+ try:
907
+ from urllib.parse import unquote, urlparse
908
+
909
+ parsed_url = urlparse(self.url)
910
+ base_filename = os.path.basename(unquote(parsed_url.path))
911
+ safe_filename = re.sub(r'[<>:"/\\|?*]', "_", base_filename)
912
+ if safe_filename and len(safe_filename) > 0:
913
+ filename = safe_filename
914
+ else:
915
+ filename = f"image_{random.randint(1000, 9999)}.jpg"
916
+ except Exception:
917
+ filename = f"image_{random.randint(1000, 9999)}.jpg"
918
+
919
+ try:
920
+ _ = Path(filename)
921
+ max_len = 255
922
+ if len(filename) > max_len:
923
+ name, ext = os.path.splitext(filename)
924
+ filename = name[: max_len - len(ext) - 1] + ext
925
+ except (OSError, ValueError):
926
+ if verbose:
927
+ console.log(f"[yellow]Invalid filename generated: {filename}[/yellow]")
928
+ if skip_invalid_filename:
929
+ if verbose:
930
+ console.log("[yellow]Skipping save due to invalid filename.[/yellow]")
931
+ return None
932
+ filename = f"image_{random.randint(1000, 9999)}.jpg"
933
+ if verbose:
934
+ console.log(f"[yellow]Using fallback filename: {filename}[/yellow]")
935
+
936
+ proxies_dict = None
937
+ if isinstance(self.proxy, str):
938
+ proxies_dict = {"http": self.proxy, "https": self.proxy}
939
+ elif isinstance(self.proxy, dict):
940
+ proxies_dict = self.proxy
941
+
942
+ dest = None
943
+ try:
944
+ async with AsyncSession(
945
+ cookies=cookies,
946
+ proxies=cast(Any, proxies_dict),
947
+ impersonate=cast(Any, self.impersonate),
948
+ ) as client:
949
+ if verbose:
950
+ console.log(f"Attempting to download image from: {self.url}")
951
+
952
+ response = await client.get(self.url)
953
+ response.raise_for_status()
954
+
955
+ content_type = response.headers.get("content-type", "").lower()
956
+ if "image" not in content_type and verbose:
957
+ console.log(
958
+ f"[yellow]Warning: Content type is '{content_type}', not an image. Saving anyway.[/yellow]"
959
+ )
960
+
961
+ dest_path = Path(path)
962
+ dest_path.mkdir(parents=True, exist_ok=True)
963
+ dest = dest_path / filename
964
+
965
+ dest.write_bytes(response.content)
966
+
967
+ if verbose:
968
+ console.log(f"Image saved successfully as {dest.resolve()}")
969
+
970
+ return str(dest.resolve())
971
+
972
+ except HTTPError as e:
973
+ console.log(
974
+ f"[red]Error downloading image {self.url}: {e.response.status_code} {e}[/red]"
975
+ )
976
+ raise
977
+ except (RequestException, CurlError) as e:
978
+ console.log(f"[red]Network error downloading image {self.url}: {e}[/red]")
979
+ raise
980
+ except IOError as e:
981
+ console.log(f"[red]Error writing image file to {dest}: {e}[/red]")
982
+ raise
983
+ except Exception as e:
984
+ console.log(f"[red]An unexpected error occurred during image save: {e}[/red]")
985
+ raise
986
+
987
+
988
+ class WebImage(Image):
989
+ """
990
+ Represents an image retrieved from web search results.
991
+
992
+ Returned when asking Gemini to "SEND an image of [something]".
993
+ """
994
+
995
+ async def save(
996
+ self,
997
+ path: str = "downloaded_images",
998
+ filename: Optional[str] = None,
999
+ cookies: Optional[Dict[str, str]] = None,
1000
+ verbose: bool = False,
1001
+ skip_invalid_filename: bool = True,
1002
+ ) -> Optional[str]:
1003
+ """
1004
+ Save the image to disk using curl_cffi.
1005
+ Parameters:
1006
+ path: str, optional
1007
+ Directory to save the image (default "downloaded_images").
1008
+ filename: str, optional
1009
+ Filename to use; if not provided, inferred from URL.
1010
+ cookies: dict, optional
1011
+ Cookies used for the image request.
1012
+ verbose: bool, optional
1013
+ If True, outputs status messages (default False).
1014
+ skip_invalid_filename: bool, optional
1015
+ If True, skips saving if the filename is invalid.
1016
+ Returns:
1017
+ Absolute path of the saved image if successful; None if skipped.
1018
+ Raises:
1019
+ HTTPError if the network request fails.
1020
+ RequestException/CurlError for other network errors.
1021
+ IOError if file writing fails.
1022
+ """
1023
+ return await super().save(path, filename, cookies, verbose, skip_invalid_filename)
1024
+
1025
+
1026
+ class GeneratedImage(Image):
1027
+ """
1028
+ Represents an image generated by Google's AI image generator (e.g., ImageFX).
1029
+
1030
+ Attributes:
1031
+ cookies (dict[str, str]): Cookies required for accessing the generated image URL,
1032
+ typically from the GeminiClient/Chatbot instance.
1033
+ """
1034
+
1035
+ cookies: Dict[str, str]
1036
+
1037
+ @field_validator("cookies")
1038
+ @classmethod
1039
+ def validate_cookies(cls, v: Dict[str, str]) -> Dict[str, str]:
1040
+ """Ensures cookies are provided for generated images."""
1041
+ if not v or not isinstance(v, dict):
1042
+ raise ValueError("GeneratedImage requires a dictionary of cookies from the client.")
1043
+ return v
1044
+
1045
+ async def save(
1046
+ self,
1047
+ path: str = "downloaded_images",
1048
+ filename: Optional[str] = None,
1049
+ cookies: Optional[Dict[str, str]] = None,
1050
+ verbose: bool = False,
1051
+ skip_invalid_filename: bool = True,
1052
+ **kwargs,
1053
+ ) -> Optional[str]:
1054
+ """
1055
+ Save the generated image to disk.
1056
+ Parameters:
1057
+ filename: str, optional
1058
+ Filename to use. If not provided, a default name including
1059
+ a timestamp and part of the URL is used. Generated images
1060
+ are often in .png or .jpg format.
1061
+ Additional arguments are passed to Image.save.
1062
+ Returns:
1063
+ Absolute path of the saved image if successful, None if skipped.
1064
+ """
1065
+ if filename is None:
1066
+ ext = ".jpg" if ".jpg" in self.url.lower() else ".png"
1067
+ url_part = self.url.split("/")[-1][:10]
1068
+ filename = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{url_part}{ext}"
1069
+
1070
+ return await super().save(
1071
+ path, filename, cookies or self.cookies, verbose, skip_invalid_filename
1072
+ )