Merge branch 'content-moderation-implementation' into 'main'
Browse filesAdd video processing functionality with OpenCV and Modal integration
See merge request sonne-technology/bsod-tv/waveform-matching-gradio-front-end!21
- app.py +89 -21
- requirements.txt +2 -1
app.py
CHANGED
|
@@ -6,8 +6,11 @@
|
|
| 6 |
|
| 7 |
|
| 8 |
import os
|
|
|
|
| 9 |
import time
|
|
|
|
| 10 |
import modal
|
|
|
|
| 11 |
import logging
|
| 12 |
import gradio as gr
|
| 13 |
|
|
@@ -20,11 +23,11 @@ def process_audio(original_audio_path, dubbed_audio_path, email, company_name, t
|
|
| 20 |
file upload to presigned URLs, and triggering the processing.
|
| 21 |
"""
|
| 22 |
# 1. Check the duration of both audio files.
|
| 23 |
-
|
| 24 |
modal_token_id = os.environ['MODAL_TOKEN_ID']
|
| 25 |
modal_token_secret = os.environ['MODAL_TOKEN_SECRET']
|
| 26 |
modal_environment = os.environ['MODAL_ENVIRONMENT']
|
| 27 |
-
modal_volume = os.environ['
|
| 28 |
processing_id = str(int(time.time()))
|
| 29 |
try:
|
| 30 |
bsodtv_storage = modal.Volume.from_name(modal_volume)
|
|
@@ -50,29 +53,94 @@ def process_audio(original_audio_path, dubbed_audio_path, email, company_name, t
|
|
| 50 |
return "Processing started. Results will be emailed to you shortly."
|
| 51 |
|
| 52 |
|
| 53 |
-
def process_video(video_path, notes, email, company_name):
|
| 54 |
"""
|
| 55 |
-
Process the input video for content moderation.
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
- path to the output video file (str)
|
| 63 |
-
For now, this is a placeholder that returns the input video unchanged.
|
| 64 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
try:
|
| 66 |
-
#
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
# Create a professional Gradio interface using the Golden ratio (1.618) for proportions
|
| 78 |
# Define custom CSS for a professional look
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
import os
|
| 9 |
+
import cv2
|
| 10 |
import time
|
| 11 |
+
import uuid
|
| 12 |
import modal
|
| 13 |
+
import shutil
|
| 14 |
import logging
|
| 15 |
import gradio as gr
|
| 16 |
|
|
|
|
| 23 |
file upload to presigned URLs, and triggering the processing.
|
| 24 |
"""
|
| 25 |
# 1. Check the duration of both audio files.
|
| 26 |
+
waveform_app = modal.App("Waveform-Matching")
|
| 27 |
modal_token_id = os.environ['MODAL_TOKEN_ID']
|
| 28 |
modal_token_secret = os.environ['MODAL_TOKEN_SECRET']
|
| 29 |
modal_environment = os.environ['MODAL_ENVIRONMENT']
|
| 30 |
+
modal_volume = os.environ['WAVEFORM_MODAL_VOLUME']
|
| 31 |
processing_id = str(int(time.time()))
|
| 32 |
try:
|
| 33 |
bsodtv_storage = modal.Volume.from_name(modal_volume)
|
|
|
|
| 53 |
return "Processing started. Results will be emailed to you shortly."
|
| 54 |
|
| 55 |
|
| 56 |
+
def process_video(video_path, notes, email, company_name) -> str:
|
| 57 |
"""
|
| 58 |
+
Process the input video for content moderation using Modal.
|
| 59 |
+
Steps:
|
| 60 |
+
1. Upload the provided video to the configured Modal Volume.
|
| 61 |
+
2. Obtain the video dimensions (width, height).
|
| 62 |
+
3. Call the Content-Moderation reception_function via Modal (synchronously with .remote).
|
| 63 |
+
4. Download the processed video returned by the function to /tmp with a random UUID filename.
|
| 64 |
+
5. Return the local path to the downloaded video.
|
|
|
|
|
|
|
| 65 |
"""
|
| 66 |
+
# Validate inputs
|
| 67 |
+
if not video_path or not os.path.exists(video_path):
|
| 68 |
+
logger.error("Invalid video path provided to process_video.")
|
| 69 |
+
return "Invalid video path."
|
| 70 |
+
|
| 71 |
+
# Helper to obtain width and height
|
| 72 |
+
def _get_video_dimensions(path: str):
|
| 73 |
+
try:
|
| 74 |
+
# type: ignore
|
| 75 |
+
cap = cv2.VideoCapture(path)
|
| 76 |
+
if cap.isOpened():
|
| 77 |
+
width = int(cap.get(3))
|
| 78 |
+
height = int(cap.get(4))
|
| 79 |
+
cap.release()
|
| 80 |
+
except Exception as e:
|
| 81 |
+
logger.debug(f"OpenCV not available or failed to read video dimensions: {e}")
|
| 82 |
+
return width, height
|
| 83 |
+
|
| 84 |
try:
|
| 85 |
+
# 1. Setup Modal app and volume
|
| 86 |
+
moderation_app = modal.App("Content-Moderation")
|
| 87 |
+
_ = os.environ.get('MODAL_TOKEN_ID') # Read to ensure environment readiness (kept for parity with process_audio)
|
| 88 |
+
_ = os.environ.get('MODAL_TOKEN_SECRET')
|
| 89 |
+
_ = os.environ.get('MODAL_ENVIRONMENT')
|
| 90 |
+
modal_volume_name = os.environ['MODERATION_MODAL_VOLUME']
|
| 91 |
+
|
| 92 |
+
# Unique processing folder and paths
|
| 93 |
+
processing_id = str(int(time.time()))
|
| 94 |
+
ext = os.path.splitext(video_path)[1]
|
| 95 |
+
remote_input_path = f"/{processing_id}/input_video{ext}"
|
| 96 |
+
|
| 97 |
+
# 2. Upload video to Modal Volume
|
| 98 |
+
volume = modal.Volume.from_name(modal_volume_name)
|
| 99 |
+
try:
|
| 100 |
+
with volume.batch_upload() as batch:
|
| 101 |
+
batch.put_file(video_path, remote_input_path)
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.error(f"Error uploading video to Modal Storage: {e}")
|
| 104 |
+
return "Error uploading video to Cloud Storage."
|
| 105 |
+
|
| 106 |
+
# 3. Obtain video dimensions
|
| 107 |
+
width, height = _get_video_dimensions(video_path)
|
| 108 |
+
|
| 109 |
+
# 4. Call Modal function synchronously
|
| 110 |
+
try:
|
| 111 |
+
moderation_function = modal.Function.from_name("Content-Moderation", "reception_function")
|
| 112 |
+
processed_remote_path = moderation_function.remote(
|
| 113 |
+
input_text=str(notes) if notes is not None else "",
|
| 114 |
+
video_path=remote_input_path,
|
| 115 |
+
size=(int(width), int(height)),
|
| 116 |
+
)
|
| 117 |
+
except Exception as e:
|
| 118 |
+
logger.error(f"Error calling Modal reception_function: {e}")
|
| 119 |
+
return "Error calling Outpost to trigger processing."
|
| 120 |
+
|
| 121 |
+
if not processed_remote_path or not isinstance(processed_remote_path, str):
|
| 122 |
+
logger.error("Modal function did not return a valid path to the processed video.")
|
| 123 |
+
return "Processing failed to return an output path."
|
| 124 |
+
|
| 125 |
+
# 5. Download the processed video to /tmp with UUID filename
|
| 126 |
+
local_ext = os.path.splitext(processed_remote_path)[1] or ext or ".mp4"
|
| 127 |
+
local_output_path = f"/tmp/{uuid.uuid4().hex}{local_ext}"
|
| 128 |
+
try:
|
| 129 |
+
# Use Modal Volume.read_file to stream the remote file to the local path
|
| 130 |
+
with open(local_output_path, "wb") as dst:
|
| 131 |
+
for chunk in volume.read_file(processed_remote_path):
|
| 132 |
+
if chunk:
|
| 133 |
+
dst.write(chunk)
|
| 134 |
+
except Exception as e:
|
| 135 |
+
logger.error(f"Error downloading processed video from Modal Storage using read_file: {e}")
|
| 136 |
+
return "Error downloading processed video from Cloud Storage."
|
| 137 |
+
|
| 138 |
+
# 6. Return local path
|
| 139 |
+
return local_output_path
|
| 140 |
|
| 141 |
+
except Exception as e:
|
| 142 |
+
logger.error(f"Unexpected error in process_video: {e}")
|
| 143 |
+
return "Unexpected error during video processing."
|
| 144 |
|
| 145 |
# Create a professional Gradio interface using the Golden ratio (1.618) for proportions
|
| 146 |
# Define custom CSS for a professional look
|
requirements.txt
CHANGED
|
@@ -1,2 +1,3 @@
|
|
| 1 |
modal
|
| 2 |
-
gradio
|
|
|
|
|
|
| 1 |
modal
|
| 2 |
+
gradio
|
| 3 |
+
opencv-python-headless
|