116 lines
5.9 KiB
Python
116 lines
5.9 KiB
Python
# backend/core/utils.py
|
|
|
|
import torch
|
|
import gc
|
|
import re
|
|
import json
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, Any, Optional
|
|
from backend.config import user_db, item_db # These are now mock DBs, kept for autofill logic
|
|
from backend.models import InvoiceData, InvoiceItem, ValidationError # ValidationError for type hinting/catching
|
|
|
|
def clear_gpu_memory():
|
|
"""Clears CUDA memory cache and runs garbage collection."""
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
def autofill_invoice_data(invoice_data: InvoiceData) -> InvoiceData:
|
|
"""
|
|
Autofills invoice data based on simulated local databases (user_db, item_db).
|
|
Fills in missing client address, item prices, and calculates totals/dates.
|
|
|
|
Args:
|
|
invoice_data (InvoiceData): The invoice data Pydantic model to autofill.
|
|
|
|
Returns:
|
|
InvoiceData: The updated invoice data with autofilled information.
|
|
"""
|
|
# Create a mutable copy to avoid modifying the original object in place
|
|
# if this function is called on an object that might be reused or
|
|
# that came directly from a FastAPI request body.
|
|
invoice_data_copy = invoice_data.model_copy(deep=True)
|
|
|
|
if invoice_data_copy.client_name:
|
|
normalized_client_name = invoice_data_copy.client_name.lower().strip()
|
|
if normalized_client_name in user_db:
|
|
client_info = user_db[normalized_client_name]
|
|
if not invoice_data_copy.client_address:
|
|
invoice_data_copy.client_address = client_info.get("address")
|
|
# Only update tax_rate if it's not explicitly set in the incoming data
|
|
if invoice_data_copy.tax_rate is None or invoice_data_copy.tax_rate == 0.08: # Default value
|
|
invoice_data_copy.tax_rate = client_info.get("default_tax_rate", 0.08)
|
|
|
|
for item in invoice_data_copy.items:
|
|
# If unit_price is missing but description exists, try to autofill
|
|
if not item.unit_price and item.description:
|
|
normalized_description = item.description.lower().strip()
|
|
for db_item, db_info in item_db.items():
|
|
if db_item in normalized_description: # Simple substring match
|
|
item.unit_price = db_info.get("unit_price")
|
|
# Break after finding the first match
|
|
break
|
|
# Ensure item total is calculated or re-calculated after autofill of unit_price
|
|
if item.quantity is not None and item.unit_price is not None:
|
|
item.total = round(item.quantity * item.unit_price, 2)
|
|
elif item.total is None: # If no quantity/unit_price but total needed
|
|
item.total = 0.0 # Default to 0 if not enough info
|
|
|
|
# Recalculate totals after autofilling item prices and ensure they are not None
|
|
invoice_data_copy.subtotal = round(sum(item.total for item in invoice_data_copy.items if item.total is not None), 2)
|
|
effective_tax_rate = invoice_data_copy.tax_rate if invoice_data_copy.tax_rate is not None else 0.0
|
|
invoice_data_copy.tax_amount = round(invoice_data_copy.subtotal * effective_tax_rate, 2)
|
|
invoice_data_copy.grand_total = round(invoice_data_copy.subtotal + invoice_data_copy.tax_amount, 2)
|
|
|
|
# Handle dates
|
|
if not invoice_data_copy.invoice_date:
|
|
invoice_data_copy.invoice_date = datetime.now().strftime("%Y-%m-%d")
|
|
if not invoice_data_copy.due_date:
|
|
try:
|
|
invoice_date_dt = datetime.strptime(invoice_data_copy.invoice_date, "%Y-%m-%d")
|
|
invoice_data_copy.due_date = (invoice_date_dt + timedelta(days=30)).strftime("%Y-%m-%d")
|
|
except (ValueError, TypeError):
|
|
# Fallback if invoice_date is invalid or not set
|
|
invoice_data_copy.due_date = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d")
|
|
|
|
|
|
return invoice_data_copy
|
|
|
|
def check_model_devices(qwen2_audio_model: Optional[Any], device: str) -> str:
|
|
"""
|
|
Checks the device placement and memory usage of the Qwen2-Audio model.
|
|
"""
|
|
status_output = []
|
|
status_output.append("--- Checking Model Device Placement ---")
|
|
|
|
if qwen2_audio_model:
|
|
try:
|
|
if hasattr(qwen2_audio_model, 'hf_device_map') and qwen2_audio_model.hf_device_map:
|
|
status_output.append(f"Qwen2-Audio model device map: {qwen2_audio_model.hf_device_map}")
|
|
else:
|
|
qwen_device = next(qwen2_audio_model.parameters()).device
|
|
status_output.append(f"Qwen2-Audio model (first parameter) is on: {qwen_device}")
|
|
|
|
status_output.append(f"Qwen2-Audio model data type: {next(qwen2_audio_model.parameters()).dtype}")
|
|
|
|
if device == "cuda":
|
|
status_output.append(f"Current GPU Memory Allocated: {torch.cuda.memory_allocated() / (1024**3):.2f} GB")
|
|
status_output.append(f"Current GPU Memory Cached: {torch.cuda.memory_reserved() / (1024**3):.2f} GB")
|
|
status_output.append(f"Max GPU Memory Allocated: {torch.cuda.max_memory_allocated() / (1024**3):.2f} GB")
|
|
status_output.append(f"Max GPU Memory Cached: {torch.cuda.max_memory_reserved() / (1024**3):.2f} GB")
|
|
except Exception as e:
|
|
status_output.append(f"Error checking model device: {e}")
|
|
else:
|
|
status_output.append("Qwen2-Audio model is not loaded.")
|
|
|
|
status_output.append(f"System detected device: {device}")
|
|
if device == "cuda":
|
|
status_output.append(f"Number of CUDA devices: {torch.cuda.device_count()}")
|
|
if torch.cuda.device_count() > 0:
|
|
status_output.append(f"Current CUDA device: {torch.cuda.current_device()} ({torch.cuda.get_device_name(torch.cuda.current_device())})")
|
|
status_output.append(f"Total GPU Memory on current device: {torch.cuda.get_device_properties(torch.cuda.current_device()).total_memory / (1024**3):.2f} GB")
|
|
else:
|
|
status_output.append("No CUDA devices detected. Running on CPU.")
|
|
|
|
return "\n".join(status_output)
|