chore: 更新项目配置和依赖,优化前后端代码

This commit is contained in:
2026-04-04 20:05:40 +08:00
parent ef162de168
commit be4000b774
33 changed files with 2338 additions and 942 deletions
+19 -11
View File
@@ -9,9 +9,14 @@ from dotenv import load_dotenv
load_dotenv()
OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gpt-oss:20b')
OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://192.168.0.120:11434')
OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://localhost:11434')
VLM_MODEL = os.getenv('VLM_MODEL', 'qwen3-vl:30b')
# Timeouts in seconds
COMPLETION_TIMEOUT = 30
OCR_TIMEOUT = 60
CONVERT_TIMEOUT = 30
client = ollama.AsyncClient(host=OLLAMA_HOST)
logger = logging.getLogger("llm")
@@ -97,7 +102,7 @@ async def call_ollama(
if thinking:
kwargs["think"] = thinking
response = await client.chat(**kwargs)
response = await asyncio.wait_for(client.chat(**kwargs), timeout=COMPLETION_TIMEOUT)
except asyncio.CancelledError:
elapsed_ms = (time.perf_counter() - start) * 1000
end_dt = datetime.now()
@@ -156,15 +161,18 @@ async def call_vlm_ocr(image_bytes: bytes, language: str = 'auto') -> str:
)
try:
response = await client.chat(
model=VLM_MODEL,
messages=[{
'role': 'user',
'content': VLM_OCR_CONTEXT_PROMPT,
'images': [image_bytes]
}],
stream=False,
options={'temperature': 0.3}
response = await asyncio.wait_for(
client.chat(
model=VLM_MODEL,
messages=[{
'role': 'user',
'content': VLM_OCR_CONTEXT_PROMPT,
'images': [image_bytes]
}],
stream=False,
options={'temperature': 0.3}
),
timeout=OCR_TIMEOUT
)
except Exception:
elapsed_ms = (time.perf_counter() - start) * 1000
+14 -8
View File
@@ -1,4 +1,4 @@
import asyncio
import asyncio
import base64
import json
import logging
@@ -238,7 +238,7 @@ async def ocr_image(request: OCRRequest, api_key: str = Security(get_api_key)):
)
image_bytes = base64.b64decode(request.image)
logger.info("[%s] /v1/ocr decoded image_bytes=%d", request_id, len(image_bytes))
result = await call_vlm_ocr(image_bytes, request.language)
result = await call_vlm_ocr(image_bytes, request.language)
logger.info(
"[%s] /v1/ocr success text_chars=%d text_preview='%s'",
request_id,
@@ -253,7 +253,7 @@ async def ocr_image(request: OCRRequest, api_key: str = Security(get_api_key)):
@app.post("/v1/convert")
async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(get_api_key)):
"""将文件转换为Markdown格式"""
"""鐏忓棙鏋冩禒鎯版祮閹诡澀璐烳arkdown閺嶇厧绱?""
request_id = str(uuid.uuid4())[:8]
try:
@@ -264,20 +264,20 @@ async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(g
len(request.file or ""),
)
# 解码Base64文件内容
# 鐟欙絿鐖淏ase64閺傚洣娆㈤崘鍛啇
file_bytes = base64.b64decode(request.file)
logger.info("[%s] /v1/convert decoded file_bytes=%d", request_id, len(file_bytes))
# 获取文件扩展名
# 閼惧嘲褰囬弬鍥︽閹碘晛鐫嶉崥?
ext = os.path.splitext(request.filename)[1].lower()
# 创建临时文件
# 閸掓稑缂撴稉瀛樻閺傚洣娆?
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
tmp.write(file_bytes)
tmp_path = tmp.name
try:
# 使用MarkItDown转换为Markdown
# 娴h法鏁arkItDown鏉烆剚宕叉稉绡梐rkdown
md = markitdown.MarkItDown()
result = md.convert(tmp_path)
markdown_text = result.text_content
@@ -294,7 +294,7 @@ async def convert_to_markdown(request: ConvertRequest, api_key: str = Security(g
"filename": request.filename
}
finally:
# 清理临时文件
# 濞撳懐鎮婃稉瀛樻閺傚洣娆?
if os.path.exists(tmp_path):
os.unlink(tmp_path)
@@ -307,3 +307,9 @@ if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)
# TTS and STT routes
from tts_asr import register_tts_asr_routes
register_tts_asr_routes(app)
+8 -1
View File
@@ -1,4 +1,4 @@
fastapi
fastapi
uvicorn
ollama
pydantic
@@ -10,3 +10,10 @@ python-docx
python-pptx
openpyxl
pypdf
# TTS and ASR dependencies
torch
transformers
soundfile
numpy
accelerate
+141
View File
@@ -0,0 +1,141 @@
# TTS and Speech Recognition API for macOS Silicon
import os
import asyncio
import logging
import base64
from typing import Optional
from fastapi import APIRouter, UploadFile, File, HTTPException, Security
from pydantic import BaseModel
from fastapi.security import APIKeyHeader
router = APIRouter()
api_key_header = APIKeyHeader(name="X-API-Key")
logger = logging.getLogger("tts_stt")
def _speak_text_macos(text: str, voice: str = "meijia", rate: float = 0.5) -> bytes:
import subprocess
import tempfile
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
output_path = tmp.name
try:
cmd = ["say", "-v", voice, "-r", str(rate * 10), "--output-format", "WAVE", "-o", output_path, text]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise Exception(f"TTS failed: {result.stderr}")
with open(output_path, "rb") as f:
audio_data = f.read()
return audio_data
finally:
if os.path.exists(output_path):
os.unlink(output_path)
async def _speak_text_macos_async(text: str, voice: str = "meijia", rate: float = 0.5) -> bytes:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _speak_text_macos, text, voice, rate)
def _recognize_speech_macos(audio_data: bytes, language: str = "zh-CN") -> str:
import tempfile
try:
import whisper
model = whisper.load_model("tiny")
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp2:
tmp2.write(audio_data)
audio_for_whisper = tmp2.name
try:
result = model.transcribe(audio_for_whisper, language=language[:2])
return result["text"]
finally:
if os.path.exists(audio_for_whisper):
os.unlink(audio_for_whisper)
except ImportError:
raise Exception("Whisper is required for speech recognition on macOS")
async def _recognize_speech_macos_async(audio_data: bytes, language: str = "zh-CN") -> str:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, _recognize_speech_macos, audio_data, language)
class TTSRequest(BaseModel):
text: str
voice: str = "meijia"
rate: float = 0.5
format: str = "wav"
class TTSResponse(BaseModel):
audio_base64: str
format: str
duration_ms: int
class STTRequest(BaseModel):
audio_base64: str
language: str = "zh-CN"
class STTResponse(BaseModel):
text: str
language: str
@router.post("/tts", response_model=TTSResponse)
async def text_to_speech(req: TTSRequest, api_key: str = Security(get_api_key)):
request_id = str(hash(req.text))[:8]
try:
logger.info("[TTS][%s] text_chars=%d voice=%s", request_id, len(req.text), req.voice)
audio_data = await _speak_text_macos_async(req.text, req.voice, req.rate)
if req.format.lower() == "mp3":
import tempfile
import subprocess
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_in:
tmp_in.write(audio_data)
input_path = tmp_in.name
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_out:
output_path = tmp_out.name
try:
cmd = ["ffmpeg", "-i", input_path, "-acodec", "libmp3lame", output_path]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise Exception(f"MP3 conversion failed: {result.stderr}")
with open(output_path, "rb") as f:
audio_data = f.read()
finally:
for p in [input_path, output_path]:
if os.path.exists(p):
os.unlink(p)
duration_ms = len(audio_data) * 1000 // 16000
logger.info("[TTS][%s] success duration_ms=%d", request_id, duration_ms)
return TTSResponse(audio_base64=base64.b64encode(audio_data).decode(), format=req.format, duration_ms=duration_ms)
except Exception as e:
logger.exception("[TTS] failed: %s", e)
raise HTTPException(status_code=500, detail=str(e))
@router.post("/stt", response_model=STTResponse)
async def speech_to_text(req: STTRequest, api_key: str = Security(get_api_key)):
request_id = str(hash(req.audio_base64))[:8]
try:
logger.info("[STT][%s] audio_base64_chars=%d language=%s", request_id, len(req.audio_base64), req.language)
audio_data = base64.b64decode(req.audio_base64)
text = await _recognize_speech_macos_async(audio_data, req.language)
logger.info("[STT][%s] success text_chars=%d", request_id, len(text))
return STTResponse(text=text, language=req.language)
except Exception as e:
logger.exception("[STT] failed: %s", e)
raise HTTPException(status_code=500, detail=str(e))
def get_api_key(api_key: str):
from backend.main import API_KEY
if api_key != API_KEY:
from fastapi import HTTPException
raise HTTPException(status_code=403, detail="Could not validate credentials")
return api_key
def register_tts_stt_routes(app):
app.include_router(router, prefix="/v1/tts-stt")