feat: sync full-stack Docker runtime and UI

This commit is contained in:
“ydy0615”
2026-06-27 22:22:42 +08:00
parent 356108e792
commit 23bfca51e4
50 changed files with 2750 additions and 2120 deletions
+94 -48
View File
@@ -19,6 +19,7 @@ from docs_store import get_document_store
from geoip import get_ip_location_text
from job_handlers import (
_sanitize_converted_markdown,
_infer_convert_suffix,
sanitize_inline_completion_content,
ALLOWED_CONVERT_EXTENSIONS,
asr_handler,
@@ -296,10 +297,6 @@ def _register_handlers() -> None:
manager.register_handler("tts", tts_handler)
manager.register_handler("asr", asr_handler)
_handlers_registered = True
# 打印注册信息便于调试
registered = list(getattr(manager, "handlers", {}).keys())
logger.info("handlers registered: %s", registered)
def _sse(event: str, data: dict) -> str:
@@ -400,6 +397,29 @@ def _estimate_completion_chars(req: CompletionRequest | ProCompletionRequest | W
return len(req.prefix or "") + len(req.suffix or "") + len(getattr(req, "instruction", "") or "")
def _estimate_job_cost(policy, raw_size: int, estimated_input_tokens: int) -> float:
if policy.profile == "speech_tts":
return round((raw_size / 1000.0) * config.speech_tts_input_cost_per_1k_chars, 8)
if policy.profile == "speech_asr":
return round((raw_size / (1024.0 * 1024.0)) * config.speech_asr_input_cost_per_mb, 8)
pricing_in = {
"completion": config.completion_input_cost_per_1k,
"pro": config.pro_input_cost_per_1k,
"vision": config.vision_input_cost_per_1k,
}[policy.profile]
pricing_out = {
"completion": config.completion_output_cost_per_1k,
"pro": config.pro_output_cost_per_1k,
"vision": config.vision_output_cost_per_1k,
}[policy.profile]
return round(
(estimated_input_tokens / 1000.0) * pricing_in
+ (policy.max_output_tokens / 1000.0) * pricing_out,
8,
)
async def _prepare_llm_payload(
request: Request,
*,
@@ -423,21 +443,7 @@ async def _prepare_llm_payload(
)
raise HTTPException(status_code=400, detail=f"输入过长,超过限制 {policy.max_input_chars}")
estimated_input_tokens = estimate_tokens(token_source_text if token_source_text is not None else json.dumps(request_body, ensure_ascii=False))
pricing_in = {
"completion": config.completion_input_cost_per_1k,
"pro": config.pro_input_cost_per_1k,
"vision": config.vision_input_cost_per_1k,
}[policy.profile]
pricing_out = {
"completion": config.completion_output_cost_per_1k,
"pro": config.pro_output_cost_per_1k,
"vision": config.vision_output_cost_per_1k,
}[policy.profile]
estimated_cost = round(
(estimated_input_tokens / 1000.0) * pricing_in
+ (policy.max_output_tokens / 1000.0) * pricing_out,
8,
)
estimated_cost = _estimate_job_cost(policy, raw_size, estimated_input_tokens)
controller = get_risk_controller(config)
llm_decision = await controller.check_llm(identity, scope=policy.model, estimated_cost=estimated_cost)
if not llm_decision.allowed:
@@ -675,7 +681,13 @@ async def convert_to_markdown(request: Request, req: ConvertRequest, auth: dict
file_bytes = base64.b64decode(req.file)
except Exception as exc:
return JSONResponse({"error": str(exc)}, status_code=500)
input_path = persist_temp_input(file_bytes, ext or ".bin")
temp_suffix = _infer_convert_suffix(file_bytes, req.filename)
if not temp_suffix:
return JSONResponse({"error": "仅支持 txt、docx、pptx、pdf 格式"}, status_code=500)
ext = os.path.splitext(req.filename)[1].lower()
if ext != temp_suffix:
return JSONResponse({"error": "仅支持 txt、docx、pptx、pdf 格式"}, status_code=500)
input_path = persist_temp_input(file_bytes, temp_suffix)
try:
job_id = await _queue_job("convert", {
"request_id": request_id,
@@ -742,32 +754,72 @@ async def get_compress_status(task_id: str, auth: dict = Security(_authorize_req
@app.post("/v1/tts-asr/tts")
async def queue_tts(req: TTSJobRequest, request: Request, auth: dict = Security(_authorize_request)):
del auth
request_id = _request_id(request)
job_id = await _queue_job("tts", {
"request_id": request_id,
"text": req.text,
"instruct": req.instruct,
"speaker": req.speaker,
"format": req.format,
}, request_id)
body = {
"text_chars": len((req.text or "").strip()),
"speaker": req.speaker or "Vivian",
"format": req.format or "wav",
}
try:
identity, payload = await _prepare_llm_payload(
request,
job_type="tts",
request_body=body,
raw_size=len((req.text or "").strip()),
token_source_text=req.text or "",
extra_payload={
"text": req.text,
"instruct": req.instruct,
"speaker": req.speaker,
"format": req.format,
},
)
job_id = await _queue_job("tts", payload, identity.request_id)
except RiskRejected as exc:
return _risk_json_response(_request_identity(request), exc.decision)
except QueueFullError as exc:
return JSONResponse({"error": str(exc), "request_id": _request_id(request)}, status_code=429)
except JobSystemError as exc:
return JSONResponse({"error": str(exc), "request_id": _request_id(request)}, status_code=503)
return await _stream_job(job_id)
@app.post("/v1/tts-asr/asr")
async def queue_asr(req: ASRJobRequest, request: Request, auth: dict = Security(_authorize_request)):
del auth
request_id = _request_id(request)
try:
audio_bytes = base64.b64decode(req.audio_base64)
except Exception as exc:
return JSONResponse({"error": str(exc)}, status_code=500)
return JSONResponse({"error": str(exc)}, status_code=400)
input_path = persist_temp_input(audio_bytes, ".wav")
try:
job_id = await _queue_job("asr", {
"request_id": request_id,
"input_path": input_path,
"language": req.language or "zh-CN",
}, request_id)
identity, payload = await _prepare_llm_payload(
request,
job_type="asr",
request_body={
"audio_bytes": len(audio_bytes),
"language": req.language or "zh-CN",
},
raw_size=len(audio_bytes),
token_source_text=f"audio-bytes:{len(audio_bytes)} language:{req.language or 'zh-CN'}",
extra_payload={
"input_path": input_path,
"language": req.language or "zh-CN",
"audio_bytes": len(audio_bytes),
},
)
job_id = await _queue_job("asr", payload, identity.request_id)
except RiskRejected as exc:
if os.path.exists(input_path):
os.unlink(input_path)
return _risk_json_response(_request_identity(request), exc.decision)
except QueueFullError as exc:
if os.path.exists(input_path):
os.unlink(input_path)
return JSONResponse({"error": str(exc), "request_id": _request_id(request)}, status_code=429)
except JobSystemError as exc:
if os.path.exists(input_path):
os.unlink(input_path)
return JSONResponse({"error": str(exc), "request_id": _request_id(request)}, status_code=503)
except Exception:
if os.path.exists(input_path):
os.unlink(input_path)
@@ -943,20 +995,11 @@ async def download_docs_blob(request: Request, node_id: str, auth: dict = Securi
return Response(content=payload.content, media_type=payload.mime_type, headers=headers)
def _register_tts_asr_routes():
try:
from tts_asr import register_tts_asr_routes
except ModuleNotFoundError as exc:
logger.warning("Skipping TTS/ASR route registration because a dependency is missing: %s", exc)
return
except Exception as exc:
logger.warning("Skipping TTS/ASR route registration because import failed: %s", exc)
return
def _register_tts_asr_routes() -> None:
from tts_asr import LLM_BASE_URL, register_tts_asr_routes as _register_fn
try:
register_tts_asr_routes(app, include_generation_routes=False)
except Exception as exc:
logger.warning("Failed to register TTS/ASR routes: %s", exc)
logger.info("TTS/ASR routes registered with shared LLM speech backend")
_register_fn(app)
_register_tts_asr_routes()
@@ -964,10 +1007,13 @@ _register_tts_asr_routes()
@app.on_event("shutdown")
async def _shutdown_job_manager(): # pragma: no cover
from tts_asr import close_speech_client
manager = get_job_manager()
close = getattr(manager, "close", None)
if close is not None:
await close()
await close_speech_client()
if __name__ == "__main__":
import uvicorn