XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

feat: improve media handling, file conversion, and error management

- Added UUID-based "x-xai-request-id" header and 403 error handling in Grok.py - Updated backend_api.py to handle empty media results and unsupported file types with error raising and file cleanup - Simplified render logic in website.py by removing is_live flag and related code - Changed "audio/wav" MIME type to "audio/x-wav" in image/__init__.py - Added is_valid_media and is_valid_audio functions to image/__init__.py for stricter media validation - Enhanced MarkItDown integration in markitdown/__init__.py with convert_stream method supporting non-seekable streams - Modified _transcribe_audio.py to use recognize_faster_whisper if available, fallback to recognize_google - Updated providers/helper.py to prioritize "text" key in to_string function - Improved stream_read_files in files.py to skip DOWNLOADS_FILE and adjust code block formatting - Added get_filename_from_url utility in files.py for consistent filename generation from URLs - Enhanced download_urls in files.py to use MarkItDown for URL conversion and improved error logging - Improved render_part and related functions in media.py to use new media validation logic and handle more cases - Adjusted merge_media and render_messages in media.py for stricter part filtering and validation

94619495
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

9 个文件 +140 -36
Modified g4f/Provider/needs_auth/Grok.py +7 -2
@@ -4,6 +4,7 @@ import os
4 4 import json
5 5 import time
6 6 import asyncio
7 import uuid
7 8 from typing import Dict, Any, AsyncIterator
8 9
9 10 try:
@@ -13,8 +14,9 @@ except ImportError:
13 14
14 15 from ...typing import Messages, AsyncResult
15 16 from ...providers.response import JsonConversation, Reasoning, ImagePreview, ImageResponse, TitleGeneration, AuthResult, RequestLogin
16 from ...requests import StreamSession, get_nodriver, DEFAULT_HEADERS
17 from ...requests import StreamSession, get_nodriver, DEFAULT_HEADERS, merge_cookies
17 18 from ...requests.raise_for_status import raise_for_status
19 from ...errors import MissingAuthError
18 20 from ..base_provider import AsyncAuthedProvider, ProviderModelMixin
19 21 from ..helper import format_prompt, get_last_user_message
20 22
@@ -112,7 +114,10 @@ class Grok(AsyncAuthedProvider, ProviderModelMixin):
112 114 url = f"{cls.conversation_url}/new"
113 115 else:
114 116 url = f"{cls.conversation_url}/{conversation_id}/responses"
115 async with session.post(url, json=payload) as response:
117 async with session.post(url, json=payload, headers={"x-xai-request-id": str(uuid.uuid4())}) as response:
118 if response.status == 403:
119 raise MissingAuthError("Invalid secrets")
120 auth_result.cookies = merge_cookies(auth_result.cookies, response)
116 121 await raise_for_status(response)
117 122 thinking_duration = None
118 123 async for line in response.iter_lines():
Modified g4f/gui/server/backend_api.py +8 -3
@@ -19,7 +19,6 @@ try:
19 19 from ...integration.markitdown import MarkItDown, StreamInfo
20 20 has_markitdown = True
21 21 except ImportError as e:
22 print(e)
23 22 has_markitdown = False
24 23
25 24 from ...client.service import convert_to_provider
@@ -367,10 +366,16 @@ class Backend_Api(Api):
367 366 if is_media:
368 367 os.makedirs(media_dir, exist_ok=True)
369 368 newfile = os.path.join(media_dir, filename)
370 media.append({"name": filename, "text": result})
371 elif not result and is_supported:
369 if result:
370 media.append({"name": filename, "text": result})
371 else:
372 media.append({"name": filename})
373 elif is_supported:
372 374 newfile = os.path.join(bucket_dir, filename)
373 375 filenames.append(filename)
376 else:
377 os.remove(copyfile)
378 raise ValueError(f"Unsupported file type: {filename}")
374 379 try:
375 380 os.rename(copyfile, newfile)
376 381 except OSError:
Modified g4f/gui/server/website.py +3 -6
@@ -14,9 +14,7 @@ def redirect_home():
14 14 return redirect('/chat')
15 15
16 16 def render(filename = "chat"):
17 is_live = True
18 17 if os.path.exists(DIST_DIR):
19 is_live = False
20 18 path = os.path.abspath(os.path.join(os.path.dirname(DIST_DIR), (filename + ("" if "." in filename else ".html"))))
21 19 return send_from_directory(os.path.dirname(path), os.path.basename(path))
22 20 try:
@@ -25,13 +23,12 @@ def render(filename = "chat"):
25 23 latest_version = version.utils.current_version
26 24 today = datetime.today().strftime('%Y-%m-%d')
27 25 cache_dir = os.path.join(get_cookies_dir(), ".gui_cache")
28 cache_file = os.path.join(cache_dir, f"{today}.{secure_filename(f'{filename}.{version.utils.current_version}-{latest_version}')}{'.live' if is_live else ''}.html")
26 cache_file = os.path.join(cache_dir, f"{today}.{secure_filename(f'{filename}.{version.utils.current_version}-{latest_version}')}.html")
29 27 if not os.path.exists(cache_file):
30 28 os.makedirs(cache_dir, exist_ok=True)
31 29 html = requests.get(f"{STATIC_URL}{filename}.html").text
32 if is_live:
33 html = html.replace("../dist/", f"dist/")
34 html = html.replace("\"dist/", f"\"{STATIC_URL}dist/")
30 html = html.replace("../dist/", f"dist/")
31 html = html.replace("\"dist/", f"\"{STATIC_URL}dist/")
35 32 with open(cache_file, 'w', encoding='utf-8') as f:
36 33 f.write(html)
37 34 return send_from_directory(os.path.abspath(cache_dir), os.path.basename(cache_file))
Modified g4f/image/__init__.py +23 -2
@@ -25,7 +25,7 @@ EXTENSIONS_MAP: dict[str, str] = {
25 25 "gif": "image/gif",
26 26 "webp": "image/webp",
27 27 # Audio
28 "wav": "audio/wav",
28 "wav": "audio/x-wav",
29 29 "mp3": "audio/mpeg",
30 30 "flac": "audio/flac",
31 31 "opus": "audio/opus",
@@ -107,18 +107,39 @@ def is_data_an_media(data, filename: str = None) -> str:
107 107 return is_accepted_format(data)
108 108 return is_data_uri_an_image(data)
109 109
110 def is_valid_media(data, filename: str = None) -> str:
111 if is_valid_audio(data, filename):
112 return True
113 if filename:
114 extension = get_extension(filename)
115 if extension is not None:
116 media_type = EXTENSIONS_MAP[extension]
117 if media_type.startswith("image/"):
118 return media_type
119 if isinstance(data, bytes):
120 return is_accepted_format(data)
121 return is_data_uri_an_image(data)
122
110 123 def is_data_an_audio(data_uri: str = None, filename: str = None) -> str:
111 124 if filename:
112 125 extension = get_extension(filename)
113 126 if extension is not None:
114 127 media_type = EXTENSIONS_MAP[extension]
115 if media_type.startswith("audio/") or media_type == "video/webm":
128 if media_type.startswith("audio/"):
116 129 return media_type
117 130 if isinstance(data_uri, str):
118 131 audio_format = re.match(r'^data:(audio/\w+);base64,', data_uri)
119 132 if audio_format:
120 133 return audio_format.group(1)
121 134
135 def is_valid_audio(data_uri: str = None, filename: str = None) -> bool:
136 mimetype = is_data_an_audio(data_uri, filename)
137 if mimetype is None:
138 return False
139 if MEDIA_TYPE_MAP.get(mimetype) not in ("wav", "mp3"):
140 return False
141 return True
142
122 143 def is_data_uri_an_image(data_uri: str) -> bool:
123 144 """
124 145 Checks if the given data URI represents an image.
Modified g4f/integration/markitdown/__init__.py +50 -2
@@ -1,6 +1,7 @@
1 1 import re
2 2 import sys
3 from typing import List, Union, BinaryIO
3 import io
4 from typing import List, Union, BinaryIO, Optional, Any
4 5 from markitdown import MarkItDown as BaseMarkItDown
5 6 from markitdown._stream_info import StreamInfo
6 7 from markitdown._base_converter import DocumentConverterResult
@@ -117,4 +118,51 @@ class MarkItDown(BaseMarkItDown):
117 118 # Nothing can handle it!
118 119 raise UnsupportedFormatException(
119 120 f"Could not convert stream to Markdown. No converter attempted a conversion, suggesting that the filetype is simply not supported."
120 )
121 )
122
123 def convert_stream(
124 self,
125 stream: BinaryIO,
126 *,
127 stream_info: Optional[StreamInfo] = None,
128 file_extension: Optional[str] = None, # Deprecated -- use stream_info
129 url: Optional[str] = None, # Deprecated -- use stream_info
130 **kwargs: Any,
131 ) -> DocumentConverterResult:
132 guesses: List[StreamInfo] = []
133
134 # Do we have anything on which to base a guess?
135 base_guess = None
136 if stream_info is not None or file_extension is not None or url is not None:
137 # Start with a non-Null base guess
138 if stream_info is None:
139 base_guess = StreamInfo()
140 else:
141 base_guess = stream_info
142
143 if file_extension is not None:
144 # Deprecated -- use stream_info
145 assert base_guess is not None # for mypy
146 base_guess = base_guess.copy_and_update(extension=file_extension)
147
148 if url is not None:
149 # Deprecated -- use stream_info
150 assert base_guess is not None # for mypy
151 base_guess = base_guess.copy_and_update(url=url)
152
153 # Check if we have a seekable stream. If not, load the entire stream into memory.
154 if not hasattr(stream, "seekable") or not stream.seekable():
155 buffer = io.BytesIO()
156 while True:
157 chunk = stream.read(4096)
158 if not chunk:
159 break
160 buffer.write(chunk)
161 buffer.seek(0)
162 stream = buffer
163
164 # Add guesses based on stream content
165 guesses = self._get_stream_info_guesses(
166 file_stream=stream, base_guess=base_guess or StreamInfo()
167 )
168 return self._convert(file_stream=stream, stream_info_guesses=guesses, **kwargs)
Modified g4f/integration/markitdown/_transcribe_audio.py +4 -1
@@ -47,5 +47,8 @@ def transcribe_audio(file_stream: BinaryIO, *, audio_format: str = "wav", langua
47 47 audio = recognizer.record(source)
48 48 if language is None:
49 49 language = "en-US"
50 transcript = recognizer.recognize_google(audio, language=language).strip()
50 try:
51 transcript = recognizer.recognize_faster_whisper(audio, language=language.split("-")[0]).strip()
52 except ImportError:
53 transcript = recognizer.recognize_google(audio, language=language).strip()
51 54 return "[No speech detected]" if transcript == "" else transcript.strip()
Modified g4f/providers/helper.py +3 -3
@@ -12,13 +12,13 @@ def to_string(value) -> str:
12 12 if isinstance(value, str):
13 13 return value
14 14 elif isinstance(value, dict):
15 if "name" in value:
15 if "text" in value:
16 return value["text"]
17 elif "name" in value:
16 18 return ""
17 19 elif "bucket_id" in value:
18 20 bucket_dir = Path(get_bucket_dir(value.get("bucket_id")))
19 21 return "".join(read_bucket(bucket_dir))
20 elif value.get("type") == "text":
21 return value.get("text")
22 22 return ""
23 23 elif isinstance(value, list):
24 24 return "".join([to_string(v) for v in value if v.get("type", "text") == "text"])
Modified g4f/tools/files.py +30 -8
@@ -69,6 +69,11 @@ try:
69 69 has_beautifulsoup4 = True
70 70 except ImportError:
71 71 has_beautifulsoup4 = False
72 try:
73 from markitdown import MarkItDown
74 has_markitdown = True
75 except ImportError:
76 has_markitdown = False
72 77
73 78 from .web_search import scrape_text
74 79 from ..cookies import get_cookies_dir
@@ -169,8 +174,10 @@ def get_filenames(bucket_dir: Path):
169 174 return [filename.strip() for filename in f.readlines()]
170 175 return []
171 176
172 def stream_read_files(bucket_dir: Path, filenames: list, delete_files: bool = False) -> Iterator[str]:
177 def stream_read_files(bucket_dir: Path, filenames: list[str], delete_files: bool = False) -> Iterator[str]:
173 178 for filename in filenames:
179 if filename.startswith(DOWNLOADS_FILE):
180 continue
174 181 file_path: Path = bucket_dir / filename
175 182 if not file_path.exists() or file_path.lstat().st_size <= 0:
176 183 continue
@@ -192,7 +199,7 @@ def stream_read_files(bucket_dir: Path, filenames: list, delete_files: bool = Fa
192 199 else:
193 200 os.unlink(filepath)
194 201 continue
195 yield f"```{filename.replace('.md', '')}\n"
202 yield f"```{filename}\n"
196 203 if has_pypdf2 and filename.endswith(".pdf"):
197 204 try:
198 205 reader = PyPDF2.PdfReader(file_path)
@@ -339,6 +346,13 @@ def split_file_by_size_and_newline(input_filename, output_dir, chunk_size_bytes=
339 346 with open(output_filename, 'w', encoding='utf-8') as outfile:
340 347 outfile.write(current_chunk)
341 348
349 def get_filename_from_url(url: str) -> str:
350 parsed_url = urllib.parse.urlparse(url)
351 sha256_hash = hashlib.sha256(url.encode()).digest()
352 base32_encoded = base64.b32encode(sha256_hash).decode()
353 url_hash = base32_encoded[:24].lower()
354 return f"{parsed_url.netloc}+{parsed_url.path[1:].replace('/', '_')}+{url_hash}.md"
355
342 356 async def get_filename(response: ClientResponse) -> str:
343 357 """
344 358 Attempts to extract a filename from an aiohttp response. Prioritizes Content-Disposition, then URL.
@@ -364,11 +378,7 @@ async def get_filename(response: ClientResponse) -> str:
364 378 if content_type and url:
365 379 extension = await get_file_extension(response)
366 380 if extension:
367 parsed_url = urllib.parse.urlparse(url)
368 sha256_hash = hashlib.sha256(url.encode()).digest()
369 base32_encoded = base64.b32encode(sha256_hash).decode()
370 url_hash = base32_encoded[:24].lower()
371 return f"{parsed_url.netloc}+{parsed_url.path[1:].replace('/', '_')}+{url_hash}{extension}"
381 return get_filename_from_url(url)
372 382
373 383 return None
374 384
@@ -442,17 +452,29 @@ async def download_urls(
442 452 ) -> AsyncIterator[str]:
443 453 if lock is None:
444 454 lock = asyncio.Lock()
455 md = MarkItDown()
445 456 async with ClientSession(
446 457 connector=get_connector(proxy=proxy),
447 458 timeout=ClientTimeout(timeout)
448 459 ) as session:
449 460 async def download_url(url: str, max_depth: int) -> str:
461 text_content = None
462 if has_markitdown:
463 try:
464 text_content = md.convert(url).text_content
465 if text_content:
466 filename = get_filename_from_url(url)
467 target = bucket_dir / filename
468 target.write_text(text_content, errors="replace")
469 return filename
470 except Exception as e:
471 debug.log(f"Failed to convert URL to text: {type(e).__name__}: {e}")
450 472 try:
451 473 async with session.get(url) as response:
452 474 response.raise_for_status()
453 475 filename = await get_filename(response)
454 476 if not filename:
455 print(f"Failed to get filename for {url}")
477 debug.log(f"Failed to get filename for {url}")
456 478 return None
457 479 if not is_allowed_extension(filename) and not supports_filename(filename) or filename == DOWNLOADS_FILE:
458 480 return None
Modified g4f/tools/media.py +12 -9
@@ -6,7 +6,7 @@ from typing import Iterator, Union
6 6 from pathlib import Path
7 7
8 8 from ..typing import Messages
9 from ..image import is_data_an_media, is_data_an_audio, to_input_audio, to_data_uri
9 from ..image import is_data_an_media, to_input_audio, is_valid_media, is_valid_audio, to_data_uri
10 10 from .files import get_bucket_dir, read_bucket
11 11
12 12 def render_media(bucket_id: str, name: str, url: str, as_path: bool = False, as_base64: bool = False) -> Union[str, Path]:
@@ -37,7 +37,7 @@ def render_part(part: dict) -> dict:
37 37 "type": "text",
38 38 "text": "".join(read_bucket(bucket_dir))
39 39 }
40 if is_data_an_audio(filename=filename):
40 if is_valid_audio(filename=filename):
41 41 return {
42 42 "type": "input_audio",
43 43 "input_audio": {
@@ -45,10 +45,11 @@ def render_part(part: dict) -> dict:
45 45 "format": os.path.splitext(filename)[1][1:]
46 46 }
47 47 }
48 return {
49 "type": "image_url",
50 "image_url": {"url": render_media(**part)}
51 }
48 if is_valid_media(filename=filename):
49 return {
50 "type": "image_url",
51 "image_url": {"url": render_media(**part)}
52 }
52 53
53 54 def merge_media(media: list, messages: list) -> Iterator:
54 55 buffer = []
@@ -57,7 +58,7 @@ def merge_media(media: list, messages: list) -> Iterator:
57 58 content = message.get("content")
58 59 if isinstance(content, list):
59 60 for part in content:
60 if "type" not in part and "name" in part:
61 if "type" not in part and "name" in part and "text" not in part:
61 62 path = render_media(**part, as_path=True)
62 63 buffer.append((path, os.path.basename(path)))
63 64 elif part.get("type") == "image_url":
@@ -71,9 +72,10 @@ def merge_media(media: list, messages: list) -> Iterator:
71 72 def render_messages(messages: Messages, media: list = None) -> Iterator:
72 73 for idx, message in enumerate(messages):
73 74 if isinstance(message["content"], list):
75 parts = [render_part(part) for part in message["content"] if part]
74 76 yield {
75 77 **message,
76 "content": [render_part(part) for part in message["content"] if part]
78 "content": [part for part in parts if part]
77 79 }
78 80 else:
79 81 if media is not None and idx == len(messages) - 1:
@@ -84,11 +86,12 @@ def render_messages(messages: Messages, media: list = None) -> Iterator:
84 86 "type": "input_audio",
85 87 "input_audio": to_input_audio(media_data, filename)
86 88 }
87 if is_data_an_audio(media_data, filename) else {
89 if is_valid_audio(media_data, filename) else {
88 90 "type": "image_url",
89 91 "image_url": {"url": to_data_uri(media_data)}
90 92 }
91 93 for media_data, filename in media
94 if is_valid_media(media_data, filename)
92 95 ] + ([{"type": "text", "text": message["content"]}] if isinstance(message["content"], str) else message["content"])
93 96 }
94 97 else: