Soumik-404 commited on
Commit
4b54fab
·
1 Parent(s): a70f425

feat: oom

Browse files
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
- # FIXED: Removed all line breaks and spaces inside the python3 -c execution string to prevent IndentationError
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 all models)...")
41
  loop = asyncio.get_running_loop()
42
- await loop.run_in_executor(None, _embedding_service.load_all_models)
43
- await loop.run_in_executor(None, _embedding_service.load_vision_model)
44
- _logger.info("Embedding service initialized with dims: %s, vision=%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
- # 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:
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,185 +126,185 @@ async def create_embeddings(
126
  )
127
 
128
 
129
- @router.post(
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(
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
- )
 
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=1024, description="Target embedding dimension (384, 768, or 1024)")
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
- 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"]
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
- (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,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
- 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,14 +131,14 @@ class CodeExecutorService:
131
  "language": language, "timed_out": False,
132
  }
133
 
134
- if language == "java":
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,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"), ("java", "javac")]:
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"}[language]
 
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
- return ["sh", "-c",
255
- f"cd {run_dir} && javac Main.java 2>&1 && java -XX:CompressedClassSpaceSize=64m -Xmx96m Main 2>&1"]
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
- 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:
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:
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:
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,26 +193,26 @@ class ConverterService:
193
 
194
  start = time.perf_counter()
195
 
196
- try:
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()
 
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
- # NOTE: Fixed model mapping to align nomic-embed-text-v1.5 (dim=768) with nomic-embed-vision-v1.5 (dim=768).
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
- if self._vision_loaded:
71
- return
72
- local_path = os.path.join(self._models_dir, "vision")
73
- source = local_path if os.path.isdir(local_path) else _VISION_MODEL_NAME
74
-
75
- cfg_path = os.path.join(local_path, "config.json")
76
- if os.path.exists(cfg_path):
77
- import json
78
- with open(cfg_path) as f:
79
- d = json.load(f)
80
- if isinstance(d.get("n_inner"), float):
81
- d["n_inner"] = int(d["n_inner"])
82
- with open(cfg_path, "w") as f:
83
- json.dump(d, f, indent=2)
84
- _logger.info("Patched vision model config: n_inner float -> int")
85
-
86
- _logger.info("Loading vision embedding model from %s", source)
87
- self._vision_processor = AutoImageProcessor.from_pretrained(source)
88
- self._vision_model = AutoModel.from_pretrained(
89
- source,
90
- trust_remote_code=True,
91
- _fast_init=False,
92
- )
93
- self._vision_model.eval()
94
- self._vision_model.to(self._device)
95
- self._vision_loaded = True
96
- _logger.info("Loaded vision embedding model (device=%s)", self._device)
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
- if not self._vision_loaded or self._vision_model is None or self._vision_processor is None:
115
- raise ValueError("Vision model not loaded")
116
- all_embeddings: List[List[float]] = []
117
- with torch.no_grad():
118
- for image in images:
119
- inputs = self._vision_processor(image, return_tensors="pt")
120
- inputs = {k: v.to(self._device) for k, v in inputs.items()}
121
- outputs = self._vision_model(**inputs)
122
- emb = outputs.last_hidden_state[:, 0]
123
- emb = F.normalize(emb, p=2, dim=1)
124
- all_embeddings.append(emb.cpu().numpy().flatten().tolist())
125
- return all_embeddings
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
- return _VISION_DIMENSION
 
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