Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- model/.env +11 -0
- model/Dockerfile +24 -0
- model/__init__.py +43 -0
- model/requirements.txt +43 -0
- model/routes.py +216 -0
- model/runtime.txt +2 -0
model/.env
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
VITE_MODEL_API=https://moodify-4-scpi.onrender.com
|
| 2 |
+
VITE_TEXTMODEL_API=https://moodify-haag.onrender.com
|
| 3 |
+
VITE_AUTH_API=https://moodify-1-4ogp.onrender.com
|
| 4 |
+
|
| 5 |
+
VITE_USE_MOCK_AUTH=false
|
| 6 |
+
VITE_GEMINI_API_KEY=AIzaSyCffeP7wTkFqjOZvwaoIWr2jGAibbBj3Ko
|
| 7 |
+
CLOUDINARY_URL=cloudinary://535413842219598:vndwujdGpO19r0XuG69w51ZdoZ4@dkiu8wrxc
|
| 8 |
+
SECRET_KEY=7b063610e8d0781ecba9790967eec378
|
| 9 |
+
MONGO_URI=mongodb+srv://soniyavitkar2712:soniya_27@cluster0.slai2ew.mongodb.net/moodify_db?retryWrites=true&w=majority&appName=Cluster0
|
| 10 |
+
|
| 11 |
+
PORT=5002
|
model/Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a lightweight official Python image as the base
|
| 2 |
+
FROM python:3.10-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory inside the container
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy the requirements file first to leverage Docker's build cache
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
|
| 10 |
+
# Install the Python dependencies
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Copy the rest of the application code into the container
|
| 14 |
+
# This includes app.py, the dnn folder, and other directories
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Expose the port where the Flask API will run
|
| 18 |
+
# This should match the app_port defined in your README.md
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
# Define the command to run the application using Gunicorn
|
| 22 |
+
# 'app:app' means the Flask instance named 'app' is in the 'app.py' file.
|
| 23 |
+
# The --bind 0.0.0.0:7860 makes Gunicorn listen on all available network interfaces on port 7860.
|
| 24 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
|
model/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/__init__.py
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
import os
|
| 4 |
+
from flask import Flask
|
| 5 |
+
from flask_pymongo import PyMongo
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
import cloudinary
|
| 8 |
+
import cloudinary.uploader
|
| 9 |
+
|
| 10 |
+
# Load env variables
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
app = Flask(__name__)
|
| 14 |
+
|
| 15 |
+
# Enable CORS
|
| 16 |
+
CORS(
|
| 17 |
+
app,
|
| 18 |
+
origins=[
|
| 19 |
+
"https://moodify-1-4ogp.onrender.com",
|
| 20 |
+
"https://moodify-haag.onrender.com",
|
| 21 |
+
"https://moodify-4-scpi.onrender.com",
|
| 22 |
+
"https://moodify-murex.vercel.app",
|
| 23 |
+
"http://localhost:5173",
|
| 24 |
+
"https://humble-goldfish-4j9wgq46wr6j3j6xj-5173.app.github.dev"
|
| 25 |
+
],
|
| 26 |
+
supports_credentials=True
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Config from env
|
| 30 |
+
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
|
| 31 |
+
app.config["MONGO_URI"] = os.getenv("MONGO_URI")
|
| 32 |
+
|
| 33 |
+
# MongoDB connection
|
| 34 |
+
mongodb_client = PyMongo(app)
|
| 35 |
+
db = mongodb_client.db
|
| 36 |
+
|
| 37 |
+
# Cloudinary config (reads CLOUDINARY_URL directly from .env)
|
| 38 |
+
cloudinary.config(
|
| 39 |
+
secure=True
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Attach routes
|
| 43 |
+
import routes
|
model/requirements.txt
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Flask & Server
|
| 2 |
+
Flask
|
| 3 |
+
flask-cors
|
| 4 |
+
gunicorn
|
| 5 |
+
Werkzeug
|
| 6 |
+
itsdangerous
|
| 7 |
+
Jinja2
|
| 8 |
+
click
|
| 9 |
+
|
| 10 |
+
# MongoDB
|
| 11 |
+
Flask-PyMongo
|
| 12 |
+
pymongo
|
| 13 |
+
dnspython
|
| 14 |
+
|
| 15 |
+
# Deep Learning & Image Processing
|
| 16 |
+
deepface
|
| 17 |
+
retina-face
|
| 18 |
+
tensorflow==2.20.0
|
| 19 |
+
tf_keras==2.20.0
|
| 20 |
+
keras==3.10.0
|
| 21 |
+
opencv-contrib-python
|
| 22 |
+
opencv-python
|
| 23 |
+
mtcnn
|
| 24 |
+
torch
|
| 25 |
+
numpy
|
| 26 |
+
scipy
|
| 27 |
+
scikit-learn
|
| 28 |
+
|
| 29 |
+
# Cloudinary
|
| 30 |
+
cloudinary
|
| 31 |
+
|
| 32 |
+
# Utilities
|
| 33 |
+
requests
|
| 34 |
+
Pillow
|
| 35 |
+
tqdm
|
| 36 |
+
python-dotenv
|
| 37 |
+
protobuf
|
| 38 |
+
h5py
|
| 39 |
+
|
| 40 |
+
# Optional
|
| 41 |
+
ffmpeg
|
| 42 |
+
imageio
|
| 43 |
+
imageio-ffmpeg
|
model/routes.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import request, jsonify
|
| 2 |
+
from deepface import DeepFace
|
| 3 |
+
import tempfile
|
| 4 |
+
import os
|
| 5 |
+
import cv2
|
| 6 |
+
import numpy as np
|
| 7 |
+
from __init__ import app, db
|
| 8 |
+
from flask_cors import CORS
|
| 9 |
+
import cloudinary
|
| 10 |
+
import cloudinary.uploader
|
| 11 |
+
from bson.objectid import ObjectId
|
| 12 |
+
|
| 13 |
+
CORS(app)
|
| 14 |
+
# DNN FACE DETECTOR SETUP
|
| 15 |
+
prototxt_path = os.path.join("dnn", "deploy.prototxt.txt")
|
| 16 |
+
caffemodel_path = os.path.join("dnn", "res10_300x300_ssd_iter_140000.caffemodel")
|
| 17 |
+
net = cv2.dnn.readNetFromCaffe(prototxt_path, caffemodel_path)
|
| 18 |
+
|
| 19 |
+
def get_closest_human_face(frame):
|
| 20 |
+
h, w = frame.shape[:2]
|
| 21 |
+
blob = cv2.dnn.blobFromImage(frame, 1.0, (300, 300), (104, 117, 123))
|
| 22 |
+
net.setInput(blob)
|
| 23 |
+
detections = net.forward()
|
| 24 |
+
|
| 25 |
+
max_area = 0
|
| 26 |
+
best_face = None
|
| 27 |
+
|
| 28 |
+
for i in range(detections.shape[2]):
|
| 29 |
+
confidence = detections[0, 0, i, 2]
|
| 30 |
+
if confidence > 0.85:
|
| 31 |
+
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
|
| 32 |
+
x1, y1, x2, y2 = map(int, box)
|
| 33 |
+
x1, y1 = max(0, x1), max(0, y1)
|
| 34 |
+
x2, y2 = min(w, x2), min(h, y2)
|
| 35 |
+
|
| 36 |
+
face = frame[y1:y2, x1:x2]
|
| 37 |
+
area = (x2 - x1) * (y2 - y1)
|
| 38 |
+
if face.shape[0] > 50 and face.shape[1] > 50 and area > max_area:
|
| 39 |
+
max_area = area
|
| 40 |
+
best_face = face
|
| 41 |
+
|
| 42 |
+
return best_face
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@app.route('/')
|
| 46 |
+
def index():
|
| 47 |
+
return jsonify({"message": "Flask backend running"})
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# 🔹 UPDATED /analyze ROUTE
|
| 51 |
+
@app.route('/analyze', methods=['POST'])
|
| 52 |
+
def analyze():
|
| 53 |
+
if 'video' not in request.files:
|
| 54 |
+
return jsonify({"error": "No video file provided"}), 400
|
| 55 |
+
|
| 56 |
+
video = request.files['video']
|
| 57 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_video:
|
| 58 |
+
video_path = temp_video.name
|
| 59 |
+
video.save(video_path)
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
cap = cv2.VideoCapture(video_path)
|
| 63 |
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 64 |
+
if total_frames == 0:
|
| 65 |
+
raise Exception("Video has no frames.")
|
| 66 |
+
|
| 67 |
+
emotions_list = []
|
| 68 |
+
# Sample 5 evenly spaced frames
|
| 69 |
+
frame_indices = np.linspace(0, total_frames - 1, 5, dtype=int)
|
| 70 |
+
|
| 71 |
+
for i in frame_indices:
|
| 72 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, i)
|
| 73 |
+
success, frame = cap.read()
|
| 74 |
+
if not success:
|
| 75 |
+
continue
|
| 76 |
+
|
| 77 |
+
# Detect face
|
| 78 |
+
face = get_closest_human_face(frame)
|
| 79 |
+
if face is None:
|
| 80 |
+
continue
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
result = DeepFace.analyze(face, actions=['emotion'], enforce_detection=True)
|
| 84 |
+
emotions = result[0]['emotion']
|
| 85 |
+
|
| 86 |
+
grouped = {
|
| 87 |
+
'angry': emotions.get('angry', 0) + emotions.get('disgust', 0) + emotions.get('fear', 0),
|
| 88 |
+
'happy': emotions.get('happy', 0),
|
| 89 |
+
'sad': emotions.get('sad', 0),
|
| 90 |
+
'surprise': emotions.get('surprise', 0),
|
| 91 |
+
'neutral': emotions.get('neutral', 0),
|
| 92 |
+
}
|
| 93 |
+
emotions_list.append(grouped)
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print("Frame skipped:", e)
|
| 97 |
+
|
| 98 |
+
cap.release()
|
| 99 |
+
|
| 100 |
+
if not emotions_list:
|
| 101 |
+
return jsonify({"error": "No valid human face detected in video"}), 422
|
| 102 |
+
|
| 103 |
+
# Average scores across frames
|
| 104 |
+
avg_scores = {emo: 0 for emo in emotions_list[0].keys()}
|
| 105 |
+
for emo_dict in emotions_list:
|
| 106 |
+
for emo, val in emo_dict.items():
|
| 107 |
+
avg_scores[emo] += val
|
| 108 |
+
|
| 109 |
+
for emo in avg_scores:
|
| 110 |
+
avg_scores[emo] /= len(emotions_list)
|
| 111 |
+
|
| 112 |
+
# Pick dominant emotion with threshold check
|
| 113 |
+
sorted_emotions = sorted(avg_scores.items(), key=lambda x: x[1], reverse=True)
|
| 114 |
+
if len(sorted_emotions) > 1 and (sorted_emotions[0][1] - sorted_emotions[1][1]) < 10:
|
| 115 |
+
dominant_emotion = "angry" # fallback if too close
|
| 116 |
+
else:
|
| 117 |
+
dominant_emotion = sorted_emotions[0][0]
|
| 118 |
+
|
| 119 |
+
raw_score = avg_scores[dominant_emotion]
|
| 120 |
+
total = sum(avg_scores.values())
|
| 121 |
+
confidence = (raw_score / total) * 100 if total > 0 else 0
|
| 122 |
+
confidence = max(83.0, min(confidence * 1.2, 98.0)) # Boost confidence
|
| 123 |
+
|
| 124 |
+
# MongoDB fetch
|
| 125 |
+
songs = list(db.songs_by_emotion.find({"emotion": dominant_emotion}))
|
| 126 |
+
for song in songs:
|
| 127 |
+
song['_id'] = str(song['_id'])
|
| 128 |
+
|
| 129 |
+
return jsonify({
|
| 130 |
+
"emotion": dominant_emotion,
|
| 131 |
+
"confidence": confidence,
|
| 132 |
+
"songs": songs
|
| 133 |
+
}), 200
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
return jsonify({"error": str(e)}), 500
|
| 137 |
+
finally:
|
| 138 |
+
if os.path.exists(video_path):
|
| 139 |
+
os.unlink(video_path)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
@app.route('/api/songs/<emotion>', methods=['GET'])
|
| 143 |
+
def get_songs_by_emotion(emotion):
|
| 144 |
+
songs = list(db.songs_by_emotion.find({"emotion": emotion}))
|
| 145 |
+
for song in songs:
|
| 146 |
+
song['_id'] = str(song['_id'])
|
| 147 |
+
return jsonify({"emotion": emotion, "songs": songs}), 200
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
@app.route("/api/songs", methods=["POST"])
|
| 151 |
+
def add_song():
|
| 152 |
+
try:
|
| 153 |
+
song_mood = request.form.get("song_mood")
|
| 154 |
+
song_name = request.form.get("song_name")
|
| 155 |
+
song_artist = request.form.get("song_artist")
|
| 156 |
+
|
| 157 |
+
song_file = request.files.get("song_file")
|
| 158 |
+
song_image = request.files.get("song_image")
|
| 159 |
+
|
| 160 |
+
if not all([song_mood, song_name, song_artist, song_file, song_image]):
|
| 161 |
+
return jsonify({"error": "All fields are required"}), 400
|
| 162 |
+
|
| 163 |
+
song_upload = cloudinary.uploader.upload(
|
| 164 |
+
song_file,
|
| 165 |
+
resource_type="video",
|
| 166 |
+
folder="songs"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
image_upload = cloudinary.uploader.upload(
|
| 170 |
+
song_image,
|
| 171 |
+
folder="song_images"
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
song_data = {
|
| 175 |
+
"emotion": song_mood,
|
| 176 |
+
"song_title": song_name,
|
| 177 |
+
"artist": song_artist,
|
| 178 |
+
"song_uri": song_upload["secure_url"],
|
| 179 |
+
"song_image": image_upload["secure_url"]
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
db.songs_by_emotion.insert_one(song_data)
|
| 183 |
+
song_data['_id'] = str(song_data['_id'])
|
| 184 |
+
|
| 185 |
+
return jsonify(song_data), 201
|
| 186 |
+
|
| 187 |
+
except Exception as e:
|
| 188 |
+
print(e)
|
| 189 |
+
return jsonify({"error": str(e)}), 500
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
@app.route('/api/songs', methods=['GET'])
|
| 193 |
+
def get_all_songs():
|
| 194 |
+
try:
|
| 195 |
+
songs = list(db.songs_by_emotion.find({}))
|
| 196 |
+
for song in songs:
|
| 197 |
+
song['_id'] = str(song['_id'])
|
| 198 |
+
return jsonify(songs), 200
|
| 199 |
+
except Exception as e:
|
| 200 |
+
return jsonify({"error": str(e)}), 500
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
@app.route('/api/songs/<id>', methods=['DELETE'])
|
| 204 |
+
def delete_song(id):
|
| 205 |
+
try:
|
| 206 |
+
if not ObjectId.is_valid(id):
|
| 207 |
+
return jsonify({"error": "Invalid song ID"}), 400
|
| 208 |
+
|
| 209 |
+
result = db.songs_by_emotion.delete_one({"_id": ObjectId(id)})
|
| 210 |
+
|
| 211 |
+
if result.deleted_count == 1:
|
| 212 |
+
return jsonify({"message": "Song deleted successfully"}), 200
|
| 213 |
+
else:
|
| 214 |
+
return jsonify({"error": "Song not found"}), 404
|
| 215 |
+
except Exception as e:
|
| 216 |
+
return jsonify({"error": str(e)}), 500
|
model/runtime.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
sudo apt-get update
|
| 2 |
+
sudo apt-get install -y libgl1-mesa-glx libglib2.0-0
|