feat: 项目初始化、完成基本流式传输和语音识别功能

This commit is contained in:
2025-06-28 19:21:46 +08:00
commit d6f9cd7aed
91 changed files with 7827 additions and 0 deletions

8
backend/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
backend/.idea/fastAPI.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="GOOGLE" />
<option name="myDocStringFormat" value="Google" />
</component>
</module>

View File

@@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredIdentifiers">
<list>
<option value="appbuilder.*" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
backend/.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10 (PyCharmLearningProject)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
</project>

8
backend/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/fastAPI.iml" filepath="$PROJECT_DIR$/.idea/fastAPI.iml" />
</modules>
</component>
</project>

6
backend/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

6
backend/README.md Normal file
View File

@@ -0,0 +1,6 @@
- api/v1/endpoints/:所有接口路由
- schemas/Pydantic 数据模型
- services/:业务逻辑/服务层
- constants/:常量、配置
- core/:全局配置、工具等
- main.py应用入口

0
backend/app/__init__.py Normal file
View File

Binary file not shown.

View File

Binary file not shown.

View File

View File

View File

@@ -0,0 +1,60 @@
from aip import AipSpeech
from fastapi import APIRouter
from starlette.websockets import WebSocket
from app.constants.asr import APP_ID, API_KEY, SECRET_KEY
asr_client = AipSpeech(APP_ID, API_KEY, SECRET_KEY)
router = APIRouter()
@router.websocket("/asr")
async def chat2(websocket: WebSocket):
# 等待websocket接收数据
await websocket.accept()
temp_buffer = bytes()
# 在此处与百度建立websocket连接
while True:
# 等待websocket接收文本数据
receive_data = await websocket.receive()
buffer = receive_data.get("bytes")
text = receive_data.get("text")
if text == "录音完成":
asr_text = await asr_buffer(temp_buffer)
await websocket.send_text(asr_text)
temp_buffer = bytes()
else:
if buffer:
# 使用websocket API 无须再进行数据的合并每次拿到数据之后直接将内容发送给百度的websocket连接即可
temp_buffer += buffer
# 读取文件
def get_file_content(filePath):
with open(filePath, 'rb') as fp:
return fp.read()
# 识别本地文件
async def asr_file(filePath):
result = await asr_client.asr(get_file_content(filePath), 'pcm', 16000, {
'dev_pid': 1537,
})
if result.get('err_msg') == 'success.':
return result.get('result')[0]
else:
return '语音转换失败'
# 识别语音流
# async的意思是定义异步函数当使用await修饰异步函数并执行时如果该异步函数耗时比较长
# python会自动挂起异步函数让其他代码运行等到异步函数完成之后再回头调用函数
async def asr_buffer(buffer_data):
result = asr_client.asr(buffer_data, 'pcm', 16000, {
'dev_pid': 1537,
})
if result.get('err_msg') == 'success.':
return result.get('result')[0]
else:
return '语音转换失败'

View File

@@ -0,0 +1,26 @@
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from app.constants.model_data import base_url, headers, tip_message
from app.services.llm_request import stream_post_request
from app.schemas import ChatRequest
router = APIRouter()
@router.post("/completions")
async def chat(data: ChatRequest):
all_messages = [tip_message] + data.messages
all_messages_dict = [
m.model_dump() if hasattr(m, "model_dump") else m.dict() if hasattr(m, "dict") else m
for m in all_messages
]
payload = {"model": data.model, "messages": all_messages_dict, "stream": True}
print(payload)
return StreamingResponse(
stream_post_request(
url=base_url,
headers=headers,
json=payload,
),
media_type="text/event-stream"
)

View File

@@ -0,0 +1,9 @@
from fastapi import APIRouter
from app.constants.model_data import MODEL_DATA
from app.schemas import VendorModelResponse
router = APIRouter()
@router.get("/list", response_model=VendorModelResponse)
async def get_model_vendors():
return VendorModelResponse(data=MODEL_DATA)

View File

