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

XFEstudio/gpt4free

feat: add audio generation support for multiple providers

- Added new examples for `client.media.generate` with `PollinationsAI`, `EdgeTTS`, and `Gemini` in `docs/media.md` - Modified `PollinationsAI.py` to default to `default_audio_model` when audio data is present - Adjusted `PollinationsAI.py` to conditionally construct message list from `prompt` when media is being generated - Rearranged `PollinationsAI.py` response handling to yield `save_response_media` after checking for non-JSON content types - Added support in `EdgeTTS.py` to use default values for `language`, `locale`, and `format` from class attributes - Improved voice selection logic in `EdgeTTS.py` to fallback to default locale or language when not explicitly provided - Updated `EdgeTTS.py` to yield `AudioResponse` with `text` field included - Modified `Gemini.py` to support `.ogx` audio generation when `model == "gemini-audio"` or `audio` is passed - Used `format_image_prompt` in `Gemini.py` to create audio prompt and saved audio file using `synthesize` - Appended `AudioResponse` to `Gemini.py` for audio generation flow - Added `save()` method to `Image` class in `stubs.py` to support saving `/media/` files locally - Changed `client/__init__.py` to fallback to `options["text"]` if `alt` is missing in `Images.create` - Ensured `AudioResponse` in `copy_images.py` includes the `text` (prompt) field - Added `Annotated` fallback definition in `api/__init__.py` for compatibility with older Python versions

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

代码差异

