Spaces:
Running
Running
Commit ·
4b54fab
1
Parent(s): a70f425
feat: oom
Browse files- Dockerfile +1 -5
- app/api/server.py +4 -5
- app/api/v1/embeddings.py +219 -219
- app/models/__init__.py +2 -2
- app/models/schemas.py +12 -12
- app/services/code_executor_service.py +40 -39
- app/services/converter_service.py +114 -114
- app/services/embeddings_service.py +56 -57
- requirements.txt +1 -1
Dockerfile
CHANGED
|
@@ -9,7 +9,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 9 |
ffmpeg \
|
| 10 |
libmagic1 \
|
| 11 |
nodejs \
|
| 12 |
-
openjdk-21-jdk-headless \
|
| 13 |
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
|
| 15 |
RUN groupadd --gid 1000 appuser && \
|
|
@@ -22,12 +21,9 @@ RUN pip install --no-cache-dir --upgrade pip && \
|
|
| 22 |
pip install --no-cache-dir -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu && \
|
| 23 |
python -m spacy download en_core_web_sm
|
| 24 |
|
| 25 |
-
RUN pip install --no-cache-dir "youtube-transcript-api>=1.2.4"
|
| 26 |
-
|
| 27 |
COPY --chown=appuser:appuser . .
|
| 28 |
|
| 29 |
-
|
| 30 |
-
RUN mkdir -p /app/models && python3 -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='ibm-granite/granite-embedding-small-english-r2', local_dir='/app/models/bge-384'); snapshot_download(repo_id='nomic-ai/nomic-embed-text-v1.5', local_dir='/app/models/bge-768'); snapshot_download(repo_id='lightonai/modernbert-embed-large', local_dir='/app/models/bge-1024'); snapshot_download(repo_id='nomic-ai/nomic-embed-vision-v1.5', local_dir='/app/models/vision'); import json, os; cfg='/app/models/vision/config.json'; d=json.load(open(cfg)); d['n_inner']=int(d['n_inner']) if isinstance(d.get('n_inner'), float) else d['n_inner']; json.dump(d, open(cfg, 'w'), indent=2)" && chown -R appuser:appuser /app/models
|
| 31 |
|
| 32 |
RUN mkdir -p /app/logs && \
|
| 33 |
chown -R appuser:appuser /app/logs
|
|
|
|
| 9 |
ffmpeg \
|
| 10 |
libmagic1 \
|
| 11 |
nodejs \
|
|
|
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
RUN groupadd --gid 1000 appuser && \
|
|
|
|
| 21 |
pip install --no-cache-dir -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu && \
|
| 22 |
python -m spacy download en_core_web_sm
|
| 23 |
|
|
|
|
|
|
|
| 24 |
COPY --chown=appuser:appuser . .
|
| 25 |
|
| 26 |
+
RUN mkdir -p /app/models && python3 -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='ibm-granite/granite-embedding-small-english-r2', local_dir='/app/models/bge-384')" && chown -R appuser:appuser /app/models
|
|
|
|
| 27 |
|
| 28 |
RUN mkdir -p /app/logs && \
|
| 29 |
chown -R appuser:appuser /app/logs
|
app/api/server.py
CHANGED
|
@@ -37,12 +37,11 @@ async def _self_ping():
|
|
| 37 |
|
| 38 |
@asynccontextmanager
|
| 39 |
async def lifespan(app: FastAPI):
|
| 40 |
-
_logger.info("Initializing embedding service (loading
|
| 41 |
loop = asyncio.get_running_loop()
|
| 42 |
-
await loop.run_in_executor(None, _embedding_service.
|
| 43 |
-
await loop.run_in_executor(None, _embedding_service.load_vision_model)
|
| 44 |
-
_logger.info("Embedding service initialized with dims: %s
|
| 45 |
-
_embedding_service.loaded_dimensions, _embedding_service._vision_loaded)
|
| 46 |
|
| 47 |
asyncio.create_task(_self_ping())
|
| 48 |
yield
|
|
|
|
| 37 |
|
| 38 |
@asynccontextmanager
|
| 39 |
async def lifespan(app: FastAPI):
|
| 40 |
+
_logger.info("Initializing embedding service (loading 384-dim model)...")
|
| 41 |
loop = asyncio.get_running_loop()
|
| 42 |
+
await loop.run_in_executor(None, _embedding_service.load_model, 384)
|
| 43 |
+
# await loop.run_in_executor(None, _embedding_service.load_vision_model) # DISABLED (OOM mitigation)
|
| 44 |
+
_logger.info("Embedding service initialized with dims: %s", _embedding_service.loaded_dimensions)
|
|
|
|
| 45 |
|
| 46 |
asyncio.create_task(_self_ping())
|
| 47 |
yield
|
app/api/v1/embeddings.py
CHANGED
|
@@ -2,19 +2,19 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import concurrent.futures
|
| 5 |
-
import io
|
| 6 |
import os
|
| 7 |
import time
|
| 8 |
from typing import Annotated, List, Optional
|
| 9 |
|
| 10 |
-
import httpx
|
| 11 |
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
|
| 12 |
-
from PIL import Image
|
| 13 |
|
| 14 |
from app.api.deps import require_auth, get_embeddings_service
|
| 15 |
from app.config import get_settings
|
| 16 |
from app.core.logger import get_logger
|
| 17 |
-
from app.models.schemas import EmbeddingItem, EmbeddingRequest, EmbeddingResponse, VisionUrlRequest
|
| 18 |
from app.services.embeddings_service import EmbeddingService
|
| 19 |
|
| 20 |
router = APIRouter()
|
|
@@ -22,39 +22,39 @@ _logger = get_logger(__name__)
|
|
| 22 |
_settings = get_settings()
|
| 23 |
_MAX_WORKERS = min(32, (os.cpu_count() or 1) + 4)
|
| 24 |
_thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS)
|
| 25 |
-
_MAX_VISION_ITEMS = 5
|
| 26 |
-
_MAX_IMAGE_BYTES = 15 * 1024 * 1024
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def _validate_image(raw: bytes, source: str) -> Image.Image:
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
async def _download_image(url: str) -> bytes:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
|
| 59 |
|
| 60 |
@router.post(
|
|
@@ -126,185 +126,185 @@ async def create_embeddings(
|
|
| 126 |
)
|
| 127 |
|
| 128 |
|
| 129 |
-
@router.post(
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
)
|
| 134 |
-
async def create_vision_embeddings_file(
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
) -> EmbeddingResponse:
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
@router.post(
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
)
|
| 230 |
-
async def create_vision_embeddings_url(
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
) -> EmbeddingResponse:
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import concurrent.futures
|
| 5 |
+
# import io # DISABLED (OOM mitigation) — only used by vision
|
| 6 |
import os
|
| 7 |
import time
|
| 8 |
from typing import Annotated, List, Optional
|
| 9 |
|
| 10 |
+
# import httpx # DISABLED (OOM mitigation) — only used by vision
|
| 11 |
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status
|
| 12 |
+
# from PIL import Image # DISABLED (OOM mitigation)
|
| 13 |
|
| 14 |
from app.api.deps import require_auth, get_embeddings_service
|
| 15 |
from app.config import get_settings
|
| 16 |
from app.core.logger import get_logger
|
| 17 |
+
from app.models.schemas import EmbeddingItem, EmbeddingRequest, EmbeddingResponse # , VisionUrlRequest # DISABLED (OOM mitigation)
|
| 18 |
from app.services.embeddings_service import EmbeddingService
|
| 19 |
|
| 20 |
router = APIRouter()
|
|
|
|
| 22 |
_settings = get_settings()
|
| 23 |
_MAX_WORKERS = min(32, (os.cpu_count() or 1) + 4)
|
| 24 |
_thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS)
|
| 25 |
+
# _MAX_VISION_ITEMS = 5 # DISABLED (OOM mitigation)
|
| 26 |
+
# _MAX_IMAGE_BYTES = 15 * 1024 * 1024 # DISABLED (OOM mitigation)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# def _validate_image(raw: bytes, source: str) -> Image.Image: # DISABLED (OOM mitigation)
|
| 30 |
+
# # FIX: Check if the file is completely empty (0 bytes)
|
| 31 |
+
# if not raw:
|
| 32 |
+
# raise ValueError(f"File {source} is empty (0 bytes).")
|
| 33 |
+
#
|
| 34 |
+
# if len(raw) > _MAX_IMAGE_BYTES:
|
| 35 |
+
# raise ValueError(f"Image {source} exceeds 15 MB limit")
|
| 36 |
+
#
|
| 37 |
+
# try:
|
| 38 |
+
# img = Image.open(io.BytesIO(raw))
|
| 39 |
+
# img.load()
|
| 40 |
+
# if img.mode != "RGB":
|
| 41 |
+
# img = img.convert("RGB")
|
| 42 |
+
# return img
|
| 43 |
+
# except Exception as exc:
|
| 44 |
+
# raise ValueError(f"Invalid image {source}: {exc}")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# async def _download_image(url: str) -> bytes: # DISABLED (OOM mitigation)
|
| 48 |
+
# try:
|
| 49 |
+
# async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
| 50 |
+
# resp = await client.get(url)
|
| 51 |
+
# resp.raise_for_status()
|
| 52 |
+
# ctype = resp.headers.get("content-type", "")
|
| 53 |
+
# if not ctype.startswith("image/"):
|
| 54 |
+
# raise ValueError(f"URL {url} returned non-image Content-Type: {ctype}")
|
| 55 |
+
# return resp.content
|
| 56 |
+
# except httpx.HTTPError as exc:
|
| 57 |
+
# raise ValueError(f"Failed to download {url}: {exc}")
|
| 58 |
|
| 59 |
|
| 60 |
@router.post(
|
|
|
|
| 126 |
)
|
| 127 |
|
| 128 |
|
| 129 |
+
# @router.post( # DISABLED (OOM mitigation)
|
| 130 |
+
# "/embeddings/vision/file",
|
| 131 |
+
# response_model=EmbeddingResponse,
|
| 132 |
+
# summary="Generate embeddings from uploaded images",
|
| 133 |
+
# )
|
| 134 |
+
# async def create_vision_embeddings_file(
|
| 135 |
+
# files: Annotated[List[UploadFile], File(description="Image files to embed (max 5)")],
|
| 136 |
+
# token: str = Depends(require_auth),
|
| 137 |
+
# embedding_service: EmbeddingService = Depends(get_embeddings_service),
|
| 138 |
+
# ) -> EmbeddingResponse:
|
| 139 |
+
# if not files:
|
| 140 |
+
# raise HTTPException(status_code=400, detail={"success": False, "message": "No files provided."})
|
| 141 |
+
# if len(files) > _MAX_VISION_ITEMS:
|
| 142 |
+
# raise HTTPException(status_code=400, detail={"success": False, "message": f"Maximum {_MAX_VISION_ITEMS} images per request."})
|
| 143 |
+
#
|
| 144 |
+
# if not embedding_service._vision_loaded:
|
| 145 |
+
# raise HTTPException(status_code=503, detail={"success": False, "message": "Vision model not loaded."})
|
| 146 |
+
#
|
| 147 |
+
# _logger.info("Vision embedding file request: files=%s", len(files))
|
| 148 |
+
#
|
| 149 |
+
# dim = embedding_service.vision_dimension
|
| 150 |
+
# start = time.perf_counter()
|
| 151 |
+
# images: List[Image.Image] = []
|
| 152 |
+
# item_results: List[EmbeddingItem] = []
|
| 153 |
+
#
|
| 154 |
+
# for f in files:
|
| 155 |
+
# t0 = time.perf_counter()
|
| 156 |
+
# try:
|
| 157 |
+
# # FIX: Guarantee the file cursor is at the beginning before reading!
|
| 158 |
+
# await f.seek(0)
|
| 159 |
+
# raw = await f.read()
|
| 160 |
+
#
|
| 161 |
+
# img = await asyncio.get_running_loop().run_in_executor(_thread_pool, _validate_image, raw, f.filename or "unknown")
|
| 162 |
+
# images.append(img)
|
| 163 |
+
# except Exception as exc:
|
| 164 |
+
# elapsed = (time.perf_counter() - t0) * 1000
|
| 165 |
+
# item_results.append(EmbeddingItem(
|
| 166 |
+
# success=False,
|
| 167 |
+
# time_ms=round(elapsed, 3),
|
| 168 |
+
# error_message=str(exc),
|
| 169 |
+
# ))
|
| 170 |
+
#
|
| 171 |
+
# if not images:
|
| 172 |
+
# total_ms = (time.perf_counter() - start) * 1000
|
| 173 |
+
# return EmbeddingResponse(
|
| 174 |
+
# success=False,
|
| 175 |
+
# time_ms=round(total_ms, 3),
|
| 176 |
+
# success_count=0,
|
| 177 |
+
# failed_count=len(item_results),
|
| 178 |
+
# error_message="No valid images could be processed.",
|
| 179 |
+
# results=item_results,
|
| 180 |
+
# )
|
| 181 |
+
#
|
| 182 |
+
# try:
|
| 183 |
+
# loop = asyncio.get_running_loop()
|
| 184 |
+
# vectors = await loop.run_in_executor(
|
| 185 |
+
# _thread_pool,
|
| 186 |
+
# embedding_service.generate_image_embedding,
|
| 187 |
+
# images,
|
| 188 |
+
# )
|
| 189 |
+
# except Exception as exc:
|
| 190 |
+
# elapsed = (time.perf_counter() - start) * 1000
|
| 191 |
+
# _logger.error("Vision embedding error: %s", exc)
|
| 192 |
+
# for _ in range(len(images) - len(item_results)):
|
| 193 |
+
# item_results.append(EmbeddingItem(success=False, time_ms=0, error_message=str(exc)))
|
| 194 |
+
# return EmbeddingResponse(
|
| 195 |
+
# success=False,
|
| 196 |
+
# time_ms=round(elapsed, 3),
|
| 197 |
+
# success_count=0,
|
| 198 |
+
# failed_count=len(item_results),
|
| 199 |
+
# error_message=str(exc),
|
| 200 |
+
# results=item_results,
|
| 201 |
+
# )
|
| 202 |
+
#
|
| 203 |
+
# total_ms = (time.perf_counter() - start) * 1000
|
| 204 |
+
# for i, vec in enumerate(vectors):
|
| 205 |
+
# item_results.append(EmbeddingItem(
|
| 206 |
+
# success=True,
|
| 207 |
+
# time_ms=round(total_ms / len(vectors), 3),
|
| 208 |
+
# embeddings=vec,
|
| 209 |
+
# dimension=dim,
|
| 210 |
+
# ))
|
| 211 |
+
#
|
| 212 |
+
# success_count = sum(1 for r in item_results if r.success)
|
| 213 |
+
# failed_count = len(item_results) - success_count
|
| 214 |
+
# _logger.info("Vision embedding success: items=%s, success=%s, failed=%s, total_ms=%s",
|
| 215 |
+
# len(item_results), success_count, failed_count, round(total_ms, 3))
|
| 216 |
+
# return EmbeddingResponse(
|
| 217 |
+
# success=failed_count == 0,
|
| 218 |
+
# time_ms=round(total_ms, 3),
|
| 219 |
+
# success_count=success_count,
|
| 220 |
+
# failed_count=failed_count,
|
| 221 |
+
# results=item_results,
|
| 222 |
+
# )
|
| 223 |
+
#
|
| 224 |
+
#
|
| 225 |
+
# @router.post( # DISABLED (OOM mitigation)
|
| 226 |
+
# "/embeddings/vision/url",
|
| 227 |
+
# response_model=EmbeddingResponse,
|
| 228 |
+
# summary="Generate embeddings from image URLs",
|
| 229 |
+
# )
|
| 230 |
+
# async def create_vision_embeddings_url(
|
| 231 |
+
# body: VisionUrlRequest,
|
| 232 |
+
# token: str = Depends(require_auth),
|
| 233 |
+
# embedding_service: EmbeddingService = Depends(get_embeddings_service),
|
| 234 |
+
# ) -> EmbeddingResponse:
|
| 235 |
+
# if not embedding_service._vision_loaded:
|
| 236 |
+
# raise HTTPException(status_code=503, detail={"success": False, "message": "Vision model not loaded."})
|
| 237 |
+
#
|
| 238 |
+
# _logger.info("Vision embedding URL request: urls=%s", len(body.urls))
|
| 239 |
+
#
|
| 240 |
+
# dim = embedding_service.vision_dimension
|
| 241 |
+
# start = time.perf_counter()
|
| 242 |
+
# images: List[Image.Image] = []
|
| 243 |
+
# item_results: List[EmbeddingItem] = []
|
| 244 |
+
#
|
| 245 |
+
# for url in body.urls:
|
| 246 |
+
# t0 = time.perf_counter()
|
| 247 |
+
# try:
|
| 248 |
+
# raw = await _download_image(url)
|
| 249 |
+
# img = await asyncio.get_running_loop().run_in_executor(_thread_pool, _validate_image, raw, url)
|
| 250 |
+
# images.append(img)
|
| 251 |
+
# except Exception as exc:
|
| 252 |
+
# elapsed = (time.perf_counter() - t0) * 1000
|
| 253 |
+
# item_results.append(EmbeddingItem(
|
| 254 |
+
# success=False,
|
| 255 |
+
# time_ms=round(elapsed, 3),
|
| 256 |
+
# error_message=str(exc),
|
| 257 |
+
# ))
|
| 258 |
+
#
|
| 259 |
+
# if not images:
|
| 260 |
+
# total_ms = (time.perf_counter() - start) * 1000
|
| 261 |
+
# return EmbeddingResponse(
|
| 262 |
+
# success=False,
|
| 263 |
+
# time_ms=round(total_ms, 3),
|
| 264 |
+
# success_count=0,
|
| 265 |
+
# failed_count=len(item_results),
|
| 266 |
+
# error_message="No valid images could be downloaded.",
|
| 267 |
+
# results=item_results,
|
| 268 |
+
# )
|
| 269 |
+
#
|
| 270 |
+
# try:
|
| 271 |
+
# loop = asyncio.get_running_loop()
|
| 272 |
+
# vectors = await loop.run_in_executor(
|
| 273 |
+
# _thread_pool,
|
| 274 |
+
# embedding_service.generate_image_embedding,
|
| 275 |
+
# images,
|
| 276 |
+
# )
|
| 277 |
+
# except Exception as exc:
|
| 278 |
+
# elapsed = (time.perf_counter() - start) * 1000
|
| 279 |
+
# _logger.error("Vision embedding error: %s", exc)
|
| 280 |
+
# for _ in range(len(images) - len(item_results)):
|
| 281 |
+
# item_results.append(EmbeddingItem(success=False, time_ms=0, error_message=str(exc)))
|
| 282 |
+
# return EmbeddingResponse(
|
| 283 |
+
# success=False,
|
| 284 |
+
# time_ms=round(elapsed, 3),
|
| 285 |
+
# success_count=0,
|
| 286 |
+
# failed_count=len(item_results),
|
| 287 |
+
# error_message=str(exc),
|
| 288 |
+
# results=item_results,
|
| 289 |
+
# )
|
| 290 |
+
#
|
| 291 |
+
# total_ms = (time.perf_counter() - start) * 1000
|
| 292 |
+
# for i, vec in enumerate(vectors):
|
| 293 |
+
# item_results.append(EmbeddingItem(
|
| 294 |
+
# success=True,
|
| 295 |
+
# time_ms=round(total_ms / len(vectors), 3),
|
| 296 |
+
# embeddings=vec,
|
| 297 |
+
# dimension=dim,
|
| 298 |
+
# ))
|
| 299 |
+
#
|
| 300 |
+
# success_count = sum(1 for r in item_results if r.success)
|
| 301 |
+
# failed_count = len(item_results) - success_count
|
| 302 |
+
# _logger.info("Vision embedding success: items=%s, success=%s, failed=%s, total_ms=%s",
|
| 303 |
+
# len(item_results), success_count, failed_count, round(total_ms, 3))
|
| 304 |
+
# return EmbeddingResponse(
|
| 305 |
+
# success=failed_count == 0,
|
| 306 |
+
# time_ms=round(total_ms, 3),
|
| 307 |
+
# success_count=success_count,
|
| 308 |
+
# failed_count=failed_count,
|
| 309 |
+
# results=item_results,
|
| 310 |
+
# )
|
app/models/__init__.py
CHANGED
|
@@ -15,7 +15,7 @@ from app.models.schemas import (
|
|
| 15 |
SpacyLabelsResponse,
|
| 16 |
SupportedFormatsResponse,
|
| 17 |
UrlRequest,
|
| 18 |
-
VisionUrlRequest,
|
| 19 |
)
|
| 20 |
|
| 21 |
__all__ = [
|
|
@@ -34,5 +34,5 @@ __all__ = [
|
|
| 34 |
"InfoResponse",
|
| 35 |
"SupportedFormatsResponse",
|
| 36 |
"SpacyLabelsResponse",
|
| 37 |
-
"VisionUrlRequest",
|
| 38 |
]
|
|
|
|
| 15 |
SpacyLabelsResponse,
|
| 16 |
SupportedFormatsResponse,
|
| 17 |
UrlRequest,
|
| 18 |
+
# VisionUrlRequest, # DISABLED (OOM mitigation)
|
| 19 |
)
|
| 20 |
|
| 21 |
__all__ = [
|
|
|
|
| 34 |
"InfoResponse",
|
| 35 |
"SupportedFormatsResponse",
|
| 36 |
"SpacyLabelsResponse",
|
| 37 |
+
# "VisionUrlRequest", # DISABLED (OOM mitigation)
|
| 38 |
]
|
app/models/schemas.py
CHANGED
|
@@ -235,7 +235,7 @@ class EmbeddingItem(BaseModel):
|
|
| 235 |
|
| 236 |
class EmbeddingRequest(BaseModel):
|
| 237 |
content: List[str] = Field(..., min_length=1, max_length=10, description="Array of text strings to embed (max 10)")
|
| 238 |
-
dimension: int = Field(default=384, ge=384, le=
|
| 239 |
|
| 240 |
@field_validator("content")
|
| 241 |
@classmethod
|
|
@@ -256,20 +256,20 @@ class EmbeddingResponse(BaseModel):
|
|
| 256 |
results: List[EmbeddingItem]
|
| 257 |
|
| 258 |
|
| 259 |
-
class VisionUrlRequest(BaseModel):
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
|
| 270 |
|
| 271 |
class CodeItem(BaseModel):
|
| 272 |
-
language: Literal["python", "javascript"
|
| 273 |
code: str = Field(..., min_length=1, max_length=65536, description="Source code to execute")
|
| 274 |
|
| 275 |
|
|
|
|
| 235 |
|
| 236 |
class EmbeddingRequest(BaseModel):
|
| 237 |
content: List[str] = Field(..., min_length=1, max_length=10, description="Array of text strings to embed (max 10)")
|
| 238 |
+
dimension: int = Field(default=384, ge=384, le=384, description="Target embedding dimension (384 only)")
|
| 239 |
|
| 240 |
@field_validator("content")
|
| 241 |
@classmethod
|
|
|
|
| 256 |
results: List[EmbeddingItem]
|
| 257 |
|
| 258 |
|
| 259 |
+
# class VisionUrlRequest(BaseModel): # DISABLED (OOM mitigation)
|
| 260 |
+
# urls: List[str] = Field(..., min_length=1, max_length=5, description="Array of image URLs to embed (max 5)")
|
| 261 |
+
#
|
| 262 |
+
# @field_validator("urls")
|
| 263 |
+
# @classmethod
|
| 264 |
+
# def validate_urls(cls, v: List[str]) -> List[str]:
|
| 265 |
+
# for url in v:
|
| 266 |
+
# if not url.startswith(("http://", "https://")):
|
| 267 |
+
# raise ValueError(f"Invalid URL scheme: {url}")
|
| 268 |
+
# return v
|
| 269 |
|
| 270 |
|
| 271 |
class CodeItem(BaseModel):
|
| 272 |
+
language: Literal["python", "javascript"] # "java" DISABLED (OOM mitigation)
|
| 273 |
code: str = Field(..., min_length=1, max_length=65536, description="Source code to execute")
|
| 274 |
|
| 275 |
|
app/services/code_executor_service.py
CHANGED
|
@@ -70,27 +70,27 @@ class CodeSanitizer:
|
|
| 70 |
(r"worker_threads", "worker_threads not allowed"),
|
| 71 |
(r"Buffer\s*\.", "Buffer not allowed"),
|
| 72 |
],
|
| 73 |
-
"java": [
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
],
|
| 94 |
}
|
| 95 |
|
| 96 |
@classmethod
|
|
@@ -101,11 +101,11 @@ class CodeSanitizer:
|
|
| 101 |
return False, f"Forbidden: {message}"
|
| 102 |
return True, None
|
| 103 |
|
| 104 |
-
@classmethod
|
| 105 |
-
def validate_java_class(cls, code: str) -> tuple[bool, Optional[str]]:
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
|
| 110 |
|
| 111 |
class CodeExecutorService:
|
|
@@ -131,14 +131,14 @@ class CodeExecutorService:
|
|
| 131 |
"language": language, "timed_out": False,
|
| 132 |
}
|
| 133 |
|
| 134 |
-
if language == "java":
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
|
| 143 |
exec_timeout = min(timeout or self._max_execution_time, self._max_execution_time)
|
| 144 |
|
|
@@ -233,14 +233,15 @@ class CodeExecutorService:
|
|
| 233 |
|
| 234 |
async def check_runtimes(self) -> dict[str, str]:
|
| 235 |
status = {}
|
| 236 |
-
for lang, runtime in [("python", "python3"), ("javascript", "node")
|
| 237 |
path = shutil.which(runtime)
|
| 238 |
status[lang] = f"found at {path}" if path else "missing"
|
| 239 |
return status
|
| 240 |
|
| 241 |
@staticmethod
|
| 242 |
def _filename_for(language: str) -> str:
|
| 243 |
-
return {"python": "code.py", "javascript": "code.js"
|
|
|
|
| 244 |
|
| 245 |
@staticmethod
|
| 246 |
def _build_command(language: str, run_dir: Path, filename: str) -> list[str]:
|
|
@@ -250,7 +251,7 @@ class CodeExecutorService:
|
|
| 250 |
return ["python3", str(sandbox_py), str(run_dir / filename)]
|
| 251 |
elif language == "javascript":
|
| 252 |
return ["node", str(sandbox_js), str(run_dir / filename)]
|
| 253 |
-
elif language == "java":
|
| 254 |
-
|
| 255 |
-
|
| 256 |
return []
|
|
|
|
| 70 |
(r"worker_threads", "worker_threads not allowed"),
|
| 71 |
(r"Buffer\s*\.", "Buffer not allowed"),
|
| 72 |
],
|
| 73 |
+
# "java": [ # DISABLED (OOM mitigation)
|
| 74 |
+
# (r"ProcessBuilder", "ProcessBuilder not allowed"),
|
| 75 |
+
# (r"Runtime\.exec", "Runtime.exec() not allowed"),
|
| 76 |
+
# (r"Runtime\.getRuntime\s*\(\s*\)", "Runtime.getRuntime() not allowed"),
|
| 77 |
+
# (r"System\.exit\s*\(", "System.exit() not allowed"),
|
| 78 |
+
# (r"System\.gc\s*\(", "System.gc() not allowed"),
|
| 79 |
+
# (r"File\s*\(", "File operations not allowed"),
|
| 80 |
+
# (r"FileInputStream", "FileInputStream not allowed"),
|
| 81 |
+
# (r"FileOutputStream", "FileOutputStream not allowed"),
|
| 82 |
+
# (r"FileReader", "FileReader not allowed"),
|
| 83 |
+
# (r"FileWriter", "FileWriter not allowed"),
|
| 84 |
+
# (r"Socket\s*\(", "Socket not allowed"),
|
| 85 |
+
# (r"ServerSocket\s*\(", "ServerSocket not allowed"),
|
| 86 |
+
# (r"URL\s*\(", "URL not allowed"),
|
| 87 |
+
# (r"Class\.forName", "Class.forName() not allowed"),
|
| 88 |
+
# (r"Thread\s*\(", "Thread not allowed"),
|
| 89 |
+
# (r"ThreadPoolExecutor", "ThreadPoolExecutor not allowed"),
|
| 90 |
+
# (r"Runtime\.", "Runtime not allowed"),
|
| 91 |
+
# (r"System\.getProperty", "System.getProperty not allowed"),
|
| 92 |
+
# (r"System\.getenv", "System.getenv not allowed"),
|
| 93 |
+
# ],
|
| 94 |
}
|
| 95 |
|
| 96 |
@classmethod
|
|
|
|
| 101 |
return False, f"Forbidden: {message}"
|
| 102 |
return True, None
|
| 103 |
|
| 104 |
+
# @classmethod # DISABLED (OOM mitigation)
|
| 105 |
+
# def validate_java_class(cls, code: str) -> tuple[bool, Optional[str]]:
|
| 106 |
+
# if not re.search(r"public\s+class\s+Main\s*\{", code):
|
| 107 |
+
# return False, "Java code must have 'public class Main' with 'public static void main(String[] args)'"
|
| 108 |
+
# return True, None
|
| 109 |
|
| 110 |
|
| 111 |
class CodeExecutorService:
|
|
|
|
| 131 |
"language": language, "timed_out": False,
|
| 132 |
}
|
| 133 |
|
| 134 |
+
# if language == "java": # DISABLED (OOM mitigation)
|
| 135 |
+
# valid, err = CodeSanitizer.validate_java_class(code)
|
| 136 |
+
# if not valid:
|
| 137 |
+
# return {
|
| 138 |
+
# "success": False, "output": "", "error": err,
|
| 139 |
+
# "exit_code": None, "execution_time_ms": None,
|
| 140 |
+
# "language": language, "timed_out": False,
|
| 141 |
+
# }
|
| 142 |
|
| 143 |
exec_timeout = min(timeout or self._max_execution_time, self._max_execution_time)
|
| 144 |
|
|
|
|
| 233 |
|
| 234 |
async def check_runtimes(self) -> dict[str, str]:
|
| 235 |
status = {}
|
| 236 |
+
for lang, runtime in [("python", "python3"), ("javascript", "node")]: # "java" DISABLED (OOM mitigation)
|
| 237 |
path = shutil.which(runtime)
|
| 238 |
status[lang] = f"found at {path}" if path else "missing"
|
| 239 |
return status
|
| 240 |
|
| 241 |
@staticmethod
|
| 242 |
def _filename_for(language: str) -> str:
|
| 243 |
+
return {"python": "code.py", "javascript": "code.js" # "java": "Main.java" DISABLED (OOM mitigation)
|
| 244 |
+
}[language]
|
| 245 |
|
| 246 |
@staticmethod
|
| 247 |
def _build_command(language: str, run_dir: Path, filename: str) -> list[str]:
|
|
|
|
| 251 |
return ["python3", str(sandbox_py), str(run_dir / filename)]
|
| 252 |
elif language == "javascript":
|
| 253 |
return ["node", str(sandbox_js), str(run_dir / filename)]
|
| 254 |
+
# elif language == "java": # DISABLED (OOM mitigation)
|
| 255 |
+
# return ["sh", "-c",
|
| 256 |
+
# f"cd {run_dir} && javac Main.java 2>&1 && java -XX:CompressedClassSpaceSize=64m -Xmx96m Main 2>&1"]
|
| 257 |
return []
|
app/services/converter_service.py
CHANGED
|
@@ -8,7 +8,7 @@ import time
|
|
| 8 |
from pathlib import Path
|
| 9 |
from urllib.parse import urlparse
|
| 10 |
|
| 11 |
-
import httpx
|
| 12 |
from markitdown import MarkItDown
|
| 13 |
|
| 14 |
from app.core.constants import IMAGE_EXTENSIONS, IMAGE_MIME_PREFIXES
|
|
@@ -19,99 +19,99 @@ from app.services.ocr_service import ocr_image, ocr_pdf
|
|
| 19 |
_logger = get_logger(__name__)
|
| 20 |
|
| 21 |
|
| 22 |
-
def _extract_youtube_video_id(url_or_id: str) -> str:
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def _fetch_youtube_oembed(video_id: str) -> dict | None:
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def _fetch_youtube_transcript(video_id: str) -> str | None:
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def _convert_youtube(url: str) -> ConversionResult | None:
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
|
| 116 |
|
| 117 |
def _is_image(ext: str, mime: str) -> bool:
|
|
@@ -193,26 +193,26 @@ class ConverterService:
|
|
| 193 |
|
| 194 |
start = time.perf_counter()
|
| 195 |
|
| 196 |
-
try:
|
| 197 |
-
|
| 198 |
-
except ValueError:
|
| 199 |
-
|
| 200 |
-
else:
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
|
| 217 |
try:
|
| 218 |
url_ext = Path(urlparse(url).path).suffix.lower()
|
|
|
|
| 8 |
from pathlib import Path
|
| 9 |
from urllib.parse import urlparse
|
| 10 |
|
| 11 |
+
# import httpx # DISABLED (OOM mitigation) — only used by YouTube
|
| 12 |
from markitdown import MarkItDown
|
| 13 |
|
| 14 |
from app.core.constants import IMAGE_EXTENSIONS, IMAGE_MIME_PREFIXES
|
|
|
|
| 19 |
_logger = get_logger(__name__)
|
| 20 |
|
| 21 |
|
| 22 |
+
# def _extract_youtube_video_id(url_or_id: str) -> str: # DISABLED (OOM mitigation)
|
| 23 |
+
# if len(url_or_id) == 11 and not url_or_id.startswith("http"):
|
| 24 |
+
# return url_or_id
|
| 25 |
+
# pattern = r"(?:v=|\/)([0-9A-Za-z_-]{11}).*"
|
| 26 |
+
# match = re.search(pattern, url_or_id)
|
| 27 |
+
# if match:
|
| 28 |
+
# return match.group(1)
|
| 29 |
+
# raise ValueError(f"Could not extract a valid YouTube ID from: {url_or_id}")
|
| 30 |
+
#
|
| 31 |
+
#
|
| 32 |
+
# def _fetch_youtube_oembed(video_id: str) -> dict | None: # DISABLED (OOM mitigation)
|
| 33 |
+
# oembed_url = f"https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v={video_id}&format=json"
|
| 34 |
+
# try:
|
| 35 |
+
# resp = httpx.get(oembed_url, timeout=10.0)
|
| 36 |
+
# resp.raise_for_status()
|
| 37 |
+
# return resp.json()
|
| 38 |
+
# except Exception as exc:
|
| 39 |
+
# _logger.warning("YouTube oEmbed failed for %s: %s", video_id, exc)
|
| 40 |
+
# return None
|
| 41 |
+
#
|
| 42 |
+
#
|
| 43 |
+
# def _fetch_youtube_transcript(video_id: str) -> str | None: # DISABLED (OOM mitigation)
|
| 44 |
+
# try:
|
| 45 |
+
# import json as _json
|
| 46 |
+
# import os as _os
|
| 47 |
+
#
|
| 48 |
+
# from youtube_transcript_api import YouTubeTranscriptApi
|
| 49 |
+
# from youtube_transcript_api.formatters import TextFormatter
|
| 50 |
+
# from youtube_transcript_api._errors import (
|
| 51 |
+
# TranscriptsDisabled,
|
| 52 |
+
# NoTranscriptFound,
|
| 53 |
+
# VideoUnavailable,
|
| 54 |
+
# )
|
| 55 |
+
#
|
| 56 |
+
# kwargs: dict = {}
|
| 57 |
+
# cookies_raw = _os.environ.get("YOUTUBE_COOKIES", "").strip()
|
| 58 |
+
# if cookies_raw:
|
| 59 |
+
# try:
|
| 60 |
+
# import requests as _requests
|
| 61 |
+
# session = _requests.Session()
|
| 62 |
+
# session.cookies.update(_json.loads(cookies_raw))
|
| 63 |
+
# kwargs["http_client"] = session
|
| 64 |
+
# except Exception as exc:
|
| 65 |
+
# _logger.warning("Failed to apply YOUTUBE_COOKIES: %s", exc)
|
| 66 |
+
#
|
| 67 |
+
# ytt_api = YouTubeTranscriptApi(**kwargs)
|
| 68 |
+
# transcript_data = ytt_api.fetch(video_id, languages=["en"])
|
| 69 |
+
# formatter = TextFormatter()
|
| 70 |
+
# return formatter.format_transcript(transcript_data)
|
| 71 |
+
# except TranscriptsDisabled:
|
| 72 |
+
# _logger.warning("Transcripts disabled for video %s", video_id)
|
| 73 |
+
# except NoTranscriptFound:
|
| 74 |
+
# _logger.warning("No transcript found for video %s in language 'en'", video_id)
|
| 75 |
+
# except VideoUnavailable:
|
| 76 |
+
# _logger.warning("Video %s is unavailable, deleted, or private", video_id)
|
| 77 |
+
# except ValueError as ve:
|
| 78 |
+
# _logger.warning("Invalid video ID %s: %s", video_id, ve)
|
| 79 |
+
# except Exception as exc:
|
| 80 |
+
# _logger.warning("YouTube transcript failed for %s: %s", video_id, exc)
|
| 81 |
+
# return None
|
| 82 |
+
#
|
| 83 |
+
#
|
| 84 |
+
# def _convert_youtube(url: str) -> ConversionResult | None: # DISABLED (OOM mitigation)
|
| 85 |
+
# try:
|
| 86 |
+
# video_id = _extract_youtube_video_id(url)
|
| 87 |
+
# except ValueError:
|
| 88 |
+
# return None
|
| 89 |
+
#
|
| 90 |
+
# oembed = _fetch_youtube_oembed(video_id)
|
| 91 |
+
# transcript = _fetch_youtube_transcript(video_id)
|
| 92 |
+
#
|
| 93 |
+
# lines = ["# YouTube\n"]
|
| 94 |
+
# title = (oembed or {}).get("title", "")
|
| 95 |
+
# if title:
|
| 96 |
+
# lines.append(f"\n## {title}\n")
|
| 97 |
+
#
|
| 98 |
+
# author = (oembed or {}).get("author_name", "")
|
| 99 |
+
# if author:
|
| 100 |
+
# lines.append(f"\n- **Channel:** {author}\n")
|
| 101 |
+
#
|
| 102 |
+
# desc = (oembed or {}).get("description", "")
|
| 103 |
+
# if desc:
|
| 104 |
+
# lines.append(f"\n### Description\n{desc}\n")
|
| 105 |
+
#
|
| 106 |
+
# if transcript:
|
| 107 |
+
# lines.append(f"\n### Transcript\n{transcript}\n")
|
| 108 |
+
# else:
|
| 109 |
+
# if not title and not desc:
|
| 110 |
+
# return None
|
| 111 |
+
# lines.append("\n> No transcript available.\n")
|
| 112 |
+
#
|
| 113 |
+
# markdown = "".join(lines)
|
| 114 |
+
# return _build_result(url, markdown, 0, "text/html", 0.0)
|
| 115 |
|
| 116 |
|
| 117 |
def _is_image(ext: str, mime: str) -> bool:
|
|
|
|
| 193 |
|
| 194 |
start = time.perf_counter()
|
| 195 |
|
| 196 |
+
# try: # DISABLED (OOM mitigation) — YouTube transcription
|
| 197 |
+
# _extract_youtube_video_id(url)
|
| 198 |
+
# except ValueError:
|
| 199 |
+
# pass
|
| 200 |
+
# else:
|
| 201 |
+
# result = _convert_youtube(url)
|
| 202 |
+
# if result is not None:
|
| 203 |
+
# result = ConversionResult(
|
| 204 |
+
# source=result.source,
|
| 205 |
+
# markdown=result.markdown,
|
| 206 |
+
# char_count=result.char_count,
|
| 207 |
+
# word_count=result.word_count,
|
| 208 |
+
# line_count=result.line_count,
|
| 209 |
+
# duration_ms=(time.perf_counter() - start) * 1000,
|
| 210 |
+
# file_size_bytes=result.file_size_bytes,
|
| 211 |
+
# mime_type=result.mime_type,
|
| 212 |
+
# content_hash=result.content_hash,
|
| 213 |
+
# )
|
| 214 |
+
# return result
|
| 215 |
+
# _logger.warning("YouTube conversion returned None, falling back to markitdown: %s", url)
|
| 216 |
|
| 217 |
try:
|
| 218 |
url_ext = Path(urlparse(url).path).suffix.lower()
|
app/services/embeddings_service.py
CHANGED
|
@@ -5,24 +5,23 @@ import os
|
|
| 5 |
from typing import Dict, List, Optional
|
| 6 |
|
| 7 |
import numpy as np
|
| 8 |
-
import torch
|
| 9 |
-
import torch.nn.functional as F
|
| 10 |
-
from PIL import Image
|
| 11 |
from sentence_transformers import SentenceTransformer
|
| 12 |
-
from transformers import AutoImageProcessor, AutoModel
|
| 13 |
|
| 14 |
_logger = logging.getLogger(__name__)
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
# This configuration is required if you are comparing text and image embeddings.
|
| 18 |
_MODEL_MAP: Dict[int, str] = {
|
| 19 |
384: "ibm-granite/granite-embedding-small-english-r2",
|
| 20 |
-
768: "nomic-ai/nomic-embed-text-v1.5",
|
| 21 |
-
1024: "lightonai/modernbert-embed-large",
|
| 22 |
}
|
| 23 |
|
| 24 |
-
_VISION_MODEL_NAME = "nomic-ai/nomic-embed-vision-v1.5"
|
| 25 |
-
_VISION_DIMENSION = 768
|
| 26 |
|
| 27 |
|
| 28 |
class EmbeddingService:
|
|
@@ -38,9 +37,9 @@ class EmbeddingService:
|
|
| 38 |
self._device = "cpu"
|
| 39 |
|
| 40 |
self._loaded_dimensions: List[int] = []
|
| 41 |
-
self._vision_processor: Optional[AutoImageProcessor] = None
|
| 42 |
-
self._vision_model: Optional[AutoModel] = None
|
| 43 |
-
self._vision_loaded = False
|
| 44 |
|
| 45 |
def load_model(self, dimension: int) -> None:
|
| 46 |
if dimension in self._models:
|
|
@@ -66,34 +65,34 @@ class EmbeddingService:
|
|
| 66 |
for dim in _MODEL_MAP:
|
| 67 |
self.load_model(dim)
|
| 68 |
|
| 69 |
-
def load_vision_model(self) -> None:
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
|
| 98 |
def generate_embedding(self, text: List[str], dimension: int) -> List[List[float]]:
|
| 99 |
if dimension not in self._models:
|
|
@@ -110,19 +109,19 @@ class EmbeddingService:
|
|
| 110 |
)
|
| 111 |
return result.tolist()
|
| 112 |
|
| 113 |
-
def generate_image_embedding(self, images: List[Image.Image]) -> List[List[float]]:
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
|
| 127 |
@property
|
| 128 |
def loaded_dimensions(self) -> List[int]:
|
|
@@ -131,6 +130,6 @@ class EmbeddingService:
|
|
| 131 |
def is_loaded(self, dimension: int) -> bool:
|
| 132 |
return dimension in self._models
|
| 133 |
|
| 134 |
-
@property
|
| 135 |
-
def vision_dimension(self) -> int:
|
| 136 |
-
|
|
|
|
| 5 |
from typing import Dict, List, Optional
|
| 6 |
|
| 7 |
import numpy as np
|
| 8 |
+
# import torch # DISABLED (OOM mitigation) — only used by vision
|
| 9 |
+
# import torch.nn.functional as F # DISABLED (OOM mitigation)
|
| 10 |
+
# from PIL import Image # DISABLED (OOM mitigation)
|
| 11 |
from sentence_transformers import SentenceTransformer
|
| 12 |
+
# from transformers import AutoImageProcessor, AutoModel # DISABLED (OOM mitigation)
|
| 13 |
|
| 14 |
_logger = logging.getLogger(__name__)
|
| 15 |
|
| 16 |
+
# Only 384-dim embedding is enabled. 768 and 1024 are disabled to reduce memory usage.
|
|
|
|
| 17 |
_MODEL_MAP: Dict[int, str] = {
|
| 18 |
384: "ibm-granite/granite-embedding-small-english-r2",
|
| 19 |
+
# 768: "nomic-ai/nomic-embed-text-v1.5", # DISABLED (OOM mitigation)
|
| 20 |
+
# 1024: "lightonai/modernbert-embed-large", # DISABLED (OOM mitigation)
|
| 21 |
}
|
| 22 |
|
| 23 |
+
# _VISION_MODEL_NAME = "nomic-ai/nomic-embed-vision-v1.5" # DISABLED (OOM mitigation)
|
| 24 |
+
# _VISION_DIMENSION = 768
|
| 25 |
|
| 26 |
|
| 27 |
class EmbeddingService:
|
|
|
|
| 37 |
self._device = "cpu"
|
| 38 |
|
| 39 |
self._loaded_dimensions: List[int] = []
|
| 40 |
+
# self._vision_processor: Optional[AutoImageProcessor] = None # DISABLED (OOM mitigation)
|
| 41 |
+
# self._vision_model: Optional[AutoModel] = None # DISABLED (OOM mitigation)
|
| 42 |
+
# self._vision_loaded = False
|
| 43 |
|
| 44 |
def load_model(self, dimension: int) -> None:
|
| 45 |
if dimension in self._models:
|
|
|
|
| 65 |
for dim in _MODEL_MAP:
|
| 66 |
self.load_model(dim)
|
| 67 |
|
| 68 |
+
# def load_vision_model(self) -> None: # DISABLED (OOM mitigation)
|
| 69 |
+
# if self._vision_loaded:
|
| 70 |
+
# return
|
| 71 |
+
# local_path = os.path.join(self._models_dir, "vision")
|
| 72 |
+
# source = local_path if os.path.isdir(local_path) else _VISION_MODEL_NAME
|
| 73 |
+
#
|
| 74 |
+
# cfg_path = os.path.join(local_path, "config.json")
|
| 75 |
+
# if os.path.exists(cfg_path):
|
| 76 |
+
# import json
|
| 77 |
+
# with open(cfg_path) as f:
|
| 78 |
+
# d = json.load(f)
|
| 79 |
+
# if isinstance(d.get("n_inner"), float):
|
| 80 |
+
# d["n_inner"] = int(d["n_inner"])
|
| 81 |
+
# with open(cfg_path, "w") as f:
|
| 82 |
+
# json.dump(d, f, indent=2)
|
| 83 |
+
# _logger.info("Patched vision model config: n_inner float -> int")
|
| 84 |
+
#
|
| 85 |
+
# _logger.info("Loading vision embedding model from %s", source)
|
| 86 |
+
# self._vision_processor = AutoImageProcessor.from_pretrained(source)
|
| 87 |
+
# self._vision_model = AutoModel.from_pretrained(
|
| 88 |
+
# source,
|
| 89 |
+
# trust_remote_code=True,
|
| 90 |
+
# _fast_init=False,
|
| 91 |
+
# )
|
| 92 |
+
# self._vision_model.eval()
|
| 93 |
+
# self._vision_model.to(self._device)
|
| 94 |
+
# self._vision_loaded = True
|
| 95 |
+
# _logger.info("Loaded vision embedding model (device=%s)", self._device)
|
| 96 |
|
| 97 |
def generate_embedding(self, text: List[str], dimension: int) -> List[List[float]]:
|
| 98 |
if dimension not in self._models:
|
|
|
|
| 109 |
)
|
| 110 |
return result.tolist()
|
| 111 |
|
| 112 |
+
# def generate_image_embedding(self, images: List[Image.Image]) -> List[List[float]]: # DISABLED (OOM mitigation)
|
| 113 |
+
# if not self._vision_loaded or self._vision_model is None or self._vision_processor is None:
|
| 114 |
+
# raise ValueError("Vision model not loaded")
|
| 115 |
+
# all_embeddings: List[List[float]] = []
|
| 116 |
+
# with torch.no_grad():
|
| 117 |
+
# for image in images:
|
| 118 |
+
# inputs = self._vision_processor(image, return_tensors="pt")
|
| 119 |
+
# inputs = {k: v.to(self._device) for k, v in inputs.items()}
|
| 120 |
+
# outputs = self._vision_model(**inputs)
|
| 121 |
+
# emb = outputs.last_hidden_state[:, 0]
|
| 122 |
+
# emb = F.normalize(emb, p=2, dim=1)
|
| 123 |
+
# all_embeddings.append(emb.cpu().numpy().flatten().tolist())
|
| 124 |
+
# return all_embeddings
|
| 125 |
|
| 126 |
@property
|
| 127 |
def loaded_dimensions(self) -> List[int]:
|
|
|
|
| 130 |
def is_loaded(self, dimension: int) -> bool:
|
| 131 |
return dimension in self._models
|
| 132 |
|
| 133 |
+
# @property # DISABLED (OOM mitigation)
|
| 134 |
+
# def vision_dimension(self) -> int:
|
| 135 |
+
# return _VISION_DIMENSION
|
requirements.txt
CHANGED
|
@@ -17,7 +17,7 @@ sentence-transformers==5.6.0
|
|
| 17 |
transformers==4.49.0
|
| 18 |
|
| 19 |
torch==2.12.1
|
| 20 |
-
torchvision==0.27.1
|
| 21 |
einops
|
| 22 |
spacy>=3.7.0
|
| 23 |
phonenumbers>=8.13.0
|
|
|
|
| 17 |
transformers==4.49.0
|
| 18 |
|
| 19 |
torch==2.12.1
|
| 20 |
+
# torchvision==0.27.1
|
| 21 |
einops
|
| 22 |
spacy>=3.7.0
|
| 23 |
phonenumbers>=8.13.0
|