返回提交历史
Modified
g4f/Provider/template/OpenaiTemplate.py
+1
-4
Modified
g4f/api/__init__.py
+7
-2
Modified
g4f/api/stubs.py
+2
-1
Modified
g4f/client/stubs.py
+23
-8
Modified
g4f/image/copy_images.py
+5
-4
XFEstudio/gpt4free
Fix audio response in API
9868a881
代码差异
5 个文件
+38
-19
@@ -166,11 +166,8 @@ async def read_response(response: StreamResponse, stream: bool, prompt: str, pro
166
166
audio = message.get("audio", {})
167
167
if "data" in audio:
168
168
if download_media:
169
async for chunk in save_response_media(audio["data"], prompt, [model]):
169
async for chunk in save_response_media(audio, prompt, [model]):
170
170
yield chunk
171
if "transcript" in audio:
172
yield "\n\n"
173
yield audio["transcript"]
174
171
else:
175
172
yield AudioResponse(f"data:audio/mpeg;base64,{audio['data']}", transcript=audio.get("transcript"))
176
173
if choice and "finish_reason" in choice and choice["finish_reason"] is not None:
@@ -12,6 +12,7 @@ from email.utils import formatdate
12
12
import os.path
13
13
import hashlib
14
14
import asyncio
15
import base64
15
16
from contextlib import asynccontextmanager
16
17
from urllib.parse import quote_plus
17
18
from fastapi import FastAPI, Response, Request, UploadFile, Form, Depends, Header
@@ -679,7 +680,7 @@ class Api:
679
680
HTTP_500_INTERNAL_SERVER_ERROR: {"model": ErrorResponseModel},
680
681
}
681
682
@self.app.post("/v1/audio/speech", responses=responses)
682
@self.app.post("/api/{path_provider}/audio/speech", responses=responses)
683
@self.app.post("/api/{provider}/audio/speech", responses=responses)
683
684
async def generate_speech(
684
685
config: AudioSpeechConfig,
685
686
provider: str = AppConfig.media_provider,
@@ -698,8 +699,12 @@ class Api:
698
699
prompt=config.input,
699
700
audio=filter_none(voice=config.voice, format=config.response_format, language=config.language),
700
701
api_key=api_key,
702
download_media=config.download_media,
701
703
)
702
if isinstance(response.choices[0].message.content, AudioResponse):
704
if response.choices[0].message.audio is not None:
705
response = base64.b64decode(response.choices[0].message.audio.data)
706
return Response(response, media_type=f"audio/{config.response_format.replace("mp3", "mpeg")}")
707
elif isinstance(response.choices[0].message.content, AudioResponse):
703
708
response = response.choices[0].message.content.data
704
709
response = response.replace("/media", get_media_dir())
705
710
def delete_file():
@@ -133,4 +133,5 @@ class AudioSpeechConfig(BaseModel):
133
133
voice: Optional[str] = None
134
134
instrcutions: str = "Speech this text in a natural way."
135
135
response_format: Optional[str] = None
136
language: Optional[str] = None
136
language: Optional[str] = None
137
download_media: bool = True
@@ -113,16 +113,9 @@ class ResponseMessage(BaseModel):
113
113
type: str = "message"
114
114
role: str
115
115
content: list[ResponseMessageContent]
116
audio: dict = None
117
116
118
117
@classmethod
119
118
def model_construct(cls, content: str):
120
if isinstance(content, AudioResponse):
121
return super().model_construct(
122
role="assistant",
123
audio={"data": content.data.split(",")[-1], "transcript": content.transcript},
124
content=[ResponseMessageContent.model_construct(content)]
125
)
126
119
return super().model_construct(role="assistant", content=[ResponseMessageContent.model_construct(content)])
127
120
128
121
class ResponseMessageContent(BaseModel):
@@ -137,14 +130,36 @@ class ResponseMessageContent(BaseModel):
137
130
def serialize_text(self, text: str):
138
131
return str(text)
139
132
133
class AudioResponseModel(BaseModel):
134
data: str
135
transcript: Optional[str] = None
136
137
@classmethod
138
def model_construct(cls, data: str, transcript: Optional[str] = None):
139
return super().model_construct(data=data, transcript=transcript)
140
140
141
class ChatCompletionMessage(BaseModel):
141
142
role: str
142
143
content: str
143
144
reasoning_content: Optional[str] = None
144
145
tool_calls: list[ToolCallModel] = None
145
146
audio: AudioResponseModel = None
147
148
@classmethod
149
def model_construct(cls, content: str):
150
return super().model_construct(role="assistant", content=[ResponseMessageContent.model_construct(content)])
151
146
152
@classmethod
147
153
def model_construct(cls, content: str, reasoning_content: list[Reasoning] = None, tool_calls: list = None):
154
if isinstance(content, AudioResponse) and content.data.startswith("data:"):
155
return super().model_construct(
156
role="assistant",
157
audio=AudioResponseModel.model_construct(
158
data=content.data.split(",")[-1],
159
transcript=content.transcript
160
),
161
content=content
162
)
148
163
return super().model_construct(role="assistant", content=content, **filter_none(tool_calls=tool_calls, reasoning_content=reasoning_content))
149
164
150
165
@field_serializer('content')
@@ -60,15 +60,16 @@ def update_filename(response, filename: str) -> str:
60
60
timestamp = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S %Z').timestamp()
61
61
return str(int(timestamp)) + "_" + filename.split("_", maxsplit=1)[-1]
62
62
63
async def save_response_media(response, prompt: str, tags: list[str] = []) -> AsyncIterator:
63
async def save_response_media(response, prompt: str, tags: list[str] = [], transcript: str = None) -> AsyncIterator:
64
64
"""Save media from response to local file and return URL"""
65
65
if isinstance(response, dict):
66
content_type = response.get("mimeType")
66
content_type = response.get("mimeType", "audio/mpeg")
67
transcript = response.get("transcript")
67
68
response = response.get("data")
68
69
elif hasattr(response, "headers"):
69
70
content_type = response.headers["content-type"]
70
71
else:
71
content_type = "audio/mpeg"
72
raise ValueError("Response must be a dict or have headers")
72
73
73
74
if isinstance(response, str):
74
75
response = base64.b64decode(response)
@@ -101,7 +102,7 @@ async def save_response_media(response, prompt: str, tags: list[str] = []) -> As
101
102
source_url = str(response.url)
102
103
103
104
if content_type.startswith("audio/"):
104
yield AudioResponse(media_url, text=prompt, source_url=source_url)
105
yield AudioResponse(media_url, transcript, source_url=source_url)
105
106
elif content_type.startswith("video/"):
106
107
yield VideoResponse(media_url, prompt, source_url=source_url)
107
108
else: