diff --git a/spendoo/categorization/routes.py b/spendoo/categorization/routes.py index 78fdd7e..a12ae1b 100644 --- a/spendoo/categorization/routes.py +++ b/spendoo/categorization/routes.py @@ -1,8 +1,8 @@ +from spendoo.core.services import ServiceContainer from .models import CategorizationRequest, TransactionExtractionResponse -from spendoo.categorization.service import CategorizationService from fastapi import APIRouter -service = CategorizationService() +service = ServiceContainer.get_categorization_service() router = APIRouter(prefix="/categorization", tags=["categorization"]) diff --git a/spendoo/categorization/service.py b/spendoo/categorization/service.py index cfa2a1c..f391113 100644 --- a/spendoo/categorization/service.py +++ b/spendoo/categorization/service.py @@ -26,8 +26,9 @@ def extract(self, text: str): For each item try to return: - id (incremental starting from 1 in order of appearance) - - item_name + - item_name exactly as it appears in the text (avoid interpreting it, just extract the name) but without any additional descriptions or measurements if they are mentioned in the same line. - price + - category (choose one category from the list below that best fits the item based on its description, if you are unsure make it null) - category_id (choose one id from the list below) Available categories: diff --git a/spendoo/core/config.py b/spendoo/core/config.py index 67f548e..75feca5 100644 --- a/spendoo/core/config.py +++ b/spendoo/core/config.py @@ -6,4 +6,7 @@ class Settings: GROQ_API_KEY: str = os.getenv("GROQ_API_KEY") MISTRAL_API_KEY: str = os.getenv("MISTRAL_API_KEY") + VOICE_ALLOWED_EXTENSIONS: set[str] = {".wav", ".mp3", ".ogg", ".m4a", ".flac", ".webm"} + VOICE_MAX_SIZE: int = 25 * 1024 * 1024 + settings = Settings() \ No newline at end of file diff --git a/spendoo/core/service.py b/spendoo/core/service.py deleted file mode 100644 index 8c6386e..0000000 --- a/spendoo/core/service.py +++ /dev/null @@ -1,5 +0,0 @@ -# Core service placeholder - -def get_status(): - return {'module': 'core', 'status': 'ok'} - diff --git a/spendoo/core/services.py b/spendoo/core/services.py new file mode 100644 index 0000000..a3b1a50 --- /dev/null +++ b/spendoo/core/services.py @@ -0,0 +1,26 @@ +from spendoo.ocr.service import OCRService +from spendoo.categorization.service import CategorizationService +from spendoo.voice.service import VoiceService + + +class ServiceContainer: + _voice_service = None + _ocr_service = None + _categorization_service = None + @classmethod + def get_voice_service(cls) -> VoiceService: + if cls._voice_service is None: + cls._voice_service = VoiceService() + return cls._voice_service + + @classmethod + def get_ocr_service(cls) -> OCRService: + if cls._ocr_service is None: + cls._ocr_service = OCRService() + return cls._ocr_service + + @classmethod + def get_categorization_service(cls) -> CategorizationService: + if cls._categorization_service is None: + cls._categorization_service = CategorizationService() + return cls._categorization_service \ No newline at end of file diff --git a/spendoo/ocr/pipeline.py b/spendoo/ocr/pipeline.py index add9955..022518e 100644 --- a/spendoo/ocr/pipeline.py +++ b/spendoo/ocr/pipeline.py @@ -1,13 +1,12 @@ -from spendoo.ocr.service import OCRService -from spendoo.categorization.service import CategorizationService +from spendoo.core.services import ServiceContainer import base64 class ReceiptPipeline: def __init__(self): - self.ocr = OCRService() - self.extractor = CategorizationService() + self.ocr = ServiceContainer.get_ocr_service() + self.extractor = ServiceContainer.get_categorization_service() def process_receipt(self, image_bytes): diff --git a/spendoo/ocr/routes.py b/spendoo/ocr/routes.py index 9070e48..5e69ece 100644 --- a/spendoo/ocr/routes.py +++ b/spendoo/ocr/routes.py @@ -1,5 +1,4 @@ from fastapi import APIRouter, UploadFile, HTTPException -import base64 from spendoo.ocr.pipeline import ReceiptPipeline router = APIRouter(prefix="/ocr", tags=["OCR"]) diff --git a/spendoo/voice/routes.py b/spendoo/voice/routes.py index 7a1b4b6..5c072dd 100644 --- a/spendoo/voice/routes.py +++ b/spendoo/voice/routes.py @@ -1,13 +1,28 @@ -from fastapi import APIRouter +from fastapi import APIRouter, UploadFile, HTTPException +from spendoo.core.services import ServiceContainer +import os +from spendoo.core.config import settings -router = APIRouter(prefix="/voice", tags=["voice"]) +router = APIRouter(prefix="/voice", tags=["Voice"]) +voice_service = ServiceContainer.get_voice_service() +categorization_service = ServiceContainer.get_categorization_service() -@router.get("/") -def info(): - return {"module": "voice", "status": "ok"} +@router.post("/process") +async def process_voice(file: UploadFile): + ext = os.path.splitext(file.filename)[1].lower() -@router.get("/health") -def health(): - return {"status": "ok"} + if ext not in settings.VOICE_ALLOWED_EXTENSIONS: + raise HTTPException(400, "Unsupported audio format") + + audio_bytes = await file.read() + + if len(audio_bytes) > settings.VOICE_MAX_SIZE: + raise HTTPException(400, "Audio file too large (max 25MB)") + + text = voice_service.transcribe(audio_bytes, file.filename) + + result = categorization_service.extract(text) + + return result \ No newline at end of file diff --git a/spendoo/voice/service.py b/spendoo/voice/service.py index 62ae996..049bea3 100644 --- a/spendoo/voice/service.py +++ b/spendoo/voice/service.py @@ -1,5 +1,26 @@ -# Voice service placeholder +from groq import Groq +from spendoo.core.config import settings -def get_status(): - return {'module': 'voice', 'status': 'ok'} +class VoiceService: + + def __init__(self): + self.client = Groq(api_key=settings.GROQ_API_KEY) + + def transcribe(self, audio_bytes: bytes, filename: str) -> str: + """ + Convert audio → text using Whisper + """ + + transcription = self.client.audio.transcriptions.create( + file=(filename, audio_bytes), + model="whisper-large-v3", + + prompt=""" + Specify context and highlight on different languages used + """, + + temperature=0 + ) + + return transcription.text \ No newline at end of file