8 个文件 +90 -34
Modified docs/media.md +24 -0
@@ -28,6 +28,30 @@ async def main():
28 28 asyncio.run(main())
29 29 ```
30 30
31 #### **More examples for Generate Audio:**
32
33 ```python
34 from g4f.client import Client
35
36 from g4f.Provider import EdgeTTS, Gemini, PollinationsAI
37
38 client = Client(provider=PollinationsAI)
39 response = client.media.generate("Hello", audio={"voice": "alloy", "format": "mp3"})
40 response.data[0].save("openai.mp3")
41
42 client = Client(provider=PollinationsAI)
43 response = client.media.generate("Hello", model="hypnosis-tracy")
44 response.data[0].save("hypnosis.mp3")
45
46 client = Client(provider=Gemini)
47 response = client.media.generate("Hello", model="gemini-audio")
48 response.data[0].save("gemini.ogx")
49
50 client = Client(provider=EdgeTTS)
51 response = client.media.generate("Hello", audio={"locale": "en-US"})
52 response.data[0].save("edge-tts.mp3")
53 ```
54
31 55 #### **Transcribe an Audio File:**
32 56
33 57 Some providers in G4F support audio inputs in chat completions, allowing you to transcribe audio files by instructing the model accordingly. This example demonstrates how to use the `AsyncClient` to transcribe an audio file asynchronously:
Modified g4f/Provider/PollinationsAI.py +26 -20
@@ -177,7 +177,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
177 177 if is_data_an_audio(media_data, filename):
178 178 has_audio = True
179 179 break
180 model = next(iter(cls.audio_models)) if has_audio else model
180 model = cls.default_audio_model if has_audio else model
181 181 try:
182 182 model = cls.get_model(model)
183 183 except ModelNotFoundError:
@@ -202,6 +202,11 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
202 202 ):
203 203 yield chunk
204 204 else:
205 if prompt is not None and len(messages) == 1:
206 messages = [{
207 "role": "user",
208 "content": prompt
209 }]
205 210 async for result in cls._generate_text(
206 211 model=model,
207 212 messages=messages,
@@ -315,9 +320,6 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
315 320 })
316 321 async with session.post(url, json=data) as response:
317 322 await raise_for_status(response)
318 async for chunk in save_response_media(response, format_image_prompt(messages), [model]):
319 yield chunk
320 return
321 323 if response.headers["content-type"].startswith("text/plain"):
322 324 yield await response.text()
323 325 return
@@ -339,20 +341,24 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
339 341 finish_reason = choice.get("finish_reason")
340 342 if finish_reason:
341 343 yield FinishReason(finish_reason)
342 return
343 result = await response.json()
344 if "choices" in result:
345 choice = result["choices"][0]
346 message = choice.get("message", {})
347 content = message.get("content", "")
348 if content:
349 yield content
350 if "tool_calls" in message:
351 yield ToolCalls(message["tool_calls"])
344 elif response.headers["content-type"].startswith("application/json"):
345 result = await response.json()
346 if "choices" in result:
347 choice = result["choices"][0]
348 message = choice.get("message", {})
349 content = message.get("content", "")
350 if content:
351 yield content
352 if "tool_calls" in message:
353 yield ToolCalls(message["tool_calls"])
354 else:
355 raise ResponseError(result)
356 if result.get("usage") is not None:
357 yield Usage(**result["usage"])
358 finish_reason = choice.get("finish_reason")
359 if finish_reason:
360 yield FinishReason(finish_reason)
352 361 else:
353 raise ResponseError(result)
354 if result.get("usage") is not None:
355 yield Usage(**result["usage"])
356 finish_reason = choice.get("finish_reason")
357 if finish_reason:
358 yield FinishReason(finish_reason)
362 async for chunk in save_response_media(response, format_image_prompt(messages), [model]):
363 yield chunk
364 return
Modified g4f/Provider/audio/EdgeTTS.py +10 -9
@@ -20,8 +20,9 @@ from ..helper import format_image_prompt
20 20 class EdgeTTS(AsyncGeneratorProvider, ProviderModelMixin):
21 21 label = "Edge TTS"
22 22 working = has_edge_tts
23 default_model = "edge-tts"
23 default_language = "en"
24 24 default_locale = "en-US"
25 default_format = "mp3"
25 26
26 27 @classmethod
27 28 def get_models(cls) -> list[str]:
@@ -38,29 +39,29 @@ class EdgeTTS(AsyncGeneratorProvider, ProviderModelMixin):
38 39 messages: Messages,
39 40 proxy: str = None,
40 41 prompt: str = None,
41 audio: dict = {"voice": None, "format": "mp3"},
42 audio: dict = {},
42 43 **kwargs
43 44 ) -> AsyncResult:
44 45 prompt = format_image_prompt(messages, prompt)
45 46 if not prompt:
46 47 raise ValueError("Prompt is empty.")
47 voice = audio.get("voice", model)
48 voice = audio.get("voice", model if model and model != "edge-tts" else None)
48 49 if not voice:
49 50 voices = await VoicesManager.create()
50 51 if "locale" in audio:
51 52 voices = voices.find(Locale=audio["locale"])
52 elif "language" in audio:
53 if "-" in audio["language"]:
54 voices = voices.find(Locale=audio["language"])
53 elif audio.get("language", cls.default_language) != cls.default_language:
54 if "-" in audio.get("language"):
55 voices = voices.find(Locale=audio.get("language"))
55 56 else:
56 voices = voices.find(Language=audio["language"])
57 voices = voices.find(Language=audio.get("language"))
57 58 else:
58 59 voices = voices.find(Locale=cls.default_locale)
59 60 if not voices:
60 61 raise ValueError(f"No voices found for language '{audio.get('language')}' and locale '{audio.get('locale')}'.")
61 62 voice = random.choice(voices)["Name"]
62 63
63 format = audio.get("format", "mp3")
64 format = audio.get("format", cls.default_format)
64 65 filename = get_filename([cls.default_model], prompt, f".{format}", prompt)
65 66 target_path = os.path.join(get_media_dir(), filename)
66 67 ensure_media_dir()
@@ -69,4 +70,4 @@ class EdgeTTS(AsyncGeneratorProvider, ProviderModelMixin):
69 70 communicate = edge_tts.Communicate(prompt, voice=voice, proxy=proxy, **extra_parameters)
70 71
71 72 await communicate.save(target_path)
72 yield AudioResponse(f"/media/{filename}", voice=voice, prompt=prompt)
73 yield AudioResponse(f"/media/{filename}", voice=voice, text=prompt)
Modified g4f/Provider/needs_auth/Gemini.py +16 -2
@@ -20,16 +20,17 @@ except ImportError:
20 20
21 21 from ... import debug
22 22 from ...typing import Messages, Cookies, MediaListType, AsyncResult, AsyncIterator
23 from ...providers.response import JsonConversation, Reasoning, RequestLogin, ImageResponse, YouTube
23 from ...providers.response import JsonConversation, Reasoning, RequestLogin, ImageResponse, YouTube, AudioResponse
24 24 from ...requests.raise_for_status import raise_for_status
25 25 from ...requests.aiohttp import get_connector
26 26 from ...requests import get_nodriver
27 from ...image.copy_images import get_filename, get_media_dir, ensure_media_dir
27 28 from ...errors import MissingAuthError
28 29 from ...image import to_bytes
29 30 from ...cookies import get_cookies_dir
30 31 from ...tools.media import merge_media
31 32 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
32 from ..helper import format_prompt, get_cookies, get_last_user_message
33 from ..helper import format_prompt, get_cookies, get_last_user_message, format_image_prompt
33 34 from ... import debug
34 35
35 36 REQUEST_HEADERS = {
@@ -68,6 +69,7 @@ models = {
68 69 "gemini-2.0-flash-exp": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f299729663a2343f"]'},
69 70 "gemini-2.0-flash-thinking": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"9c17b1863f581b8a"]'},
70 71 "gemini-2.0-flash-thinking-with-apps": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f8f8f5ea629f5d37"]'},
72 "gemini-audio": {}
71 73 }
72 74
73 75 class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
@@ -153,8 +155,20 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
153 155 return_conversation: bool = False,
154 156 conversation: Conversation = None,
155 157 language: str = "en",
158 prompt: str = None,
159 audio: dict = None,
156 160 **kwargs
157 161 ) -> AsyncResult:
162 if audio is not None or model == "gemini-audio":
163 prompt = format_image_prompt(messages, prompt)
164 filename = get_filename(["gemini"], prompt, ".ogx", prompt)
165 ensure_media_dir()
166 path = os.path.join(get_media_dir(), filename)
167 with open(path, "wb") as f:
168 async for chunk in cls.synthesize({"text": prompt}, proxy):
169 f.write(chunk)
170 yield AudioResponse(f"/media/{filename}", text=prompt)
171 return
158 172 cls._cookies = cookies or cls._cookies or get_cookies(GOOGLE_COOKIE_DOMAIN, False, True)
159 173 if conversation is not None and getattr(conversation, "model", None) != model:
160 174 conversation = None
Modified g4f/api/__init__.py +7 -1
@@ -33,6 +33,12 @@ from starlette.responses import FileResponse
33 33 from types import SimpleNamespace
34 34 from typing import Union, Optional, List
35 35
36 try:
37 from typing import Annotated
38 except ImportError:
39 class Annotated:
40 pass
41
36 42 import g4f
37 43 import g4f.Provider
38 44 import g4f.debug
@@ -52,7 +58,7 @@ from .stubs import (
52 58 ChatCompletionsConfig, ImageGenerationConfig,
53 59 ProviderResponseModel, ModelResponseModel,
54 60 ErrorResponseModel, ProviderResponseDetailModel,
55 FileResponseModel, UploadResponseModel, Annotated
61 FileResponseModel, UploadResponseModel
56 62 )
57 63 from g4f import debug
58 64
Modified g4f/client/__init__.py +2 -1
@@ -463,7 +463,8 @@ class Images:
463 463 urls.extend(item.urls)
464 464 if not urls:
465 465 return None
466 return MediaResponse(urls, items[0].alt, items[0].options)
466 alt = getattr(items[0], "alt", items[0].options.get("text"))
467 return MediaResponse(urls, alt, items[0].options)
467 468
468 469 def create_variation(
469 470 self,
Modified g4f/client/stubs.py +4 -0
@@ -217,6 +217,10 @@ class Image(BaseModel):
217 217 revised_prompt=revised_prompt
218 218 ))
219 219
220 def save(self, path: str):
221 if self.url is not None and self.url.startswith("/media/"):
222 os.rename(self.url.replace("/media", get_media_dir()), path)
223
220 224 class ImagesResponse(BaseModel):
221 225 data: List[Image]
222 226 model: str
Modified g4f/image/copy_images.py +1 -1
@@ -70,7 +70,7 @@ async def save_response_media(response: StreamResponse, prompt: str, tags: list[
70 70 if response.method == "GET":
71 71 media_url = f"{media_url}?url={str(response.url)}"
72 72 if content_type.startswith("audio/"):
73 yield AudioResponse(media_url)
73 yield AudioResponse(media_url, text=prompt)
74 74 elif content_type.startswith("video/"):
75 75 yield VideoResponse(media_url, prompt)
76 76 else: