diff --git a/.env.example b/.env.example index f970ecd..ca72715 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,7 @@ LOCAL_BGE_MODEL_PATH= # 可选值: paddleocr / aliyun / tencent / deepseek / auto # auto 模式: 本地 PaddleOCR 优先,失败后 fallback 到 DeepSeek / 云端 OCR OCR_PROVIDER=deepseek +OCR_CONCURRENCY=1 # ── 阿里云 OCR 配置 ── ALIYUN_OCR_ACCESS_KEY=your-aliyun-access-key diff --git a/app/api/v1/images.py b/app/api/v1/images.py index 6f597c6..6492eb7 100644 --- a/app/api/v1/images.py +++ b/app/api/v1/images.py @@ -30,6 +30,19 @@ from app.services.llm_service import LLMService logger = logging.getLogger(__name__) router = APIRouter() +# OCR 并发控制信号量(动态读取配置) +_ocr_semaphore: asyncio.Semaphore | None = None + + +def _get_ocr_semaphore() -> asyncio.Semaphore: + """获取/重建 OCR 并发信号量""" + global _ocr_semaphore + from app.config import settings + concurrency = settings.OCR_CONCURRENCY + if _ocr_semaphore is None or _ocr_semaphore._value != concurrency: + _ocr_semaphore = asyncio.Semaphore(concurrency) + return _ocr_semaphore + ALLOWED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".webp"} # 全局 BM25 索引实例(在 main.py 中初始化) @@ -126,7 +139,8 @@ async def recognize_image(image_id: int, db: AsyncSession = Depends(get_db)): await db.flush() try: - ocr_result = await OCRService.recognize(image_record.file_path) + async with _get_ocr_semaphore(): + ocr_result = await OCRService.recognize(image_record.file_path) _save_ocr_result(image_record, ocr_result, db) await db.flush() await db.refresh(image_record) @@ -171,7 +185,8 @@ async def batch_recognize(files: List[UploadFile], db: AsyncSession = Depends(ge db.add(image_record) await db.flush() try: - ocr_result = await OCRService.recognize(str(dest_path)) + async with _get_ocr_semaphore(): + ocr_result = await OCRService.recognize(str(dest_path)) # OCR 内容去重 existing_id = await _check_ocr_duplicate(ocr_result.text, db) if existing_id is not None: @@ -239,7 +254,8 @@ async def import_from_paths(data: PathImportRequest, db: AsyncSession = Depends( db.add(image_record) await db.flush() try: - ocr_result = await OCRService.recognize(file_path) + async with _get_ocr_semaphore(): + ocr_result = await OCRService.recognize(file_path) # OCR 内容去重 existing_id = await _check_ocr_duplicate(ocr_result.text, db) if existing_id is not None: @@ -279,7 +295,8 @@ async def recognize_direct(file: UploadFile, db: AsyncSession = Depends(get_db)) db.add(image_record) await db.flush() try: - ocr_result = await OCRService.recognize(str(dest_path)) + async with _get_ocr_semaphore(): + ocr_result = await OCRService.recognize(str(dest_path)) # OCR 内容去重 existing_id = await _check_ocr_duplicate(ocr_result.text, db) if existing_id is not None: diff --git a/app/api/v1/settings.py b/app/api/v1/settings.py index 1cd51d9..b13ab68 100644 --- a/app/api/v1/settings.py +++ b/app/api/v1/settings.py @@ -38,6 +38,7 @@ class SettingsResponse(BaseModel): chunk_overlap: int search_limit: int judge_batch_size: int + ocr_concurrency: int # API Key 只返回是否已配置(不返回实际值) has_minimax_key: bool has_deepseek_key: bool @@ -71,6 +72,7 @@ async def get_settings(): chunk_overlap=settings.CHUNK_OVERLAP, search_limit=settings.SEARCH_LIMIT, judge_batch_size=settings.JUDGE_BATCH_SIZE, + ocr_concurrency=settings.OCR_CONCURRENCY, has_minimax_key=bool(settings.MINIMAX_API_KEY), has_deepseek_key=bool(settings.DEEPSEEK_API_KEY), has_openai_key=bool(settings.OPENAI_API_KEY), @@ -110,6 +112,7 @@ class SettingsUpdate(BaseModel): chunk_overlap: int | None = None search_limit: int | None = None judge_batch_size: int | None = None + ocr_concurrency: int | None = None # 企业微信机器人 wework_bot_enabled: bool | None = None wework_bot_id: str | None = None @@ -159,6 +162,7 @@ async def update_settings(data: SettingsUpdate): "chunk_overlap": "CHUNK_OVERLAP", "search_limit": "SEARCH_LIMIT", "judge_batch_size": "JUDGE_BATCH_SIZE", + "ocr_concurrency": "OCR_CONCURRENCY", "wework_bot_enabled": "WEWORK_BOT_ENABLED", "wework_bot_id": "WEWORK_BOT_ID", "wework_bot_secret": "WEWORK_BOT_SECRET", diff --git a/app/config.py b/app/config.py index 54595a3..e4375f5 100644 --- a/app/config.py +++ b/app/config.py @@ -153,6 +153,9 @@ class Settings(BaseSettings): SEARCH_LIMIT: int = Field(default=3, description="图片搜索默认返回数量(1-10)") JUDGE_BATCH_SIZE: int = Field(default=10, description="每次LLM判断的文章数量(1-50)") + # ──────────────────────────── OCR ──────────────────────────── + OCR_CONCURRENCY: int = Field(default=1, ge=1, le=10, description="OCR 并发识别数量(1-10),多用户同时上传时控制并发") + # ──────────────────────────── CORS ──────────────────────────── CORS_ORIGINS: list[str] = Field( default=["*"], diff --git a/static/index.html b/static/index.html index d45e570..fe539fe 100644 --- a/static/index.html +++ b/static/index.html @@ -1093,6 +1093,14 @@ 篇(1-50) +