Spaces:
Running
Running
Upload 23 files
Browse files- Dockerfile +23 -0
- Readme.md +7 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-39.pyc +0 -0
- app/__pycache__/auth.cpython-39.pyc +0 -0
- app/__pycache__/dashboard.cpython-39.pyc +0 -0
- app/__pycache__/database.cpython-39.pyc +0 -0
- app/__pycache__/main.cpython-39.pyc +0 -0
- app/__pycache__/models.cpython-39.pyc +0 -0
- app/__pycache__/upload.cpython-39.pyc +0 -0
- app/auth.py +73 -0
- app/dashboard.py +41 -0
- app/database.py +32 -0
- app/main.py +43 -0
- app/models.py +21 -0
- app/run_once.py +13 -0
- app/testdb.py +13 -0
- app/upload.py +53 -0
- app/utils/__pycache__/s3.cpython-39.pyc +0 -0
- app/utils/__pycache__/whisper_llm.cpython-39.pyc +0 -0
- app/utils/pdf.py +15 -0
- app/utils/s3.py +20 -0
- app/utils/whisper_llm.py +22 -0
Dockerfile
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use a lightweight Python image
|
2 |
+
FROM python:3.10-slim
|
3 |
+
|
4 |
+
# Create app directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Install system dependencies
|
8 |
+
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
|
9 |
+
|
10 |
+
# Copy dependency files
|
11 |
+
COPY requirements.txt .
|
12 |
+
|
13 |
+
# Install Python deps
|
14 |
+
RUN pip install --upgrade pip && pip install -r requirements.txt
|
15 |
+
|
16 |
+
# Copy the source code
|
17 |
+
COPY . .
|
18 |
+
|
19 |
+
# Expose port (default for Hugging Face Spaces)
|
20 |
+
EXPOSE 7860
|
21 |
+
|
22 |
+
# Run app using Uvicorn
|
23 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
Readme.md
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Dubsway Video AI
|
2 |
+
|
3 |
+
This FastAPI app handles authentication, video uploads, and PDF analysis using Whisper and Transformers.
|
4 |
+
|
5 |
+
- 🔐 Auth with email
|
6 |
+
- 📤 Upload any video file
|
7 |
+
- 📄 Generates summary PDFs
|
app/__init__.py
ADDED
File without changes
|
app/__pycache__/__init__.cpython-39.pyc
ADDED
Binary file (185 Bytes). View file
|
|
app/__pycache__/auth.cpython-39.pyc
ADDED
Binary file (2.46 kB). View file
|
|
app/__pycache__/dashboard.cpython-39.pyc
ADDED
Binary file (1.43 kB). View file
|
|
app/__pycache__/database.cpython-39.pyc
ADDED
Binary file (834 Bytes). View file
|
|
app/__pycache__/main.cpython-39.pyc
ADDED
Binary file (1.31 kB). View file
|
|
app/__pycache__/models.cpython-39.pyc
ADDED
Binary file (1.16 kB). View file
|
|
app/__pycache__/upload.cpython-39.pyc
ADDED
Binary file (1.47 kB). View file
|
|
app/auth.py
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException, Depends
|
2 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
3 |
+
from sqlalchemy.future import select
|
4 |
+
from passlib.context import CryptContext
|
5 |
+
from jose import jwt
|
6 |
+
from pydantic import BaseModel, EmailStr
|
7 |
+
from app.database import get_db # Updated: use the correct async session dependency
|
8 |
+
from app.models import User
|
9 |
+
import os
|
10 |
+
import logging
|
11 |
+
from dotenv import load_dotenv
|
12 |
+
|
13 |
+
router = APIRouter()
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
+
|
16 |
+
load_dotenv()
|
17 |
+
|
18 |
+
# Load secret key and JWT algorithm
|
19 |
+
SECRET_KEY = os.getenv("SECRET_KEY", "secret")
|
20 |
+
ALGORITHM = "HS256"
|
21 |
+
|
22 |
+
# Password hashing config
|
23 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
24 |
+
|
25 |
+
|
26 |
+
# Request Schemas
|
27 |
+
class SignUp(BaseModel):
|
28 |
+
email: EmailStr
|
29 |
+
password: str
|
30 |
+
|
31 |
+
|
32 |
+
class Login(BaseModel):
|
33 |
+
email: EmailStr
|
34 |
+
password: str
|
35 |
+
|
36 |
+
|
37 |
+
@router.post("/auth/signup")
|
38 |
+
async def signup(data: SignUp, db: AsyncSession = Depends(get_db)):
|
39 |
+
# Check if user already exists
|
40 |
+
result = await db.execute(select(User).where(User.email == data.email))
|
41 |
+
existing_user = result.scalar_one_or_none()
|
42 |
+
|
43 |
+
if existing_user:
|
44 |
+
raise HTTPException(status_code=400, detail="Email already exists")
|
45 |
+
|
46 |
+
hashed_password = pwd_context.hash(data.password)
|
47 |
+
new_user = User(email=data.email, hashed_password=hashed_password)
|
48 |
+
|
49 |
+
try:
|
50 |
+
db.add(new_user)
|
51 |
+
await db.commit()
|
52 |
+
await db.refresh(new_user)
|
53 |
+
return {"message": "User created", "user_id": new_user.id}
|
54 |
+
except Exception as e:
|
55 |
+
await db.rollback()
|
56 |
+
logger.error(f"Signup error: {e}")
|
57 |
+
raise HTTPException(status_code=500, detail="Internal Server Error")
|
58 |
+
|
59 |
+
|
60 |
+
@router.post("/auth/login")
|
61 |
+
async def login(data: Login, db: AsyncSession = Depends(get_db)):
|
62 |
+
result = await db.execute(select(User).where(User.email == data.email))
|
63 |
+
user = result.scalar_one_or_none()
|
64 |
+
|
65 |
+
if not user or not pwd_context.verify(data.password, user.hashed_password):
|
66 |
+
raise HTTPException(status_code=401, detail="Invalid credentials")
|
67 |
+
|
68 |
+
token = jwt.encode({"user_id": user.id}, SECRET_KEY, algorithm=ALGORITHM)
|
69 |
+
return {
|
70 |
+
"access_token": token,
|
71 |
+
"token_type": "bearer",
|
72 |
+
"user": {"id": user.id, "email": user.email},
|
73 |
+
}
|
app/dashboard.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, HTTPException
|
2 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
3 |
+
from sqlalchemy.future import select
|
4 |
+
from sqlalchemy.orm import sessionmaker
|
5 |
+
from sqlalchemy import desc
|
6 |
+
from app.database import engine
|
7 |
+
from app.models import VideoUpload
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
|
11 |
+
# Create async DB session
|
12 |
+
async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
|
13 |
+
|
14 |
+
|
15 |
+
@router.get("/dashboard/{user_id}")
|
16 |
+
async def get_user_dashboard(user_id: int):
|
17 |
+
try:
|
18 |
+
async with async_session() as session:
|
19 |
+
query = (
|
20 |
+
select(VideoUpload)
|
21 |
+
.where(VideoUpload.user_id == user_id)
|
22 |
+
.order_by(desc(VideoUpload.created_at))
|
23 |
+
)
|
24 |
+
result = await session.execute(query)
|
25 |
+
uploads = result.scalars().all()
|
26 |
+
|
27 |
+
# Convert SQLAlchemy objects to dicts for response
|
28 |
+
return [
|
29 |
+
{
|
30 |
+
"id": upload.id,
|
31 |
+
"video_url": upload.video_url,
|
32 |
+
"pdf_url": upload.pdf_url,
|
33 |
+
"status": upload.status,
|
34 |
+
"created_at": upload.created_at,
|
35 |
+
}
|
36 |
+
for upload in uploads
|
37 |
+
]
|
38 |
+
except Exception as e:
|
39 |
+
raise HTTPException(
|
40 |
+
status_code=500, detail=f"Error fetching dashboard data: {str(e)}"
|
41 |
+
)
|
app/database.py
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
3 |
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
4 |
+
from dotenv import load_dotenv
|
5 |
+
|
6 |
+
# Load .env variables
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
# Ensure your DATABASE_URL is in correct asyncpg format
|
10 |
+
DATABASE_URL = os.getenv("DATABASE_URL")
|
11 |
+
|
12 |
+
if not DATABASE_URL:
|
13 |
+
raise RuntimeError("DATABASE_URL is not set in environment.")
|
14 |
+
|
15 |
+
# Create the async engine
|
16 |
+
engine = create_async_engine(
|
17 |
+
DATABASE_URL, echo=True, future=True # Set echo=False in production
|
18 |
+
)
|
19 |
+
|
20 |
+
# Session factory
|
21 |
+
AsyncSessionLocal = sessionmaker(
|
22 |
+
bind=engine, class_=AsyncSession, expire_on_commit=False
|
23 |
+
)
|
24 |
+
|
25 |
+
# Base class for models
|
26 |
+
Base = declarative_base()
|
27 |
+
|
28 |
+
|
29 |
+
# Dependency for routes to get the async session
|
30 |
+
async def get_db():
|
31 |
+
async with AsyncSessionLocal() as session:
|
32 |
+
yield session
|
app/main.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
import logging
|
4 |
+
|
5 |
+
from app.auth import router as auth_router
|
6 |
+
from app.upload import router as upload_router
|
7 |
+
from app.dashboard import router as dashboard_router
|
8 |
+
|
9 |
+
# Initialize logger
|
10 |
+
logging.basicConfig(level=logging.INFO)
|
11 |
+
logger = logging.getLogger(__name__)
|
12 |
+
|
13 |
+
app = FastAPI(
|
14 |
+
title="Dubsway Video AI",
|
15 |
+
description="Production-ready API for auth, video upload, and analysis",
|
16 |
+
version="1.0.0",
|
17 |
+
docs_url="/docs", # Optional: secure this in prod
|
18 |
+
redoc_url=None,
|
19 |
+
)
|
20 |
+
|
21 |
+
# Allow frontend (adjust in prod!)
|
22 |
+
app.add_middleware(
|
23 |
+
CORSMiddleware,
|
24 |
+
allow_origins=["*"], # REPLACE with frontend URL in production!
|
25 |
+
allow_credentials=True,
|
26 |
+
allow_methods=["*"],
|
27 |
+
allow_headers=["*"],
|
28 |
+
)
|
29 |
+
|
30 |
+
# Include API routes
|
31 |
+
app.include_router(auth_router, prefix="/api", tags=["Auth"])
|
32 |
+
app.include_router(upload_router, prefix="/api", tags=["Upload"])
|
33 |
+
app.include_router(dashboard_router, prefix="/api", tags=["Dashboard"])
|
34 |
+
|
35 |
+
|
36 |
+
@app.on_event("startup")
|
37 |
+
async def startup_event():
|
38 |
+
logger.info("✅ FastAPI app started")
|
39 |
+
|
40 |
+
|
41 |
+
@app.on_event("shutdown")
|
42 |
+
async def shutdown_event():
|
43 |
+
logger.info("🛑 FastAPI app shutdown")
|
app/models.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime
|
2 |
+
from sqlalchemy.sql import func
|
3 |
+
from .database import Base
|
4 |
+
|
5 |
+
|
6 |
+
class User(Base):
|
7 |
+
__tablename__ = "users"
|
8 |
+
id = Column(Integer, primary_key=True, index=True)
|
9 |
+
email = Column(String, unique=True, index=True)
|
10 |
+
hashed_password = Column(String)
|
11 |
+
|
12 |
+
|
13 |
+
class VideoUpload(Base):
|
14 |
+
__tablename__ = "video_uploads"
|
15 |
+
id = Column(Integer, primary_key=True, index=True)
|
16 |
+
user_id = Column(Integer, ForeignKey("users.id"))
|
17 |
+
video_url = Column(Text)
|
18 |
+
pdf_url = Column(Text)
|
19 |
+
status = Column(String, default="pending") # pending, processing, completed
|
20 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
21 |
+
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
app/run_once.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# run_once.py
|
2 |
+
from app.database import Base, engine
|
3 |
+
from app import models
|
4 |
+
|
5 |
+
|
6 |
+
async def init():
|
7 |
+
async with engine.begin() as conn:
|
8 |
+
await conn.run_sync(Base.metadata.create_all)
|
9 |
+
|
10 |
+
|
11 |
+
import asyncio
|
12 |
+
|
13 |
+
asyncio.run(init())
|
app/testdb.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# test_db.py
|
2 |
+
from app.database import engine
|
3 |
+
from sqlalchemy import text
|
4 |
+
import asyncio
|
5 |
+
|
6 |
+
|
7 |
+
async def test_connection():
|
8 |
+
async with engine.connect() as conn:
|
9 |
+
result = await conn.execute(text("SELECT 1"))
|
10 |
+
print("DB Connection OK:", result.scalar())
|
11 |
+
|
12 |
+
|
13 |
+
asyncio.run(test_connection())
|
app/upload.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, UploadFile, Form, HTTPException, Depends
|
2 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
3 |
+
from sqlalchemy.future import select
|
4 |
+
from app.database import get_db
|
5 |
+
from app.models import VideoUpload
|
6 |
+
from .utils.s3 import upload_to_s3
|
7 |
+
import uuid
|
8 |
+
import os
|
9 |
+
|
10 |
+
router = APIRouter()
|
11 |
+
|
12 |
+
# Accept any video file
|
13 |
+
ALLOWED_VIDEO_MIME_TYPES = {
|
14 |
+
"video/mp4",
|
15 |
+
"video/x-matroska", # mkv
|
16 |
+
"video/quicktime", # mov
|
17 |
+
"video/x-msvideo", # avi
|
18 |
+
"video/webm",
|
19 |
+
"video/mpeg",
|
20 |
+
}
|
21 |
+
|
22 |
+
|
23 |
+
@router.post("/upload")
|
24 |
+
async def upload_video(
|
25 |
+
user_id: int = Form(...),
|
26 |
+
file: UploadFile = Form(...),
|
27 |
+
db: AsyncSession = Depends(get_db),
|
28 |
+
):
|
29 |
+
if file.content_type not in ALLOWED_VIDEO_MIME_TYPES:
|
30 |
+
raise HTTPException(
|
31 |
+
status_code=400, detail=f"Unsupported file type: {file.content_type}"
|
32 |
+
)
|
33 |
+
|
34 |
+
try:
|
35 |
+
uid = str(uuid.uuid4())
|
36 |
+
s3_key = f"videos/{uid}/{file.filename}"
|
37 |
+
video_url = upload_to_s3(file, s3_key)
|
38 |
+
|
39 |
+
new_video = VideoUpload(
|
40 |
+
user_id=user_id,
|
41 |
+
video_url=video_url,
|
42 |
+
pdf_url="", # will be set after analysis
|
43 |
+
status="pending",
|
44 |
+
)
|
45 |
+
db.add(new_video)
|
46 |
+
await db.commit()
|
47 |
+
await db.refresh(new_video)
|
48 |
+
|
49 |
+
return {"status": "uploaded", "video_url": video_url, "video_id": new_video.id}
|
50 |
+
|
51 |
+
except Exception as e:
|
52 |
+
await db.rollback()
|
53 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/utils/__pycache__/s3.cpython-39.pyc
ADDED
Binary file (930 Bytes). View file
|
|
app/utils/__pycache__/whisper_llm.cpython-39.pyc
ADDED
Binary file (936 Bytes). View file
|
|
app/utils/pdf.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from reportlab.pdfgen import canvas
|
2 |
+
from io import BytesIO
|
3 |
+
|
4 |
+
|
5 |
+
def generate(transcription: str, summary: str):
|
6 |
+
buffer = BytesIO()
|
7 |
+
c = canvas.Canvas(buffer)
|
8 |
+
c.drawString(100, 800, "📄 Video Summary Report")
|
9 |
+
c.drawString(100, 770, "Transcription:")
|
10 |
+
c.drawString(100, 750, transcription[:1000])
|
11 |
+
c.drawString(100, 700, "Summary:")
|
12 |
+
c.drawString(100, 680, summary[:1000])
|
13 |
+
c.save()
|
14 |
+
buffer.seek(0)
|
15 |
+
return buffer.read()
|
app/utils/s3.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import boto3, os
|
2 |
+
from dotenv import load_dotenv
|
3 |
+
|
4 |
+
s3 = boto3.client(
|
5 |
+
"s3",
|
6 |
+
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
|
7 |
+
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
|
8 |
+
region_name=os.getenv("AWS_REGION"),
|
9 |
+
)
|
10 |
+
bucket = os.getenv("S3_BUCKET")
|
11 |
+
|
12 |
+
|
13 |
+
def upload_to_s3(file, key):
|
14 |
+
s3.upload_fileobj(file.file, bucket, key)
|
15 |
+
return f"https://{bucket}.s3.amazonaws.com/{key}"
|
16 |
+
|
17 |
+
|
18 |
+
def upload_pdf_bytes(data: bytes, key: str):
|
19 |
+
s3.put_object(Bucket=bucket, Key=key, Body=data, ContentType="application/pdf")
|
20 |
+
return f"https://{bucket}.s3.amazonaws.com/{key}"
|
app/utils/whisper_llm.py
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import whisper
|
2 |
+
from transformers import pipeline
|
3 |
+
import requests
|
4 |
+
import tempfile
|
5 |
+
|
6 |
+
|
7 |
+
def analyze(video_url: str):
|
8 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
|
9 |
+
with requests.get(video_url, stream=True) as r:
|
10 |
+
for chunk in r.iter_content(8192):
|
11 |
+
tmp.write(chunk)
|
12 |
+
tmp.close()
|
13 |
+
|
14 |
+
model = whisper.load_model("base")
|
15 |
+
result = model.transcribe(tmp.name)
|
16 |
+
text = result["text"]
|
17 |
+
|
18 |
+
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
|
19 |
+
summary = summarizer(text, max_length=512, min_length=128, do_sample=False)[0][
|
20 |
+
"summary_text"
|
21 |
+
]
|
22 |
+
return text, summary
|