Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from flask import Flask
from routes.video_routes import video_routes
import logging
import coloredlogs
from utils.logger import get_logger
from dotenv import load_dotenv
import os
from flask_cors import CORS

load_dotenv()

app = Flask(__name__)
# Initialize logger at module level so Cloud Run deployments
# get structured logging - not just local `python app.py` runs.
logger = get_logger(__name__)

app = Flask(__name__)
CORS(
app,
resources={r"/*": {
Expand All @@ -20,22 +22,14 @@
}},
supports_credentials=True
)

app.register_blueprint(video_routes)

app.config["DEBUG"] = os.environ.get("FLASK_DEBUG", False)

if __name__ == "__main__":
coloredlogs.install(
level="INFO",
fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

logger = logging.getLogger(__name__)
logger.info("Starting the application")
logger.info("Application initialised (routes registered, CORS configured)")

# 🔁 Local
if __name__ == "__main__":
logger.info("Starting development server on localhost:5000")
# Local development server
app.run(host="localhost", port=5000)

# ☁️ Cloud Run (comentado localmente)
# Cloud Run - uncomment for deployment
# app.run(host="0.0.0.0", port=8080)
62 changes: 62 additions & 0 deletions utils/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Centralized logging configuration for the RuxaiLab Facial Sentiment API.

Provides a single `get_logger()` factory so every module uses a consistent
format, level, and handler setup — regardless of whether the app is run
locally or deployed on Cloud Run.

Usage:
from utils.logger import get_logger
logger = get_logger(__name__)
"""

import logging
import os
import sys

_configured = False

def _configure_root_logger() -> None:
"""
Configure the root logger once at import time.
Level is read from the LOG_LEVEL environment variable (default: INFO).
Outputs structured plaintext to stdout so Cloud Run can ingest it.
"""
global _configured
if _configured:
return

log_level_str = os.environ.get("LOG_LEVEL", "INFO").upper()
log_level = getattr(logging, log_level_str, logging.INFO)

handler = logging.StreamHandler(sys.stdout)
handler.setLevel(log_level)

formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S"
)
handler.setFormatter(formatter)

root_logger = logging.getLogger()
root_logger.setLevel(log_level)

# Avoid duplicate handlers if called multiple times
if not root_logger.handlers:
root_logger.addHandler(handler)

_configured = True


def get_logger(name: str) -> logging.Logger:
"""
Return a named logger with guaranteed root configuration applied.

Args:
name: Typically __name__ of the calling module.

Returns:
A configured logging.Logger instance.
"""
_configure_root_logger()
return logging.getLogger(name)
36 changes: 19 additions & 17 deletions utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
import os
import shutil

from utils.logger import get_logger

logger = get_logger(__name__)


def load_model(model_path: str):
return tf.keras.models.load_model(model_path)
Expand All @@ -30,8 +33,8 @@ def getPercentages(predictions):
percentages = {emotion: round((count / len(predictions) * 100), 2) for emotion, count in emotion_count_map.items()}
return percentages

#Delete /static/videos/ content
def delete_video():
"""Remove all files and subdirectories under static/videos/."""
folder = "static/videos/"
for filename in os.listdir(folder):
file_path = os.path.join(folder, filename)
Expand All @@ -40,30 +43,29 @@ def delete_video():
os.unlink(file_path)
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
logger.debug("Deleted: %s", file_path)
except Exception as e:
print('Failed to delete %s. Reason: %s' % (file_path, e))
logger.error("Failed to delete %s: %s", file_path, e)

#Split videos
def split_video_into_clips(video_path):
#Get video length
"""Split a video into 15-second clips and move them to static/videos/."""
cap = cv2.VideoCapture(video_path)

fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) #we get the total number of frames of the video
seconds = frame_count/fps
print(f'Video length: {int(seconds)} seconds')
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
seconds = frame_count / fps
cap.release()

logger.info("Video length: %d seconds", int(seconds))

video_paths = []
#Split video into clips of 10 seconds
for i in range(0, int(seconds), 10):
starttime = i
endtime = i+15
targetname = str(i)+".mp4"
video_paths.append('static/videos/'+targetname)
endtime = i + 15
targetname = str(i) + ".mp4"
video_paths.append('static/videos/' + targetname)
ffmpeg_extract_subclip(video_path, starttime, endtime, targetname=targetname)
#move the video to the clips folder
print(f"Moving {targetname} to clips folder")
#show acutal path
print(os.path.abspath(targetname))
logger.debug("Moving clip %s to static/videos/", targetname)
logger.debug("Absolute path: %s", os.path.abspath(targetname))
shutil.move(targetname, 'static/videos/')

return video_paths