feat: add video OCR/ASR, DOCX/PDF export, input block and risk config updates

- Video pipeline: video file OCR via VLM plus audio track ASR, integrated
  into job_handlers with progress emit per phase. New media_utils.py for
  audio extraction from video files.
- Document export: richExport.js replaces inline docx builder; DOCX and PDF
  export buttons are now enabled in MilkdownEditor. File size limit raised to
  100 MB.
- Input block: new InputBlockCrepe.vue component with inputBlockPlugin.ts
  and inputBlock.js for custom user-input nodes in the editor.
- Risk config: added Vite dev server ports (5173) to CORS allowlist and
  increased OCR max input from 10 MB to 100 MB.
- TTS/ASR refactor: simplified tts_asr.py model loading and warmup logic.
- Test coverage: updated tests for llm, main endpoints, pro completions and
  web search modules.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
“ydy0615”
2026-06-18 16:32:31 +08:00
parent 4813196b0a
commit 356108e792
34 changed files with 1457 additions and 563 deletions
+24 -7
View File
@@ -106,6 +106,8 @@ class OCRRequest(BaseModel):
image: str
filename: str = "image.jpg"
language: str = "auto"
media_type: str = "image"
mime_type: str | None = None
class ConvertRequest(BaseModel):
@@ -281,8 +283,10 @@ def _register_handlers() -> None:
确保 Redis 重连或实例重建后处理器不会丢失。"""
global _handlers_registered
manager = get_job_manager()
# 强制清空旧 handlers,避免重复注册累积
manager.handlers.clear()
# 强制清空旧 handlers,避免重复注册累积;测试替身只暴露 register_handler。
handlers = getattr(manager, "handlers", None)
if handlers is not None:
handlers.clear()
manager.register_handler("completion", completion_handler)
manager.register_handler("pro_completion", pro_completion_handler)
manager.register_handler("web_search", web_search_handler)
@@ -294,7 +298,8 @@ def _register_handlers() -> None:
_handlers_registered = True
# 打印注册信息便于调试
logger.info("handlers registered: %s", list(manager.handlers.keys()))tered: %s", list(manager.handlers.keys()))
registered = list(getattr(manager, "handlers", {}).keys())
logger.info("handlers registered: %s", registered)
def _sse(event: str, data: dict) -> str:
@@ -623,16 +628,28 @@ async def ocr_image(request: Request, req: OCRRequest, auth: dict = Security(_au
except Exception as exc:
return JSONResponse({"error": str(exc)}, status_code=500)
if len(image_bytes) > config.ocr_max_input_bytes:
return JSONResponse({"error": "图片过大,无法执行 OCR"}, status_code=400)
return JSONResponse({"error": "文件过大,无法执行 OCR/视频解析"}, status_code=400)
input_path = persist_temp_input(image_bytes, os.path.splitext(req.filename)[1] or ".img")
try:
identity, payload = await _prepare_llm_payload(
request,
job_type="ocr",
request_body={"filename": req.filename, "language": req.language, "image_bytes": len(image_bytes)},
request_body={
"filename": req.filename,
"language": req.language,
"media_type": req.media_type,
"mime_type": req.mime_type,
"image_bytes": len(image_bytes),
},
raw_size=len(image_bytes),
token_source_text=f"{req.filename}:{len(image_bytes)}:{req.language}",
extra_payload={"input_path": input_path, "filename": req.filename, "language": req.language},
token_source_text=f"{req.filename}:{req.media_type}:{req.mime_type}:{len(image_bytes)}:{req.language}",
extra_payload={
"input_path": input_path,
"filename": req.filename,
"language": req.language,
"media_type": req.media_type,
"mime_type": req.mime_type,
},
)
job_id = await _queue_job("ocr", payload, identity.request_id)
except RiskRejected as exc: