Initial commit for fresh VoiceInvoiceAI repo

This commit is contained in:
Hardik Sankhla
2025-06-25 23:03:48 +05:30
commit 6fe387d900
13 changed files with 1113 additions and 0 deletions

238
backend/main.py Normal file
View File

@@ -0,0 +1,238 @@
# backend/main.py
from fastapi import FastAPI, UploadFile, File, HTTPException, status
from fastapi.responses import FileResponse, JSONResponse
from typing import Optional
import os
import shutil
import uuid # For generating unique IDs for uploaded files
from io import BytesIO
from backend.models import InvoiceData
from backend.config import MINIO_AUDIO_BUCKET, MINIO_PDF_BUCKET
# Import the llm_service module itself, not its internal global variables directly
import backend.services.llm_service as llm_service
from backend.services.pdf_service import generate_invoice_pdf
from backend.core.utils import check_model_devices
from backend.services.storage_service import minio_storage_service # Import the MinIO service instance
app = FastAPI(
title="Voice-Powered Invoice Generator Backend",
description="API for processing audio requests to generate structured invoice data and PDF invoices, with MinIO storage.",
version="0.1.0"
)
# Startup event: Initialize MinIO client and ensure buckets exist, load LLM model
@app.on_event("startup")
async def startup_event():
"""
Initializes MinIO client (which in turn ensures buckets exist)
and loads the Qwen2-Audio LLM model on application startup.
"""
print("Application startup: Initializing services...")
try:
# MinIO storage service is instantiated globally in storage_service.py
# Its __init__ method will ensure buckets exist.
_ = minio_storage_service
print("MinIO storage service initialized and buckets ensured.")
# Call the loading function within the llm_service module
llm_service.load_qwen2_audio_model()
except Exception as e:
print(f"CRITICAL ERROR during startup: {e}")
# Depending on criticality, you might want to exit or log more severely
# For now, just print and allow app to start, but subsequent calls will fail.
@app.get("/")
async def root():
"""Root endpoint providing a welcome message."""
return {"message": "Welcome to the Voice-Powered Invoice Generator Backend. Visit /docs for API documentation."}
@app.post("/load_model/", summary="Load the Qwen2-Audio LLM Model")
async def load_model_endpoint():
"""
Explicitly loads the Qwen2-Audio model and processor into memory.
This can be called if the model failed to load at startup or for re-initialization.
"""
# Call the loading function within the llm_service module
status_message = llm_service.load_qwen2_audio_model()
return {"status": status_message}
@app.get("/model_status/", summary="Check Qwen2-Audio Model Device and Memory Status")
async def get_model_status():
"""
Provides detailed information about the Qwen2-Audio model's device placement
and GPU memory usage if running on CUDA.
"""
# Access the global variables directly from the llm_service module
status_message = check_model_devices(llm_service.qwen2_audio_model, llm_service.device)
return {"status": status_message}
@app.post("/generate_invoice_from_audio/", response_model=dict, summary="Generate Invoice from Audio Input")
async def generate_invoice_from_audio_endpoint(
audio_file: UploadFile = File(..., description="The audio file (e.g., MP3, WAV) containing the invoice request."),
transcript_text: Optional[str] = File(None, description="Optional pre-provided transcript of the audio.")
):
"""
Receives an audio file, uploads it to MinIO, uses the Qwen2-Audio model to extract invoice data,
autofills missing details, generates a PDF invoice, uploads the PDF to MinIO,
and returns the structured data along with the PDF's MinIO path.
Returns:
JSONResponse: Contains the extracted InvoiceData and the MinIO object name for the PDF.
"""
# Check the global variables directly from the llm_service module
if llm_service.qwen2_audio_model is None or llm_service.qwen2_audio_processor is None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Qwen2-Audio model is not loaded. Please call /load_model/ first."
)
# Generate a unique object name for the audio file in MinIO
file_extension = os.path.splitext(audio_file.filename)[1]
audio_object_name = f"audio-{uuid.uuid4()}{file_extension}"
try:
# 1. Read audio content into BytesIO
audio_content = await audio_file.read()
audio_bytesio = BytesIO(audio_content)
# 2. Upload audio to MinIO
minio_storage_service.upload_file(
bucket_name=MINIO_AUDIO_BUCKET,
object_name=audio_object_name,
data=audio_bytesio,
length=len(audio_content),
content_type=audio_file.content_type
)
print(f"Audio uploaded to MinIO: {MINIO_AUDIO_BUCKET}/{audio_object_name}")
# 3. Process audio with LLM to extract invoice data (LLM service will download from MinIO internally)
# Call the function from the llm_service module
invoice_data = await llm_service.extract_and_validate_invoice_data(audio_object_name, transcript_text)
# 4. Generate PDF and upload to MinIO
pdf_object_name = generate_invoice_pdf(invoice_data)
print(f"PDF generated and uploaded to MinIO: {MINIO_PDF_BUCKET}/{pdf_object_name}")
return JSONResponse(content={
"invoice_data": invoice_data.model_dump(),
"audio_object_name": audio_object_name, # Return the MinIO path for the uploaded audio
"pdf_object_name": pdf_object_name, # Return the MinIO path for the generated PDF
"message": "Invoice generated successfully."
})
except FileNotFoundError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except RuntimeError as e:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Service error: {e}")
except Exception as e:
# Catch-all for unexpected errors
print(f"Unexpected error in generate_invoice_from_audio_endpoint: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An unexpected error occurred: {e}")
@app.post("/generate_invoice_from_data/", response_model=dict, summary="Generate Invoice from Structured Data")
async def generate_invoice_from_data_endpoint(invoice_data: InvoiceData):
"""
Generates a PDF invoice directly from provided structured invoice data and uploads it to MinIO.
This endpoint can be used if the frontend already has the parsed invoice details.
Args:
invoice_data (InvoiceData): The structured invoice data.
Returns:
JSONResponse: Contains the processed InvoiceData and the MinIO object name for the PDF.
"""
try:
# Create a mutable copy to apply autofill without affecting the original request payload
invoice_data_processed = invoice_data.model_copy(deep=True)
from backend.core.utils import autofill_invoice_data
invoice_data_processed = autofill_invoice_data(invoice_data_processed)
# Generate PDF and upload to MinIO
pdf_object_name = generate_invoice_pdf(invoice_data_processed)
print(f"PDF generated and uploaded to MinIO: {MINIO_PDF_BUCKET}/{pdf_object_name}")
return JSONResponse(content={
"invoice_data": invoice_data_processed.model_dump(),
"pdf_object_name": pdf_object_name,
"message": "Invoice PDF generated from provided data and stored in MinIO."
})
except Exception as e:
print(f"Error in generate_invoice_from_data_endpoint: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error generating invoice from data: {e}")
@app.get("/download_invoice/{object_name:path}", summary="Download Generated Invoice PDF from MinIO")
async def download_invoice(object_name: str):
"""
Downloads a previously generated PDF invoice from MinIO by its object name (path within the bucket).
Args:
object_name (str): The full object name (e.g., 'invoice_client_INV-2025-XXX.pdf') of the PDF in the MinIO bucket.
Returns:
FileResponse: The PDF file.
"""
try:
# Check if the object exists in MinIO
if not minio_storage_service.object_exists(MINIO_PDF_BUCKET, object_name):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Invoice PDF '{object_name}' not found in MinIO bucket '{MINIO_PDF_BUCKET}'.")
# Download the file from MinIO
pdf_data_bytesio = minio_storage_service.download_file(MINIO_PDF_BUCKET, object_name)
# Determine the filename for the client download
filename_for_download = os.path.basename(object_name)
return FileResponse(
content=pdf_data_bytesio.getvalue(), # Get the content bytes from BytesIO
media_type="application/pdf",
filename=filename_for_download,
headers={"Content-Disposition": f"attachment; filename={filename_for_download}"}
)
except FileNotFoundError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except Exception as e:
print(f"Error downloading invoice from MinIO: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An error occurred while downloading the PDF: {e}")
@app.get("/get_audio/{object_name:path}", summary="Get Audio Input from MinIO")
async def get_audio(object_name: str):
"""
Retrieves a previously uploaded audio input file from MinIO by its object name.
Args:
object_name (str): The full object name (e.g., 'audio-UUID.wav') of the audio in the MinIO bucket.
Returns:
FileResponse: The audio file.
"""
try:
# Check if the object exists in MinIO
if not minio_storage_service.object_exists(MINIO_AUDIO_BUCKET, object_name):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Audio file '{object_name}' not found in MinIO bucket '{MINIO_AUDIO_BUCKET}'.")
# Download the file from MinIO
audio_data_bytesio = minio_storage_service.download_file(MINIO_AUDIO_BUCKET, object_name)
# Determine the filename for the client download
filename_for_download = os.path.basename(object_name)
# Attempt to infer content type; fallback to octet-stream
content_type = "application/octet-stream"
if filename_for_download.lower().endswith(".wav"):
content_type = "audio/wav"
elif filename_for_download.lower().endswith(".mp3"):
content_type = "audio/mpeg" # or audio/mp3
return FileResponse(
content=audio_data_bytesio.getvalue(),
media_type=content_type,
filename=filename_for_download,
headers={"Content-Disposition": f"inline; filename={filename_for_download}"} # Use inline to play in browser
)
except FileNotFoundError as e:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
except Exception as e:
print(f"Error getting audio from MinIO: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"An error occurred while retrieving the audio: {e}")