Spaces:
Sleeping
Sleeping
Ajit Panday
commited on
Commit
·
c6d0d33
1
Parent(s):
6d5c3a1
Initial commit with complete vBot implementation
Browse files- app/auth.py +139 -1
- app/models.py +24 -2
- asterisk_dialplan.conf +14 -0
- main.py +73 -21
- requirements.txt +6 -1
app/auth.py
CHANGED
|
@@ -1 +1,139 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 2 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from jose import JWTError, jwt
|
| 6 |
+
from passlib.context import CryptContext
|
| 7 |
+
from typing import Optional
|
| 8 |
+
import secrets
|
| 9 |
+
import os
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from . import models
|
| 12 |
+
|
| 13 |
+
# Load environment variables
|
| 14 |
+
load_dotenv()
|
| 15 |
+
|
| 16 |
+
# Security configuration
|
| 17 |
+
SECRET_KEY = os.getenv("JWT_SECRET", "your-secret-key")
|
| 18 |
+
ALGORITHM = "HS256"
|
| 19 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
| 20 |
+
|
| 21 |
+
# Password hashing
|
| 22 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 23 |
+
|
| 24 |
+
# OAuth2 scheme
|
| 25 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
| 26 |
+
|
| 27 |
+
router = APIRouter()
|
| 28 |
+
|
| 29 |
+
# Admin user credentials (in production, use database)
|
| 30 |
+
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
|
| 31 |
+
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin")
|
| 32 |
+
|
| 33 |
+
def verify_password(plain_password, hashed_password):
|
| 34 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 35 |
+
|
| 36 |
+
def get_password_hash(password):
|
| 37 |
+
return pwd_context.hash(password)
|
| 38 |
+
|
| 39 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
| 40 |
+
to_encode = data.copy()
|
| 41 |
+
if expires_delta:
|
| 42 |
+
expire = datetime.utcnow() + expires_delta
|
| 43 |
+
else:
|
| 44 |
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
| 45 |
+
to_encode.update({"exp": expire})
|
| 46 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 47 |
+
return encoded_jwt
|
| 48 |
+
|
| 49 |
+
async def get_current_admin(token: str = Depends(oauth2_scheme)):
|
| 50 |
+
credentials_exception = HTTPException(
|
| 51 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 52 |
+
detail="Could not validate credentials",
|
| 53 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 54 |
+
)
|
| 55 |
+
try:
|
| 56 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 57 |
+
username: str = payload.get("sub")
|
| 58 |
+
if username is None:
|
| 59 |
+
raise credentials_exception
|
| 60 |
+
except JWTError:
|
| 61 |
+
raise credentials_exception
|
| 62 |
+
if username != ADMIN_USERNAME:
|
| 63 |
+
raise credentials_exception
|
| 64 |
+
return username
|
| 65 |
+
|
| 66 |
+
@router.post("/token")
|
| 67 |
+
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
| 68 |
+
if form_data.username != ADMIN_USERNAME or form_data.password != ADMIN_PASSWORD:
|
| 69 |
+
raise HTTPException(
|
| 70 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 71 |
+
detail="Incorrect username or password",
|
| 72 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 73 |
+
)
|
| 74 |
+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 75 |
+
access_token = create_access_token(
|
| 76 |
+
data={"sub": form_data.username}, expires_delta=access_token_expires
|
| 77 |
+
)
|
| 78 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
| 79 |
+
|
| 80 |
+
@router.post("/customers/", response_model=dict)
|
| 81 |
+
async def create_customer(
|
| 82 |
+
customer_data: dict,
|
| 83 |
+
db: Session = Depends(models.get_db),
|
| 84 |
+
current_admin: str = Depends(get_current_admin)
|
| 85 |
+
):
|
| 86 |
+
# Generate API key
|
| 87 |
+
api_key = secrets.token_urlsafe(32)
|
| 88 |
+
|
| 89 |
+
# Create new customer
|
| 90 |
+
customer = models.Customer(
|
| 91 |
+
name=customer_data["name"],
|
| 92 |
+
company_name=customer_data["company_name"],
|
| 93 |
+
email=customer_data["email"],
|
| 94 |
+
api_key=api_key
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
db.add(customer)
|
| 98 |
+
db.commit()
|
| 99 |
+
db.refresh(customer)
|
| 100 |
+
|
| 101 |
+
return {
|
| 102 |
+
"id": customer.id,
|
| 103 |
+
"name": customer.name,
|
| 104 |
+
"company_name": customer.company_name,
|
| 105 |
+
"email": customer.email,
|
| 106 |
+
"api_key": customer.api_key
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
@router.get("/customers/", response_model=list)
|
| 110 |
+
async def list_customers(
|
| 111 |
+
db: Session = Depends(models.get_db),
|
| 112 |
+
current_admin: str = Depends(get_current_admin)
|
| 113 |
+
):
|
| 114 |
+
customers = db.query(models.Customer).all()
|
| 115 |
+
return customers
|
| 116 |
+
|
| 117 |
+
@router.get("/customers/{customer_id}", response_model=dict)
|
| 118 |
+
async def get_customer(
|
| 119 |
+
customer_id: int,
|
| 120 |
+
db: Session = Depends(models.get_db),
|
| 121 |
+
current_admin: str = Depends(get_current_admin)
|
| 122 |
+
):
|
| 123 |
+
customer = db.query(models.Customer).filter(models.Customer.id == customer_id).first()
|
| 124 |
+
if not customer:
|
| 125 |
+
raise HTTPException(status_code=404, detail="Customer not found")
|
| 126 |
+
return customer
|
| 127 |
+
|
| 128 |
+
@router.delete("/customers/{customer_id}")
|
| 129 |
+
async def delete_customer(
|
| 130 |
+
customer_id: int,
|
| 131 |
+
db: Session = Depends(models.get_db),
|
| 132 |
+
current_admin: str = Depends(get_current_admin)
|
| 133 |
+
):
|
| 134 |
+
customer = db.query(models.Customer).filter(models.Customer.id == customer_id).first()
|
| 135 |
+
if not customer:
|
| 136 |
+
raise HTTPException(status_code=404, detail="Customer not found")
|
| 137 |
+
db.delete(customer)
|
| 138 |
+
db.commit()
|
| 139 |
+
return {"message": "Customer deleted successfully"}
|
app/models.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
from sqlalchemy import Column, Integer, String, DateTime, create_engine
|
| 2 |
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
-
from sqlalchemy.orm import sessionmaker
|
| 4 |
from datetime import datetime
|
| 5 |
import os
|
| 6 |
from dotenv import load_dotenv
|
|
@@ -17,16 +17,38 @@ engine = create_engine(DATABASE_URL)
|
|
| 17 |
# Create declarative base
|
| 18 |
Base = declarative_base()
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
class CallRecord(Base):
|
| 21 |
__tablename__ = "call_records"
|
| 22 |
|
| 23 |
id = Column(String(36), primary_key=True)
|
|
|
|
|
|
|
|
|
|
| 24 |
file_path = Column(String(255))
|
| 25 |
transcription = Column(String(10000))
|
|
|
|
| 26 |
sentiment = Column(String(20))
|
| 27 |
keywords = Column(String(1000))
|
| 28 |
created_at = Column(DateTime, default=datetime.utcnow)
|
| 29 |
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# Create all tables
|
| 32 |
Base.metadata.create_all(engine)
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, create_engine
|
| 2 |
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
+
from sqlalchemy.orm import sessionmaker, relationship
|
| 4 |
from datetime import datetime
|
| 5 |
import os
|
| 6 |
from dotenv import load_dotenv
|
|
|
|
| 17 |
# Create declarative base
|
| 18 |
Base = declarative_base()
|
| 19 |
|
| 20 |
+
class Customer(Base):
|
| 21 |
+
__tablename__ = "customers"
|
| 22 |
+
|
| 23 |
+
id = Column(Integer, primary_key=True)
|
| 24 |
+
name = Column(String(100), nullable=False)
|
| 25 |
+
company_name = Column(String(100), nullable=False)
|
| 26 |
+
email = Column(String(100), unique=True, nullable=False)
|
| 27 |
+
api_key = Column(String(64), unique=True, nullable=False)
|
| 28 |
+
is_active = Column(Boolean, default=True)
|
| 29 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 30 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 31 |
+
|
| 32 |
+
# Relationship with call records
|
| 33 |
+
call_records = relationship("CallRecord", back_populates="customer")
|
| 34 |
+
|
| 35 |
class CallRecord(Base):
|
| 36 |
__tablename__ = "call_records"
|
| 37 |
|
| 38 |
id = Column(String(36), primary_key=True)
|
| 39 |
+
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False)
|
| 40 |
+
caller_number = Column(String(20), nullable=False)
|
| 41 |
+
called_number = Column(String(20), nullable=False)
|
| 42 |
file_path = Column(String(255))
|
| 43 |
transcription = Column(String(10000))
|
| 44 |
+
summary = Column(String(1000))
|
| 45 |
sentiment = Column(String(20))
|
| 46 |
keywords = Column(String(1000))
|
| 47 |
created_at = Column(DateTime, default=datetime.utcnow)
|
| 48 |
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 49 |
+
|
| 50 |
+
# Relationship with customer
|
| 51 |
+
customer = relationship("Customer", back_populates="call_records")
|
| 52 |
|
| 53 |
# Create all tables
|
| 54 |
Base.metadata.create_all(engine)
|
asterisk_dialplan.conf
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[from-internal]
|
| 2 |
+
; Replace YOUR_API_KEY with the API key provided by vBot
|
| 3 |
+
exten => _X.,1,Answer()
|
| 4 |
+
same => n,Set(CALLERID=${CALLERID(num)})
|
| 5 |
+
same => n,Set(CALLED=${EXTEN})
|
| 6 |
+
same => n,Record(/tmp/call-${UNIQUEID}.wav,0,30)
|
| 7 |
+
same => n,System(curl -X POST "https://your-huggingface-space-url/api/v1/process-call" \
|
| 8 |
+
-H "api-key: YOUR_API_KEY" \
|
| 9 |
+
-H "caller-number: ${CALLERID}" \
|
| 10 |
+
-H "called-number: ${CALLED}" \
|
| 11 |
+
-F "file=@/tmp/call-${UNIQUEID}.wav")
|
| 12 |
+
same => n,System(rm /tmp/call-${UNIQUEID}.wav)
|
| 13 |
+
same => n,Dial(SIP/${EXTEN})
|
| 14 |
+
same => n,Hangup()
|
main.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
| 1 |
-
from fastapi import FastAPI, File, UploadFile, HTTPException, Depends
|
| 2 |
-
from fastapi.security import OAuth2PasswordBearer
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 4 |
from datetime import datetime
|
| 5 |
import os
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
-
from typing import
|
| 8 |
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# Load environment variables
|
| 11 |
load_dotenv()
|
|
@@ -26,24 +31,33 @@ app.add_middleware(
|
|
| 26 |
allow_headers=["*"],
|
| 27 |
)
|
| 28 |
|
| 29 |
-
#
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
raise HTTPException(
|
| 37 |
status_code=401,
|
| 38 |
-
detail="
|
| 39 |
-
headers={"WWW-Authenticate": "Bearer"},
|
| 40 |
)
|
| 41 |
-
return
|
| 42 |
|
| 43 |
@app.post("/api/v1/process-call")
|
| 44 |
async def process_call(
|
| 45 |
file: UploadFile = File(...),
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
):
|
| 48 |
"""
|
| 49 |
Process a voice call recording file.
|
|
@@ -52,23 +66,61 @@ async def process_call(
|
|
| 52 |
# Generate unique ID for this processing request
|
| 53 |
process_id = str(uuid.uuid4())
|
| 54 |
|
| 55 |
-
#
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
# Mock response for now
|
| 61 |
return {
|
| 62 |
"id": process_id,
|
| 63 |
-
"
|
| 64 |
-
"
|
| 65 |
-
"
|
| 66 |
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 67 |
}
|
| 68 |
|
| 69 |
except Exception as e:
|
| 70 |
raise HTTPException(status_code=500, detail=str(e))
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
if __name__ == "__main__":
|
| 73 |
import uvicorn
|
| 74 |
host = os.getenv("HOST", "0.0.0.0")
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException, Depends, Header
|
|
|
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from fastapi.responses import JSONResponse
|
| 4 |
from datetime import datetime
|
| 5 |
import os
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
+
from typing import Optional
|
| 8 |
import uuid
|
| 9 |
+
import tempfile
|
| 10 |
+
from transformers import pipeline
|
| 11 |
+
from sqlalchemy.orm import Session
|
| 12 |
+
import asyncio
|
| 13 |
+
from app import models, auth
|
| 14 |
|
| 15 |
# Load environment variables
|
| 16 |
load_dotenv()
|
|
|
|
| 31 |
allow_headers=["*"],
|
| 32 |
)
|
| 33 |
|
| 34 |
+
# Initialize Hugging Face models
|
| 35 |
+
transcriber = pipeline("automatic-speech-recognition", model="openai/whisper-base")
|
| 36 |
+
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
|
| 37 |
+
sentiment_analyzer = pipeline("sentiment-analysis", model="nlptown/bert-base-multilingual-uncased-sentiment")
|
| 38 |
|
| 39 |
+
# Include auth router
|
| 40 |
+
app.include_router(auth.router, prefix="/api/v1", tags=["auth"])
|
| 41 |
+
|
| 42 |
+
async def get_customer_by_api_key(api_key: str = Header(...), db: Session = Depends(models.get_db)):
|
| 43 |
+
customer = db.query(models.Customer).filter(
|
| 44 |
+
models.Customer.api_key == api_key,
|
| 45 |
+
models.Customer.is_active == True
|
| 46 |
+
).first()
|
| 47 |
+
if not customer:
|
| 48 |
raise HTTPException(
|
| 49 |
status_code=401,
|
| 50 |
+
detail="Invalid or inactive API key"
|
|
|
|
| 51 |
)
|
| 52 |
+
return customer
|
| 53 |
|
| 54 |
@app.post("/api/v1/process-call")
|
| 55 |
async def process_call(
|
| 56 |
file: UploadFile = File(...),
|
| 57 |
+
caller_number: str = Header(...),
|
| 58 |
+
called_number: str = Header(...),
|
| 59 |
+
customer: models.Customer = Depends(get_customer_by_api_key),
|
| 60 |
+
db: Session = Depends(models.get_db)
|
| 61 |
):
|
| 62 |
"""
|
| 63 |
Process a voice call recording file.
|
|
|
|
| 66 |
# Generate unique ID for this processing request
|
| 67 |
process_id = str(uuid.uuid4())
|
| 68 |
|
| 69 |
+
# Save the uploaded file temporarily
|
| 70 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_file:
|
| 71 |
+
content = await file.read()
|
| 72 |
+
temp_file.write(content)
|
| 73 |
+
temp_file_path = temp_file.name
|
| 74 |
+
|
| 75 |
+
# Process audio using Hugging Face models
|
| 76 |
+
# Note: In production, these should be processed asynchronously
|
| 77 |
+
transcription = transcriber(temp_file_path)["text"]
|
| 78 |
+
summary = summarizer(transcription, max_length=130, min_length=30, do_sample=False)[0]["summary_text"]
|
| 79 |
+
sentiment = sentiment_analyzer(transcription)[0]["label"]
|
| 80 |
+
|
| 81 |
+
# Create call record
|
| 82 |
+
call_record = models.CallRecord(
|
| 83 |
+
id=process_id,
|
| 84 |
+
customer_id=customer.id,
|
| 85 |
+
caller_number=caller_number,
|
| 86 |
+
called_number=called_number,
|
| 87 |
+
file_path=temp_file_path,
|
| 88 |
+
transcription=transcription,
|
| 89 |
+
summary=summary,
|
| 90 |
+
sentiment=sentiment
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
db.add(call_record)
|
| 94 |
+
db.commit()
|
| 95 |
+
|
| 96 |
+
# Clean up temporary file
|
| 97 |
+
os.unlink(temp_file_path)
|
| 98 |
|
|
|
|
| 99 |
return {
|
| 100 |
"id": process_id,
|
| 101 |
+
"transcription": transcription,
|
| 102 |
+
"summary": summary,
|
| 103 |
+
"sentiment": sentiment,
|
| 104 |
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 105 |
}
|
| 106 |
|
| 107 |
except Exception as e:
|
| 108 |
raise HTTPException(status_code=500, detail=str(e))
|
| 109 |
|
| 110 |
+
@app.get("/api/v1/calls/{customer_id}")
|
| 111 |
+
async def list_calls(
|
| 112 |
+
customer_id: int,
|
| 113 |
+
db: Session = Depends(models.get_db),
|
| 114 |
+
current_admin: str = Depends(auth.get_current_admin)
|
| 115 |
+
):
|
| 116 |
+
"""
|
| 117 |
+
List all calls for a specific customer.
|
| 118 |
+
"""
|
| 119 |
+
calls = db.query(models.CallRecord).filter(
|
| 120 |
+
models.CallRecord.customer_id == customer_id
|
| 121 |
+
).all()
|
| 122 |
+
return calls
|
| 123 |
+
|
| 124 |
if __name__ == "__main__":
|
| 125 |
import uvicorn
|
| 126 |
host = os.getenv("HOST", "0.0.0.0")
|
requirements.txt
CHANGED
|
@@ -7,4 +7,9 @@ pymysql==1.1.0
|
|
| 7 |
cryptography==42.0.2
|
| 8 |
python-dotenv==1.0.0
|
| 9 |
passlib[bcrypt]==1.7.4
|
| 10 |
-
PyJWT==2.8.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
cryptography==42.0.2
|
| 8 |
python-dotenv==1.0.0
|
| 9 |
passlib[bcrypt]==1.7.4
|
| 10 |
+
PyJWT==2.8.0
|
| 11 |
+
transformers==4.37.2
|
| 12 |
+
torch==2.2.0
|
| 13 |
+
soundfile==0.12.1
|
| 14 |
+
librosa==0.10.1
|
| 15 |
+
numpy==1.26.3
|