Spaces:
Paused
Paused
Update main.py
Browse files
main.py
CHANGED
|
@@ -5,8 +5,10 @@ import asyncio
|
|
| 5 |
import sqlite3
|
| 6 |
import bcrypt
|
| 7 |
import tempfile
|
|
|
|
|
|
|
| 8 |
from fastapi import FastAPI, Request, Query, Form, UploadFile, File, HTTPException, Depends
|
| 9 |
-
from fastapi.responses import HTMLResponse, JSONResponse
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
@@ -47,7 +49,11 @@ the following products are spelled correctly
|
|
| 47 |
# Authentication & Security
|
| 48 |
SECRET_KEY = os.getenv("SECRET_KEY", None) # Change in production
|
| 49 |
ALGORITHM = "HS256"
|
| 50 |
-
ACCESS_TOKEN_EXPIRE_MINUTES =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
# Database setup
|
| 53 |
DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower() # Default to SQLite if not specified
|
|
@@ -90,6 +96,7 @@ class UserModel(Base):
|
|
| 90 |
disabled = Column(Boolean, default=True) # Default to disabled
|
| 91 |
is_admin = Column(Boolean, default=False) # Add admin field
|
| 92 |
|
|
|
|
| 93 |
# Create tables
|
| 94 |
Base.metadata.create_all(bind=engine)
|
| 95 |
|
|
@@ -115,7 +122,7 @@ class User(UserBase):
|
|
| 115 |
is_admin: bool = False # Add admin field
|
| 116 |
|
| 117 |
class Config:
|
| 118 |
-
|
| 119 |
|
| 120 |
class Token(BaseModel):
|
| 121 |
access_token: str
|
|
@@ -152,7 +159,7 @@ class Transcription(TranscriptionBase):
|
|
| 152 |
created_at: str
|
| 153 |
|
| 154 |
class Config:
|
| 155 |
-
|
| 156 |
|
| 157 |
# Add these utilities
|
| 158 |
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
@@ -346,7 +353,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
|
| 346 |
if expires_delta:
|
| 347 |
expire = datetime.utcnow() + expires_delta
|
| 348 |
else:
|
| 349 |
-
expire = datetime.utcnow() + timedelta(minutes=
|
| 350 |
to_encode.update({"exp": expire})
|
| 351 |
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 352 |
return encoded_jwt
|
|
@@ -391,7 +398,7 @@ app.add_middleware(
|
|
| 391 |
)
|
| 392 |
|
| 393 |
# Also ensure your login form data is properly sent
|
| 394 |
-
@app.post("/token")
|
| 395 |
async def login_for_access_token(
|
| 396 |
form_data: OAuth2PasswordRequestForm = Depends(),
|
| 397 |
db: Session = Depends(get_db)
|
|
@@ -409,7 +416,8 @@ async def login_for_access_token(
|
|
| 409 |
status_code=400,
|
| 410 |
detail="User is disabled"
|
| 411 |
)
|
| 412 |
-
|
|
|
|
| 413 |
return {"access_token": access_token, "token_type": "bearer"}
|
| 414 |
except Exception as e:
|
| 415 |
print(f"Login error: {str(e)}") # Add logging for debugging
|
|
@@ -759,6 +767,159 @@ async def upload_audio(
|
|
| 759 |
except Exception as e:
|
| 760 |
raise HTTPException(status_code=500, detail=str(e))
|
| 761 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
@app.get("/api/transcriptions")
|
| 763 |
async def get_transcriptions(
|
| 764 |
current_user: User = Depends(get_current_user),
|
|
|
|
| 5 |
import sqlite3
|
| 6 |
import bcrypt
|
| 7 |
import tempfile
|
| 8 |
+
import shutil
|
| 9 |
+
import subprocess # to use "ffmpeg" in order to use "ffmpeg-python" library for video to audio conversion
|
| 10 |
from fastapi import FastAPI, Request, Query, Form, UploadFile, File, HTTPException, Depends
|
| 11 |
+
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
|
|
| 49 |
# Authentication & Security
|
| 50 |
SECRET_KEY = os.getenv("SECRET_KEY", None) # Change in production
|
| 51 |
ALGORITHM = "HS256"
|
| 52 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
| 53 |
+
|
| 54 |
+
# Directory definitions
|
| 55 |
+
UPLOAD_DIR = "uploads"
|
| 56 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 57 |
|
| 58 |
# Database setup
|
| 59 |
DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower() # Default to SQLite if not specified
|
|
|
|
| 96 |
disabled = Column(Boolean, default=True) # Default to disabled
|
| 97 |
is_admin = Column(Boolean, default=False) # Add admin field
|
| 98 |
|
| 99 |
+
|
| 100 |
# Create tables
|
| 101 |
Base.metadata.create_all(bind=engine)
|
| 102 |
|
|
|
|
| 122 |
is_admin: bool = False # Add admin field
|
| 123 |
|
| 124 |
class Config:
|
| 125 |
+
from_attributes = True
|
| 126 |
|
| 127 |
class Token(BaseModel):
|
| 128 |
access_token: str
|
|
|
|
| 159 |
created_at: str
|
| 160 |
|
| 161 |
class Config:
|
| 162 |
+
from_attributes = True
|
| 163 |
|
| 164 |
# Add these utilities
|
| 165 |
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
|
|
|
| 353 |
if expires_delta:
|
| 354 |
expire = datetime.utcnow() + expires_delta
|
| 355 |
else:
|
| 356 |
+
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 357 |
to_encode.update({"exp": expire})
|
| 358 |
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 359 |
return encoded_jwt
|
|
|
|
| 398 |
)
|
| 399 |
|
| 400 |
# Also ensure your login form data is properly sent
|
| 401 |
+
@app.post("/token") #, response_model=Token)
|
| 402 |
async def login_for_access_token(
|
| 403 |
form_data: OAuth2PasswordRequestForm = Depends(),
|
| 404 |
db: Session = Depends(get_db)
|
|
|
|
| 416 |
status_code=400,
|
| 417 |
detail="User is disabled"
|
| 418 |
)
|
| 419 |
+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 420 |
+
access_token = create_access_token(data={"sub": user.username}, expires_delta=access_token_expires)
|
| 421 |
return {"access_token": access_token, "token_type": "bearer"}
|
| 422 |
except Exception as e:
|
| 423 |
print(f"Login error: {str(e)}") # Add logging for debugging
|
|
|
|
| 767 |
except Exception as e:
|
| 768 |
raise HTTPException(status_code=500, detail=str(e))
|
| 769 |
|
| 770 |
+
@app.post("/api/upload-audio-chunk")
|
| 771 |
+
async def upload_audio_chunk(
|
| 772 |
+
file: UploadFile = File(...),
|
| 773 |
+
upload_id: str = Form(...),
|
| 774 |
+
offset: int = Form(...),
|
| 775 |
+
total_size: int = Form(...),
|
| 776 |
+
current_user: User = Depends(get_current_user)
|
| 777 |
+
):
|
| 778 |
+
# Create upload directory for this file in system temp directory
|
| 779 |
+
temp_dir = tempfile.gettempdir()
|
| 780 |
+
upload_path = os.path.join(temp_dir, f"transtudio_upload_{upload_id}")
|
| 781 |
+
os.makedirs(upload_path, exist_ok=True)
|
| 782 |
+
|
| 783 |
+
# Path for this specific chunk
|
| 784 |
+
chunk_path = os.path.join(upload_path, f"chunk_{offset}")
|
| 785 |
+
|
| 786 |
+
# Save the chunk
|
| 787 |
+
with open(chunk_path, "wb") as buffer:
|
| 788 |
+
shutil.copyfileobj(file.file, buffer)
|
| 789 |
+
|
| 790 |
+
# Update metadata file with progress info
|
| 791 |
+
metadata_path = os.path.join(upload_path, "metadata.txt")
|
| 792 |
+
with open(metadata_path, "w") as f:
|
| 793 |
+
f.write(f"{offset + file.size}/{total_size}")
|
| 794 |
+
|
| 795 |
+
return {"success": True, "received_bytes": offset + file.size}
|
| 796 |
+
|
| 797 |
+
@app.post("/api/finalize-audio-upload")
|
| 798 |
+
async def finalize_audio_upload(
|
| 799 |
+
upload_info: dict,
|
| 800 |
+
current_user: User = Depends(get_current_user)
|
| 801 |
+
):
|
| 802 |
+
upload_id = upload_info.get("upload_id")
|
| 803 |
+
if not upload_id:
|
| 804 |
+
raise HTTPException(status_code=400, detail="Upload ID is required")
|
| 805 |
+
|
| 806 |
+
temp_dir = tempfile.gettempdir()
|
| 807 |
+
upload_path = os.path.join(temp_dir, f"transtudio_upload_{upload_id}")
|
| 808 |
+
if not os.path.exists(upload_path):
|
| 809 |
+
raise HTTPException(status_code=404, detail="Upload not found")
|
| 810 |
+
|
| 811 |
+
# Read metadata to get original filename if available
|
| 812 |
+
metadata_path = os.path.join(upload_path, "metadata.txt")
|
| 813 |
+
filename = f"{uuid.uuid4()}.mp3" # Default filename
|
| 814 |
+
|
| 815 |
+
# Combine all chunks into final file
|
| 816 |
+
output_path = os.path.join(UPLOAD_DIR, filename)
|
| 817 |
+
|
| 818 |
+
with open(output_path, "wb") as outfile:
|
| 819 |
+
# Get all chunks sorted by offset
|
| 820 |
+
chunks = [f for f in os.listdir(upload_path) if f.startswith("chunk_")]
|
| 821 |
+
chunks.sort(key=lambda x: int(x.split("_")[1]))
|
| 822 |
+
|
| 823 |
+
for chunk_name in chunks:
|
| 824 |
+
chunk_path = os.path.join(upload_path, chunk_name)
|
| 825 |
+
with open(chunk_path, "rb") as infile:
|
| 826 |
+
shutil.copyfileobj(infile, outfile)
|
| 827 |
+
|
| 828 |
+
# Clean up temporary files
|
| 829 |
+
shutil.rmtree(upload_path)
|
| 830 |
+
|
| 831 |
+
return {"success": True, "filename": filename}
|
| 832 |
+
|
| 833 |
+
@app.post("/api/cancel-audio-upload")
|
| 834 |
+
async def cancel_audio_upload(
|
| 835 |
+
upload_info: dict,
|
| 836 |
+
current_user: User = Depends(get_current_user)
|
| 837 |
+
):
|
| 838 |
+
upload_id = upload_info.get("upload_id")
|
| 839 |
+
if not upload_id:
|
| 840 |
+
raise HTTPException(status_code=400, detail="Upload ID is required")
|
| 841 |
+
|
| 842 |
+
temp_dir = tempfile.gettempdir()
|
| 843 |
+
upload_path = os.path.join(temp_dir, f"transtudio_upload_{upload_id}")
|
| 844 |
+
if os.path.exists(upload_path):
|
| 845 |
+
shutil.rmtree(upload_path)
|
| 846 |
+
|
| 847 |
+
return {"success": True}
|
| 848 |
+
|
| 849 |
+
@app.get("/api/audio-files")
|
| 850 |
+
async def get_audio_files(
|
| 851 |
+
current_user: User = Depends(get_current_user)
|
| 852 |
+
):
|
| 853 |
+
try:
|
| 854 |
+
# Scan uploads directory for audio files
|
| 855 |
+
upload_dir = os.path.join(os.getcwd(), UPLOAD_DIR)
|
| 856 |
+
file_list = []
|
| 857 |
+
|
| 858 |
+
for filename in os.listdir(upload_dir):
|
| 859 |
+
file_path = os.path.join(upload_dir, filename)
|
| 860 |
+
if os.path.isfile(file_path) and not filename.startswith('.'):
|
| 861 |
+
# Get file stats
|
| 862 |
+
file_stats = os.stat(file_path)
|
| 863 |
+
file_size = os.path.getsize(file_path)
|
| 864 |
+
|
| 865 |
+
file_list.append({
|
| 866 |
+
"id": filename, # Use filename as ID
|
| 867 |
+
"filename": filename,
|
| 868 |
+
"original_filename": filename,
|
| 869 |
+
"size": file_size,
|
| 870 |
+
"uploaded_at": datetime.fromtimestamp(file_stats.st_mtime).isoformat()
|
| 871 |
+
})
|
| 872 |
+
|
| 873 |
+
# Sort by upload date (newest first)
|
| 874 |
+
file_list.sort(key=lambda x: x["uploaded_at"], reverse=True)
|
| 875 |
+
|
| 876 |
+
return file_list
|
| 877 |
+
except Exception as e:
|
| 878 |
+
print(f"Error getting audio files: {str(e)}")
|
| 879 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 880 |
+
|
| 881 |
+
@app.delete("/api/audio-files/{filename}")
|
| 882 |
+
async def delete_audio_file(
|
| 883 |
+
filename: str,
|
| 884 |
+
current_user: User = Depends(get_current_user)
|
| 885 |
+
):
|
| 886 |
+
try:
|
| 887 |
+
# Check if file exists in uploads directory
|
| 888 |
+
file_path = os.path.join(os.getcwd(), UPLOAD_DIR, filename)
|
| 889 |
+
if not os.path.exists(file_path):
|
| 890 |
+
raise HTTPException(status_code=404, detail="Audio file not found")
|
| 891 |
+
|
| 892 |
+
# Delete the physical file
|
| 893 |
+
os.remove(file_path)
|
| 894 |
+
|
| 895 |
+
return {"status": "success", "message": "Audio file deleted successfully"}
|
| 896 |
+
except HTTPException:
|
| 897 |
+
raise
|
| 898 |
+
except Exception as e:
|
| 899 |
+
print(f"Error deleting audio file: {str(e)}")
|
| 900 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 901 |
+
|
| 902 |
+
@app.post("/extract-audio/")
|
| 903 |
+
async def extract_audio(file: UploadFile = File(...)):
|
| 904 |
+
# Save uploaded video temporarily
|
| 905 |
+
video_path = f"temp/{file.filename}"
|
| 906 |
+
audio_path = f"temp/{os.path.splitext(file.filename)[0]}.mp3"
|
| 907 |
+
|
| 908 |
+
with open(video_path, "wb") as buffer:
|
| 909 |
+
buffer.write(await file.read())
|
| 910 |
+
|
| 911 |
+
# Extract audio using FFmpeg
|
| 912 |
+
subprocess.run([
|
| 913 |
+
"ffmpeg", "-i", video_path,
|
| 914 |
+
"-q:a", "0", "-map", "a", audio_path
|
| 915 |
+
])
|
| 916 |
+
|
| 917 |
+
# Clean up video file
|
| 918 |
+
os.remove(video_path)
|
| 919 |
+
|
| 920 |
+
# Return audio file
|
| 921 |
+
return FileResponse(audio_path)
|
| 922 |
+
|
| 923 |
@app.get("/api/transcriptions")
|
| 924 |
async def get_transcriptions(
|
| 925 |
current_user: User = Depends(get_current_user),
|