feat: sync full-stack Docker runtime and UI
This commit is contained in:
+94
-48
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user