Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# === Gradio Demo App: gradio_app.py ===
|
| 2 |
+
# This script creates a user-friendly web interface to demonstrate the
|
| 3 |
+
# multimodal moderation capabilities of the main FastAPI server.
|
| 4 |
+
#
|
| 5 |
+
# It interacts with the /v3/moderations endpoint.
|
| 6 |
+
# --------------------------------------------------------------------
|
| 7 |
+
|
| 8 |
+
import base64
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import logging
|
| 12 |
+
|
| 13 |
+
import gradio as gr
|
| 14 |
+
import httpx
|
| 15 |
+
from dotenv import load_dotenv
|
| 16 |
+
|
| 17 |
+
# --- Configuration ---
|
| 18 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
| 19 |
+
load_dotenv()
|
| 20 |
+
|
| 21 |
+
# The URL of your running FastAPI server.
|
| 22 |
+
# It's crucial to set this in your .env file for deployment.
|
| 23 |
+
API_BASE_URL = os.environ.get("API_BASE_URL", "")
|
| 24 |
+
MODERATION_ENDPOINT = f"{API_BASE_URL}/v3/moderations"
|
| 25 |
+
|
| 26 |
+
# --- NEW: Full list of Whisper V3 supported languages ---
|
| 27 |
+
# Mapping user-friendly names to ISO 639-1 codes
|
| 28 |
+
WHISPER_LANGUAGES = {
|
| 29 |
+
"English": "en", "Chinese": "zh", "German": "de", "Spanish": "es", "Russian": "ru",
|
| 30 |
+
"Korean": "ko", "French": "fr", "Japanese": "ja", "Portuguese": "pt", "Turkish": "tr",
|
| 31 |
+
"Polish": "pl", "Catalan": "ca", "Dutch": "nl", "Arabic": "ar", "Swedish": "sv",
|
| 32 |
+
"Italian": "it", "Indonesian": "id", "Hindi": "hi", "Finnish": "fi", "Vietnamese": "vi",
|
| 33 |
+
"Hebrew": "he", "Ukrainian": "uk", "Greek": "el", "Malay": "ms", "Czech": "cs",
|
| 34 |
+
"Romanian": "ro", "Danish": "da", "Hungarian": "hu", "Tamil": "ta", "Norwegian": "no",
|
| 35 |
+
"Thai": "th", "Urdu": "ur", "Croatian": "hr", "Bulgarian": "bg", "Lithuanian": "lt",
|
| 36 |
+
"Latin": "la", "Maori": "mi", "Malayalam": "ml", "Welsh": "cy", "Slovak": "sk",
|
| 37 |
+
"Telugu": "te", "Persian": "fa", "Latvian": "lv", "Bengali": "bn", "Serbian": "sr",
|
| 38 |
+
"Azerbaijani": "az", "Slovenian": "sl", "Kannada": "kn", "Estonian": "et", "Macedonian": "mk",
|
| 39 |
+
"Breton": "br", "Basque": "eu", "Icelandic": "is", "Armenian": "hy", "Nepali": "ne",
|
| 40 |
+
"Mongolian": "mn", "Bosnian": "bs", "Kazakh": "kk", "Albanian": "sq", "Swahili": "sw",
|
| 41 |
+
"Galician": "gl", "Marathi": "mr", "Punjabi": "pa", "Sinhala": "si", "Khmer": "km",
|
| 42 |
+
"Shona": "sn", "Yoruba": "yo", "Somali": "so", "Afrikaans": "af", "Occitan": "oc",
|
| 43 |
+
"Georgian": "ka", "Belarusian": "be", "Tajik": "tg", "Sindhi": "sd", "Gujarati": "gu",
|
| 44 |
+
"Amharic": "am", "Yiddish": "yi", "Lao": "lo", "Uzbek": "uz", "Faroese": "fo",
|
| 45 |
+
"Haitian Creole": "ht", "Pashto": "ps", "Turkmen": "tk", "Nynorsk": "nn", "Maltese": "mt",
|
| 46 |
+
"Sanskrit": "sa", "Luxembourgish": "lb", "Myanmar (Burmese)": "my", "Tibetan": "bo",
|
| 47 |
+
"Tagalog": "tl", "Malagasy": "mg", "Assamese": "as", "Tatar": "tt", "Hawaiian": "haw",
|
| 48 |
+
"Lingala": "ln", "Hausa": "ha", "Bashkir": "ba", "Javanese": "jw", "Sundanese": "su",
|
| 49 |
+
}
|
| 50 |
+
# Sort languages alphabetically for the dropdown
|
| 51 |
+
SORTED_LANGUAGES = dict(sorted(WHISPER_LANGUAGES.items()))
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# --- Helper Function ---
|
| 55 |
+
def file_to_base64(filepath: str) -> str:
|
| 56 |
+
"""Reads a file and converts it to a base64 encoded string."""
|
| 57 |
+
if not filepath:
|
| 58 |
+
return None
|
| 59 |
+
try:
|
| 60 |
+
with open(filepath, "rb") as f:
|
| 61 |
+
encoded_string = base64.b64encode(f.read()).decode("utf-8")
|
| 62 |
+
return encoded_string
|
| 63 |
+
except Exception as e:
|
| 64 |
+
logging.error(f"Failed to convert file {filepath} to base64: {e}")
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
# --- Core Logic ---
|
| 68 |
+
def moderate_content(text_input, image_input, video_input, audio_input, language_full_name):
|
| 69 |
+
"""
|
| 70 |
+
Prepares the payload, calls the moderation API, and formats the response.
|
| 71 |
+
"""
|
| 72 |
+
if not any([text_input, image_input, video_input, audio_input]):
|
| 73 |
+
return "Please provide at least one input (text, image, video, or audio).", None
|
| 74 |
+
|
| 75 |
+
logging.info("Preparing payload for moderation API...")
|
| 76 |
+
payload = {
|
| 77 |
+
"model": "nai-moderation-latest" # This is the model name expected by our API
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if text_input:
|
| 81 |
+
payload["input"] = text_input
|
| 82 |
+
|
| 83 |
+
# Gradio provides file paths; we need to convert them to base64
|
| 84 |
+
image_b64 = file_to_base64(image_input)
|
| 85 |
+
if image_b64:
|
| 86 |
+
payload["image"] = image_b64
|
| 87 |
+
|
| 88 |
+
video_b64 = file_to_base64(video_input)
|
| 89 |
+
if video_b64:
|
| 90 |
+
payload["video"] = video_b64
|
| 91 |
+
|
| 92 |
+
audio_b64 = file_to_base64(audio_input)
|
| 93 |
+
if audio_b64:
|
| 94 |
+
payload["voice"] = audio_b64
|
| 95 |
+
# --- NEW: Add selected language to the payload ---
|
| 96 |
+
language_code = SORTED_LANGUAGES.get(language_full_name, "en") # Default to 'en' if not found
|
| 97 |
+
payload["language"] = language_code
|
| 98 |
+
logging.info(f"Audio detected. Using language: {language_full_name} ({language_code})")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
logging.info(f"Sending request to {MODERATION_ENDPOINT} with inputs: {list(payload.keys())}")
|
| 102 |
+
|
| 103 |
+
summary_output = "An error occurred. Please check the logs."
|
| 104 |
+
full_response_output = {}
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
# Using a synchronous client is simpler for this Gradio function
|
| 108 |
+
with httpx.Client(timeout=180.0) as client:
|
| 109 |
+
response = client.post(MODERATION_ENDPOINT, json=payload)
|
| 110 |
+
response.raise_for_status() # Raises an exception for 4xx/5xx errors
|
| 111 |
+
|
| 112 |
+
data = response.json()
|
| 113 |
+
full_response_output = data
|
| 114 |
+
|
| 115 |
+
if not data.get("results"):
|
| 116 |
+
summary_output = "API returned an empty result. This might happen if media processing fails (e.g., a video with no frames)."
|
| 117 |
+
return summary_output, full_response_output
|
| 118 |
+
|
| 119 |
+
# The v3 endpoint returns a single aggregated result
|
| 120 |
+
result = data["results"][0]
|
| 121 |
+
|
| 122 |
+
# Format a nice, human-readable summary
|
| 123 |
+
status = "π¨ FLAGGED π¨" if result["flagged"] else "β
SAFE β
"
|
| 124 |
+
reason = result.get("reason") or "N/A"
|
| 125 |
+
transcribed = result.get("transcribed_text") or "N/A"
|
| 126 |
+
|
| 127 |
+
# Create a clean list of flagged categories
|
| 128 |
+
flagged_categories = [cat for cat, flagged in result.get("categories", {}).items() if flagged]
|
| 129 |
+
categories_str = ", ".join(flagged_categories) if flagged_categories else "None"
|
| 130 |
+
|
| 131 |
+
summary_output = f"""
|
| 132 |
+
**Moderation Status:** {status}
|
| 133 |
+
---
|
| 134 |
+
**Reason:** {reason}
|
| 135 |
+
---
|
| 136 |
+
**Flagged Categories:** {categories_str}
|
| 137 |
+
---
|
| 138 |
+
**Transcribed Text (from audio):**
|
| 139 |
+
{transcribed}
|
| 140 |
+
"""
|
| 141 |
+
logging.info("Successfully received and parsed moderation response.")
|
| 142 |
+
|
| 143 |
+
except httpx.HTTPStatusError as e:
|
| 144 |
+
error_details = e.response.text
|
| 145 |
+
summary_output = f"HTTP Error: {e.response.status_code}\n\nCould not connect to the moderation service or the service returned an error.\n\nDetails:\n{error_details}"
|
| 146 |
+
logging.error(f"HTTP Status Error: {error_details}")
|
| 147 |
+
except httpx.RequestError as e:
|
| 148 |
+
summary_output = f"Request Error: Could not connect to the API server at {API_BASE_URL}.\nPlease ensure the server is running and the URL is correct."
|
| 149 |
+
logging.error(f"Request Error: {e}")
|
| 150 |
+
except Exception as e:
|
| 151 |
+
summary_output = f"An unexpected error occurred: {str(e)}"
|
| 152 |
+
logging.error(f"Unexpected Error: {e}", exc_info=True)
|
| 153 |
+
|
| 154 |
+
return summary_output, full_response_output
|
| 155 |
+
|
| 156 |
+
# --- Gradio Interface ---
|
| 157 |
+
with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as demo:
|
| 158 |
+
gr.Markdown(
|
| 159 |
+
"""
|
| 160 |
+
# π€ Multimodal Content Moderation Demo
|
| 161 |
+
This demo uses a custom API server to perform advanced content moderation.
|
| 162 |
+
You can provide any combination of text, image, video, and audio. The system will analyze all inputs together.
|
| 163 |
+
"""
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
with gr.Row():
|
| 167 |
+
with gr.Column(scale=1):
|
| 168 |
+
gr.Markdown("### 1. Provide Your Content")
|
| 169 |
+
text_input = gr.Textbox(label="Text Input", lines=4, placeholder="Enter any text here...")
|
| 170 |
+
image_input = gr.Image(label="Image Input", type="filepath")
|
| 171 |
+
video_input = gr.Video(label="Video Input")
|
| 172 |
+
audio_input = gr.Audio(label="Voice/Audio Input", type="filepath")
|
| 173 |
+
|
| 174 |
+
# --- NEW: Language selection dropdown ---
|
| 175 |
+
language_input = gr.Dropdown(
|
| 176 |
+
label="Audio Language (if providing audio)",
|
| 177 |
+
choices=list(SORTED_LANGUAGES.keys()),
|
| 178 |
+
value="English",
|
| 179 |
+
interactive=True
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
submit_button = gr.Button("Moderate Content", variant="primary")
|
| 183 |
+
|
| 184 |
+
with gr.Column(scale=2):
|
| 185 |
+
gr.Markdown("### 2. See the Results")
|
| 186 |
+
result_output = gr.Markdown(label="Moderation Summary")
|
| 187 |
+
full_response_output = gr.JSON(label="Full API Response")
|
| 188 |
+
|
| 189 |
+
submit_button.click(
|
| 190 |
+
fn=moderate_content,
|
| 191 |
+
# --- UPDATED: Add language_input to the list ---
|
| 192 |
+
inputs=[text_input, image_input, video_input, audio_input, language_input],
|
| 193 |
+
outputs=[result_output, full_response_output]
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
gr.Examples(
|
| 197 |
+
examples=[
|
| 198 |
+
["This is a test of the system with safe text.", None, None, None, "English"],
|
| 199 |
+
["I am going to kill the process on my computer.", None, None, None, "English"],
|
| 200 |
+
],
|
| 201 |
+
# --- UPDATED: Add language_input to the list ---
|
| 202 |
+
inputs=[text_input, image_input, video_input, audio_input, language_input],
|
| 203 |
+
outputs=[result_output, full_response_output],
|
| 204 |
+
fn=moderate_content
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
logging.info(f"Connecting to API server at: {API_BASE_URL}")
|
| 210 |
+
if API_BASE_URL == "http://127.0.0.1:8000":
|
| 211 |
+
logging.warning("API_BASE_URL is set to the default local address. Make sure this is correct or set it in your .env file.")
|
| 212 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|