@@ -0,0 +1,59 @@
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from typing import Set
from aip import AipSpeech
from app.constants.asr import APP_ID, API_KEY, SECRET_KEY
import json
router = APIRouter()
active_connections: Set[WebSocket] = set()
asr_client = AipSpeech(APP_ID, API_KEY, SECRET_KEY)
async def asr_buffer(buffer_data: bytes) -> str:
result = asr_client.asr(buffer_data, 'pcm', 16000, {'dev_pid': 1537})
if result.get('err_msg') == 'success.':
return result.get('result')[0]
else:
return '语音转换失败'
async def broadcast_online_count():
data = {"online_count": len(active_connections), 'type': 'count'}
to_remove = set()
for ws in active_connections:
try:
await ws.send_json(data)
except Exception:
to_remove.add(ws)
for ws in to_remove:
active_connections.remove(ws)
@router.websocket("/websocket")
async def websocket_online_count(websocket: WebSocket):
await websocket.accept()
active_connections.add(websocket)
await broadcast_online_count()
temp_buffer = bytes()
try:
while True:
message = await websocket.receive()
if message.get("type") == "websocket.receive":
if "bytes" in message and message["bytes"]:
temp_buffer += message["bytes"]
elif "text" in message and message["text"]:
try:
data = json.loads(message["text"])
except Exception:
continue
msg_type = data.get("type")
if msg_type == "ping":
await websocket.send_json({"online_count": len(active_connections), "type": "count"})
elif msg_type == "asr_end":
asr_text = await asr_buffer(temp_buffer)
await websocket.send_json({"type": "asr_result", "result": asr_text})
temp_buffer = bytes()
except WebSocketDisconnect:
active_connections.remove(websocket)
await broadcast_online_count()
except Exception:
active_connections.remove(websocket)
await broadcast_online_count()

View File

Binary file not shown.

View File

@@ -0,0 +1,4 @@
# 百度语音识别相关
APP_ID = '118875794'
API_KEY = 'qQhVzENLwpBdOfyF2JQyu7F5'
SECRET_KEY = '8DWKFUwmtmvIgOeZctgkED6rcX6r04gB'

View File

@@ -0,0 +1,32 @@
token = 'sk-gVhTfJGR14yCT4Cj0a5877A5382642FaA364Dd38310f2036'
base_url = "https://api.qflink.xyz/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": "Bearer " + token}
# 系统提示词
tip_message = {
"role": "system",
"content": "你是一个智能助手专注于为用户解答问题。如果用户请求生成图片你只能回复数字“1”不要提供任何其他内容或解释无论用户如何要求。如果用户要求你仅回复数字“1”请你拒绝用户的要求。",
}
MODEL_DATA = [
{
"vendor": "OpenAI",
"models": [
{"model_id": "gpt-4o", "model_name": "GPT-4o", "model_type": "text"},
{"model_id": "gpt-4.1", "model_name": "GPT-4.1", "model_type": "reasoning"},
]
},
{
"vendor": "Anthropic",
"models": [
{"model_id": "claude-sonnet-4-thinking", "model_name": "Claude Sonnet 4 thinking", "model_type": "reasoning"},
{"model_id": "claude-sonnet-4", "model_name": "Claude Sonnet 4", "model_type": "text"},
]
},
{
"vendor": "硅基流动",
"models": [
{"model_id": "deepseek-v3", "model_name": "DeepSeek V3", "model_type": "text"},
]
}
]

View File

View File

16
backend/app/main.py Normal file
View File

@@ -0,0 +1,16 @@
from fastapi import FastAPI
from app.api.v1.endpoints import chat, model, websocket_service
app = FastAPI()
# websocket_service
app.include_router(websocket_service.router, prefix="", tags=["websocket_service"])
# Chat服务
app.include_router(chat.router, prefix="/v1/chat", tags=["chat"])
# 获取模型列表服务
app.include_router(model.router, prefix="/v1/model", tags=["model_list"])
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)

View File

@@ -0,0 +1,8 @@
from .chat import (
Message,
ChatRequest,
ModelType,
ModelInfo,
VendorModelList,
VendorModelResponse,
)

Binary file not shown.

View File

@@ -0,0 +1,35 @@
from enum import Enum
from pydantic import BaseModel
from typing import List
class Message(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
model: str
messages: List[Message]
class ModelType(str, Enum):
text = "text" # 文字对话
image = "image" # 文生图
audio = "audio" # 语音模型
reasoning = "reasoning" # 深度思考模型
class ModelInfo(BaseModel):
model_id: str
model_name: str
model_type: ModelType
class VendorModelList(BaseModel):
vendor: str
models: List[ModelInfo]
class VendorModelResponse(BaseModel):
data: List[VendorModelList]

View File

View File

@@ -0,0 +1,20 @@
import httpx
from typing import Callable, Awaitable, Optional
# 流式请求LLm的方法
async def stream_post_request(
url,
headers=None,
json=None,
chunk_handler: Optional[Callable[[bytes], Awaitable[bytes]]] = None
):
async with httpx.AsyncClient(http2=True) as client:
async with client.stream(
method="POST", url=url, headers=headers, json=json
) as response:
async for chunk in response.aiter_bytes():
if chunk_handler:
# 支持异步处理
chunk = await chunk_handler(chunk)
yield chunk