Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions spendoo/categorization/routes.py
Original file line number Diff line number Diff line change
@@ -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"])


Expand Down
3 changes: 2 additions & 1 deletion spendoo/categorization/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions spendoo/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
5 changes: 0 additions & 5 deletions spendoo/core/service.py

This file was deleted.

26 changes: 26 additions & 0 deletions spendoo/core/services.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 3 additions & 4 deletions spendoo/ocr/pipeline.py
Original file line number Diff line number Diff line change
@@ -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):

Expand Down
1 change: 0 additions & 1 deletion spendoo/ocr/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from fastapi import APIRouter, UploadFile, HTTPException
import base64
from spendoo.ocr.pipeline import ReceiptPipeline

router = APIRouter(prefix="/ocr", tags=["OCR"])
Expand Down
31 changes: 23 additions & 8 deletions spendoo/voice/routes.py
Original file line number Diff line number Diff line change
@@ -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
27 changes: 24 additions & 3 deletions spendoo/voice/service.py
Original file line number Diff line number Diff line change
@@ -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
Loading