Spaces:
Running
Running
feat: services package
Browse files- services/__init__.py +0 -0
- services/__pycache__/__init__.cpython-311.pyc +0 -0
- services/__pycache__/featherless_provider.cpython-311.pyc +0 -0
- services/__pycache__/hf_provider.cpython-311.pyc +0 -0
- services/__pycache__/ingestion.cpython-311.pyc +0 -0
- services/__pycache__/json_parser.cpython-311.pyc +0 -0
- services/__pycache__/model_router.cpython-311.pyc +0 -0
- services/featherless_provider.py +23 -0
- services/hf_provider.py +54 -0
- services/ingestion.py +51 -0
- services/json_parser.py +23 -0
- services/model_router.py +49 -0
services/__init__.py
ADDED
|
File without changes
|
services/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (178 Bytes). View file
|
|
|
services/__pycache__/featherless_provider.cpython-311.pyc
ADDED
|
Binary file (2.13 kB). View file
|
|
|
services/__pycache__/hf_provider.cpython-311.pyc
ADDED
|
Binary file (5.32 kB). View file
|
|
|
services/__pycache__/ingestion.cpython-311.pyc
ADDED
|
Binary file (3.97 kB). View file
|
|
|
services/__pycache__/json_parser.cpython-311.pyc
ADDED
|
Binary file (1.52 kB). View file
|
|
|
services/__pycache__/model_router.cpython-311.pyc
ADDED
|
Binary file (3.43 kB). View file
|
|
|
services/featherless_provider.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import json
|
| 3 |
+
import urllib.request
|
| 4 |
+
|
| 5 |
+
FEATHERLESS_API_BASE = "https://api.featherless.ai/v1"
|
| 6 |
+
|
| 7 |
+
def generate(model_id: str, prompt: str, system: str = "",
|
| 8 |
+
api_key: str = "", max_tokens: int = 1024, temperature: float = 0.3) -> str:
|
| 9 |
+
"""Featherless.ai (OpenAI-compatible) β Nemotron reasoning tasks."""
|
| 10 |
+
url = f"{FEATHERLESS_API_BASE}/chat/completions"
|
| 11 |
+
messages = []
|
| 12 |
+
if system:
|
| 13 |
+
messages.append({"role": "system", "content": system})
|
| 14 |
+
messages.append({"role": "user", "content": prompt})
|
| 15 |
+
payload = {"model": model_id, "messages": messages,
|
| 16 |
+
"max_tokens": max_tokens, "temperature": temperature}
|
| 17 |
+
data = json.dumps(payload).encode("utf-8")
|
| 18 |
+
headers = {"Content-Type": "application/json",
|
| 19 |
+
"Authorization": f"Bearer {api_key}"}
|
| 20 |
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
| 21 |
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
| 22 |
+
result = json.loads(resp.read().decode("utf-8"))
|
| 23 |
+
return result["choices"][0]["message"]["content"]
|
services/hf_provider.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import json
|
| 3 |
+
import urllib.request
|
| 4 |
+
|
| 5 |
+
HF_API_BASE = "https://api-inference.huggingface.co/models"
|
| 6 |
+
|
| 7 |
+
def generate(model_id: str, prompt: str, system: str = "",
|
| 8 |
+
api_key: str = "", max_tokens: int = 1024, temperature: float = 0.3) -> str:
|
| 9 |
+
"""Text generation β used for MiniCPM concept extraction and Tiny Aya translation."""
|
| 10 |
+
url = f"{HF_API_BASE}/{model_id}"
|
| 11 |
+
messages = []
|
| 12 |
+
if system:
|
| 13 |
+
messages.append({"role": "system", "content": system})
|
| 14 |
+
messages.append({"role": "user", "content": prompt})
|
| 15 |
+
payload = {"inputs": {"messages": messages},
|
| 16 |
+
"parameters": {"max_new_tokens": max_tokens, "temperature": temperature}}
|
| 17 |
+
data = json.dumps(payload).encode("utf-8")
|
| 18 |
+
headers = {"Content-Type": "application/json"}
|
| 19 |
+
if api_key:
|
| 20 |
+
headers["Authorization"] = f"Bearer {api_key}"
|
| 21 |
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
| 22 |
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
| 23 |
+
result = json.loads(resp.read().decode("utf-8"))
|
| 24 |
+
if isinstance(result, list): return result[0].get("generated_text", str(result[0]))
|
| 25 |
+
if isinstance(result, dict): return result.get("generated_text", result.get("text", str(result)))
|
| 26 |
+
return str(result)
|
| 27 |
+
|
| 28 |
+
def vision_generate(model_id: str, image_b64: str, prompt: str,
|
| 29 |
+
api_key: str = "", max_tokens: int = 1024) -> str:
|
| 30 |
+
"""Vision+language β MiniCPM-V for OCR and visual understanding."""
|
| 31 |
+
url = f"{HF_API_BASE}/{model_id}"
|
| 32 |
+
payload = {"inputs": {"image": image_b64, "question": prompt},
|
| 33 |
+
"parameters": {"max_new_tokens": max_tokens}}
|
| 34 |
+
data = json.dumps(payload).encode("utf-8")
|
| 35 |
+
headers = {"Content-Type": "application/json"}
|
| 36 |
+
if api_key:
|
| 37 |
+
headers["Authorization"] = f"Bearer {api_key}"
|
| 38 |
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
| 39 |
+
with urllib.request.urlopen(req, timeout=90) as resp:
|
| 40 |
+
result = json.loads(resp.read().decode("utf-8"))
|
| 41 |
+
if isinstance(result, list): return str(result[0])
|
| 42 |
+
if isinstance(result, dict): return result.get("generated_text", result.get("text", str(result)))
|
| 43 |
+
return str(result)
|
| 44 |
+
|
| 45 |
+
def transcribe(model_id: str, audio_bytes: bytes, api_key: str = "") -> str:
|
| 46 |
+
"""ASR β Whisper via HuggingFace for voice input."""
|
| 47 |
+
url = f"{HF_API_BASE}/{model_id}"
|
| 48 |
+
headers = {"Content-Type": "audio/wav"}
|
| 49 |
+
if api_key:
|
| 50 |
+
headers["Authorization"] = f"Bearer {api_key}"
|
| 51 |
+
req = urllib.request.Request(url, data=audio_bytes, headers=headers, method="POST")
|
| 52 |
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
| 53 |
+
result = json.loads(resp.read().decode("utf-8"))
|
| 54 |
+
return result.get("text", "")
|
services/ingestion.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import os
|
| 3 |
+
import base64
|
| 4 |
+
from typing import Tuple
|
| 5 |
+
|
| 6 |
+
def load_file(path: str) -> Tuple[str, str]:
|
| 7 |
+
"""
|
| 8 |
+
Returns (text, image_b64).
|
| 9 |
+
- text: extracted text layer (empty if image-only)
|
| 10 |
+
- image_b64: base64 image (empty if plain text)
|
| 11 |
+
Callers decide which to use. For PDF: both may be non-empty.
|
| 12 |
+
"""
|
| 13 |
+
if not os.path.exists(path):
|
| 14 |
+
raise FileNotFoundError(f"File not found: {path}")
|
| 15 |
+
ext = os.path.splitext(path)[1].lower()
|
| 16 |
+
if ext in {".txt", ".md"}:
|
| 17 |
+
return _read_text(path), ""
|
| 18 |
+
if ext == ".pdf":
|
| 19 |
+
return _pdf_text(path), _pdf_first_page_b64(path)
|
| 20 |
+
if ext in {".png", ".jpg", ".jpeg", ".webp"}:
|
| 21 |
+
return "", _image_b64(path)
|
| 22 |
+
return _read_text(path), ""
|
| 23 |
+
|
| 24 |
+
def _read_text(path: str) -> str:
|
| 25 |
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
| 26 |
+
return f.read()
|
| 27 |
+
|
| 28 |
+
def _pdf_text(path: str) -> str:
|
| 29 |
+
try:
|
| 30 |
+
import fitz
|
| 31 |
+
doc = fitz.open(path)
|
| 32 |
+
text = "\n".join(page.get_text("text") for page in doc).strip()
|
| 33 |
+
doc.close()
|
| 34 |
+
return text
|
| 35 |
+
except Exception:
|
| 36 |
+
return ""
|
| 37 |
+
|
| 38 |
+
def _pdf_first_page_b64(path: str) -> str:
|
| 39 |
+
try:
|
| 40 |
+
import fitz
|
| 41 |
+
doc = fitz.open(path)
|
| 42 |
+
pix = doc[0].get_pixmap(dpi=150)
|
| 43 |
+
b64 = base64.b64encode(pix.tobytes("png")).decode("utf-8")
|
| 44 |
+
doc.close()
|
| 45 |
+
return b64
|
| 46 |
+
except Exception:
|
| 47 |
+
return ""
|
| 48 |
+
|
| 49 |
+
def _image_b64(path: str) -> str:
|
| 50 |
+
with open(path, "rb") as f:
|
| 51 |
+
return base64.b64encode(f.read()).decode("utf-8")
|
services/json_parser.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import json
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
def extract_json(text: str) -> dict:
|
| 6 |
+
"""
|
| 7 |
+
Robustly extract JSON from LLM response.
|
| 8 |
+
Handles: raw JSON, markdown code fences, leading/trailing prose.
|
| 9 |
+
"""
|
| 10 |
+
text = text.strip()
|
| 11 |
+
text = re.sub(r"```(?:json)?\s*", "", text).replace("```", "")
|
| 12 |
+
try:
|
| 13 |
+
return json.loads(text)
|
| 14 |
+
except json.JSONDecodeError:
|
| 15 |
+
pass
|
| 16 |
+
for pattern in (r"\{[\s\S]*\}", r"\[[\s\S]*\]"):
|
| 17 |
+
match = re.search(pattern, text)
|
| 18 |
+
if match:
|
| 19 |
+
try:
|
| 20 |
+
return json.loads(match.group())
|
| 21 |
+
except json.JSONDecodeError:
|
| 22 |
+
pass
|
| 23 |
+
raise ValueError(f"No valid JSON found in response. First 200 chars: {text[:200]}")
|
services/model_router.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from services import hf_provider, featherless_provider
|
| 3 |
+
|
| 4 |
+
class ModelRouter:
|
| 5 |
+
"""
|
| 6 |
+
Routes tasks to the correct model and provider.
|
| 7 |
+
understand() β MiniCPM-V (HF) β all document tasks
|
| 8 |
+
reason() β Nemotron 4B (Featherless) β all reasoning tasks
|
| 9 |
+
translate() β Tiny Aya (HF) β multilingual
|
| 10 |
+
transcribe() β Whisper (HF) β speech to text
|
| 11 |
+
"""
|
| 12 |
+
def __init__(self, ocr_model: str, reasoning_model: str,
|
| 13 |
+
multilingual_model: str, speech_model: str,
|
| 14 |
+
hf_api_key: str = "", featherless_api_key: str = "",
|
| 15 |
+
max_tokens: int = 1024, temperature: float = 0.3):
|
| 16 |
+
self._ocr_model = ocr_model
|
| 17 |
+
self._reason_model = reasoning_model
|
| 18 |
+
self._multi_model = multilingual_model
|
| 19 |
+
self._speech_model = speech_model
|
| 20 |
+
self._hf_key = hf_api_key
|
| 21 |
+
self._fl_key = featherless_api_key
|
| 22 |
+
self._max_tokens = max_tokens
|
| 23 |
+
self._temperature = temperature
|
| 24 |
+
|
| 25 |
+
def understand(self, prompt: str, image_b64: str = "") -> str:
|
| 26 |
+
"""MiniCPM-V via HuggingFace β OCR, diagram reading, concept extraction."""
|
| 27 |
+
if image_b64:
|
| 28 |
+
return hf_provider.vision_generate(
|
| 29 |
+
self._ocr_model, image_b64, prompt,
|
| 30 |
+
self._hf_key, self._max_tokens)
|
| 31 |
+
return hf_provider.generate(
|
| 32 |
+
self._ocr_model, prompt, api_key=self._hf_key,
|
| 33 |
+
max_tokens=self._max_tokens, temperature=self._temperature)
|
| 34 |
+
|
| 35 |
+
def reason(self, prompt: str, system: str = "") -> str:
|
| 36 |
+
"""Nemotron 3 Nano 4B via Featherless β quests, questions, tutor."""
|
| 37 |
+
return featherless_provider.generate(
|
| 38 |
+
self._reason_model, prompt, system,
|
| 39 |
+
self._fl_key, self._max_tokens, self._temperature)
|
| 40 |
+
|
| 41 |
+
def translate(self, prompt: str, system: str = "") -> str:
|
| 42 |
+
"""Tiny Aya 3.3B via HuggingFace β multilingual explanations."""
|
| 43 |
+
return hf_provider.generate(
|
| 44 |
+
self._multi_model, prompt, system,
|
| 45 |
+
self._hf_key, self._max_tokens, self._temperature)
|
| 46 |
+
|
| 47 |
+
def transcribe(self, audio_bytes: bytes) -> str:
|
| 48 |
+
"""Whisper via HuggingFace β speech to text."""
|
| 49 |
+
return hf_provider.transcribe(self._speech_model, audio_bytes, self._hf_key)
|