返回提交历史
Modified
g4f/Provider/Perchance.py
+86
-8
XFEstudio/gpt4free
feat: add image generation support and fix headless bypass to Perchance provider
35fac2bd
代码差异
1 个文件
+86
-8
@@ -3,34 +3,47 @@ from __future__ import annotations
3
3
import asyncio
4
4
import random
5
5
import json
6
from aiohttp import ClientSession
6
7
from g4f.requests.cdp import CDPSession
7
8
from ..typing import AsyncResult, Messages
8
9
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
9
from .helper import format_prompt
10
from .helper import format_prompt, format_media_prompt
10
11
from ..requests import StreamSession
12
from ..providers.response import ImageResponse
11
13
12
14
class Perchance(AsyncGeneratorProvider, ProviderModelMixin):
13
15
url = "https://perchance.org/ai-chat"
14
16
api_endpoint = "https://text-generation.perchance.org/api/generate"
17
image_api_endpoint = "https://image-generation.perchance.org/api/generate"
15
18
verify_url = "https://text-generation.perchance.org/embed?thread=0"
16
19
working = True
17
20
supports_stream = True
18
21
supports_system_message = True
19
22
supports_message_history = True
20
23
default_model = "perchance"
24
default_image_model = "perchance-image"
25
image_models = ["perchance-image"]
26
models = [default_model] + image_models
21
27
22
28
# Class-level cache for credentials to avoid restarting browser for every request
23
29
_user_key: str | None = None
24
30
_cookies: dict | None = None
25
31
_headers: dict | None = None
32
33
_image_user_key: str | None = None
34
_image_cookies: dict | None = None
35
_image_headers: dict | None = None
26
36
27
37
@classmethod
28
async def _get_user_key(cls, proxy: str = None) -> tuple[str, dict, dict]:
38
async def _get_user_key(cls, is_image: bool = False, proxy: str = None) -> tuple[str, dict, dict]:
29
39
"""Runs CDP to solve Turnstile and extract the userKey and session cookies."""
30
session = CDPSession(headless=True)
40
session = CDPSession(headless=False)
31
41
await session.start()
32
42
try:
33
await session.navigate(cls.verify_url)
43
verify_url = "https://image-generation.perchance.org/embed" if is_image else cls.verify_url
44
origin = "https://image-generation.perchance.org" if is_image else "https://text-generation.perchance.org"
45
46
await session.navigate(verify_url)
34
47
35
48
# Setup message capture in page context
36
49
setup_js = """
@@ -66,19 +79,30 @@ class Perchance(AsyncGeneratorProvider, ProviderModelMixin):
66
79
headers = {
67
80
"user-agent": user_agent,
68
81
"Accept": "*/*",
69
"Origin": "https://text-generation.perchance.org",
70
"Referer": "https://text-generation.perchance.org/embed",
82
"Origin": origin,
83
"Referer": verify_url,
71
84
}
72
85
73
86
return user_key, cookies, headers
74
87
finally:
75
88
await session.close()
76
89
90
@classmethod
91
async def get_ad_access_code(cls, proxy: str = None) -> str:
92
cache_bust = random.randint(1000000, 9999999)
93
url = f"https://perchance.org/api/getAccessCodeForAdPoweredStuff?__cacheBust={cache_bust}"
94
async with ClientSession() as session:
95
async with session.get(url, proxy=proxy) as response:
96
if response.status == 200:
97
return await response.text()
98
return ""
99
77
100
@classmethod
78
101
async def create_async_generator(
79
102
cls,
80
103
model: str,
81
104
messages: Messages,
105
prompt: str = None,
82
106
proxy: str = None,
83
107
**kwargs
84
108
) -> AsyncResult:
@@ -86,7 +110,61 @@ class Perchance(AsyncGeneratorProvider, ProviderModelMixin):
86
110
if not cls._user_key or not cls._cookies or not cls._headers:
87
111
cls._user_key, cls._cookies, cls._headers = await cls._get_user_key(proxy=proxy)
88
112
89
# Approximate token count (chars divided by ~4)
113
# Image Generation Flow
114
if model in cls.image_models or kwargs.get("image"):
115
if not cls._image_user_key or not cls._image_cookies or not cls._image_headers:
116
cls._image_user_key, cls._image_cookies, cls._image_headers = await cls._get_user_key(is_image=True, proxy=proxy)
117
118
if not prompt:
119
prompt = format_media_prompt(messages)
120
121
ad_access_code = await cls.get_ad_access_code(proxy=proxy)
122
req_id = random.random()
123
124
payload = {
125
"prompt": prompt,
126
"negativePrompt": "",
127
"seed": -1,
128
"resolution": "512x512",
129
"guidanceScale": 7,
130
"channel": "text-to-image-plugin-example",
131
"subChannel": "public",
132
"userKey": cls._image_user_key,
133
"adAccessCode": ad_access_code,
134
"requestId": str(req_id)
135
}
136
137
gen_url = f"{cls.image_api_endpoint}?userKey={cls._image_user_key}&requestId={req_id}&adAccessCode={ad_access_code}&__cacheBust={random.random()}"
138
139
async with StreamSession(
140
headers=cls._image_headers,
141
cookies=cls._image_cookies,
142
proxies={"all": proxy} if proxy else None,
143
impersonate="chrome"
144
) as session:
145
async with session.post(gen_url, json=payload, proxy=proxy) as response:
146
if response.status in (401, 403):
147
cls._image_user_key = cls._image_cookies = cls._image_headers = None
148
raise RuntimeError("auth_failed")
149
if response.status != 200:
150
text = await response.text()
151
if "invalid_key" in text or "failed_verification" in text:
152
cls._image_user_key = cls._image_cookies = cls._image_headers = None
153
raise RuntimeError("auth_failed")
154
raise RuntimeError(f"image generation failed: {response.status} {text}")
155
156
data = await response.json()
157
if data.get("status") == "success":
158
image_url = data.get("imageDownloadUrl")
159
if image_url:
160
if not image_url.startswith("http"):
161
image_url = "https://image-generation.perchance.org" + image_url
162
yield ImageResponse(image_url, prompt)
163
return
164
raise RuntimeError(f"Failed to generate image: {data}")
165
return
166
167
# Text Generation Flow
90
168
instruction = format_prompt(messages)
91
169
instruction_token_count = max(1, len(instruction) // 4)
92
170
@@ -139,7 +217,7 @@ class Perchance(AsyncGeneratorProvider, ProviderModelMixin):
139
217
if str(e) == "auth_failed":
140
218
# Clear cached credentials and retry once with fresh verification
141
219
cls._user_key = cls._cookies = cls._headers = None
142
async for chunk in cls.create_async_generator(model, messages, proxy, **kwargs):
220
async for chunk in cls.create_async_generator(model, messages, prompt, proxy, **kwargs):
143
221
yield chunk
144
222
else:
145
223
raise e