Update app.py
Browse files
app.py
CHANGED
@@ -1,8 +1,9 @@
|
|
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
|
@@ -19,11 +20,9 @@ from dotenv import load_dotenv
|
|
19 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
20 |
load_dotenv()
|
21 |
|
22 |
-
|
23 |
-
# It's crucial to set this in your .env file for deployment.
|
24 |
-
API_BASE_URL = os.environ.get("API_BASE_URL", "")
|
25 |
MODERATION_ENDPOINT = f"{API_BASE_URL}/v3/moderations"
|
26 |
-
|
27 |
# --- Full list of Whisper V3 supported languages ---
|
28 |
# Mapping user-friendly names to ISO 639-1 codes
|
29 |
WHISPER_LANGUAGES = {
|
@@ -48,33 +47,27 @@ WHISPER_LANGUAGES = {
|
|
48 |
"Tagalog": "tl", "Malagasy": "mg", "Assamese": "as", "Tatar": "tt", "Hawaiian": "haw",
|
49 |
"Lingala": "ln", "Hausa": "ha", "Bashkir": "ba", "Javanese": "jw", "Sundanese": "su",
|
50 |
}
|
51 |
-
# Sort languages alphabetically for the dropdown
|
52 |
SORTED_LANGUAGES = dict(sorted(WHISPER_LANGUAGES.items()))
|
53 |
-
|
54 |
-
|
55 |
-
# --- Helper Function ---
|
56 |
def file_to_base64(filepath: str) -> str:
|
57 |
-
|
58 |
-
if not filepath:
|
59 |
-
return None
|
60 |
try:
|
61 |
with open(filepath, "rb") as f:
|
62 |
-
|
63 |
-
return encoded_string
|
64 |
except Exception as e:
|
65 |
logging.error(f"Failed to convert file {filepath} to base64: {e}")
|
66 |
return None
|
67 |
-
|
68 |
-
#
|
|
|
|
|
|
|
|
|
|
|
69 |
def moderate_content(text_input, image_input, video_input, audio_input, language_full_name):
|
70 |
-
"""
|
71 |
-
Prepares the payload, calls the moderation API, and formats the response.
|
72 |
-
"""
|
73 |
if not any([text_input, image_input, video_input, audio_input]):
|
74 |
-
return "Please provide at least one input (text, image, video, or audio).", None
|
75 |
-
|
76 |
logging.info("Preparing payload for moderation API...")
|
77 |
-
payload = {
|
78 |
if text_input: payload["input"] = text_input
|
79 |
if image_b64 := file_to_base64(image_input): payload["image"] = image_b64
|
80 |
if video_b64 := file_to_base64(video_input): payload["video"] = video_b64
|
@@ -83,154 +76,127 @@ def moderate_content(text_input, image_input, video_input, audio_input, language
|
|
83 |
language_code = SORTED_LANGUAGES.get(language_full_name, "en")
|
84 |
payload["language"] = language_code
|
85 |
logging.info(f"Audio detected. Using language: {language_full_name} ({language_code})")
|
86 |
-
|
87 |
logging.info(f"Sending request to {MODERATION_ENDPOINT} with inputs: {list(payload.keys())}")
|
88 |
-
|
89 |
-
summary_output = "An error occurred. Please check the logs."
|
90 |
-
full_response_output = {}
|
91 |
latency_ms = None
|
92 |
-
|
93 |
try:
|
94 |
with httpx.Client(timeout=180.0) as client:
|
95 |
-
start_time = time.monotonic()
|
96 |
response = client.post(MODERATION_ENDPOINT, json=payload)
|
97 |
latency_ms = (time.monotonic() - start_time) * 1000
|
98 |
logging.info(f"API response received in {latency_ms:.2f} ms with status code {response.status_code}")
|
99 |
-
|
100 |
response.raise_for_status()
|
101 |
-
|
102 |
data = response.json()
|
103 |
-
full_response_output = data # <-- MODIFIED: Assign raw data, without adding latency
|
104 |
-
|
105 |
if not data.get("results"):
|
106 |
-
|
107 |
-
return summary_output, full_response_output
|
108 |
-
|
109 |
result = data["results"][0]
|
110 |
-
|
111 |
-
|
112 |
-
reason = result.get("reason") or "
|
113 |
-
transcribed = result.get("transcribed_text") or "
|
114 |
flagged_categories = [cat for cat, flagged in result.get("categories", {}).items() if flagged]
|
115 |
categories_str = ", ".join(flagged_categories) if flagged_categories else "None"
|
116 |
-
|
117 |
-
summary_output = f"""
|
118 |
-
**API Latency:** {latency_ms:.2f} ms
|
119 |
-
---
|
120 |
-
**Moderation Status:** {status}
|
121 |
-
---
|
122 |
-
**Reason:** {reason}
|
123 |
-
---
|
124 |
-
**Flagged Categories:** {categories_str}
|
125 |
-
---
|
126 |
-
**Transcribed Text (from audio):**
|
127 |
-
{transcribed}
|
128 |
-
"""
|
129 |
logging.info("Successfully parsed moderation response.")
|
130 |
-
|
131 |
except httpx.HTTPStatusError as e:
|
132 |
-
|
133 |
-
error_details = ""
|
134 |
-
latency_str = f"**API Latency:** {latency_ms:.2f} ms" if latency_ms is not None else ""
|
135 |
-
|
136 |
try:
|
137 |
error_json = e.response.json()
|
138 |
detail = error_json.get("detail", "No specific error detail provided.")
|
139 |
-
error_details = f"
|
140 |
-
|
141 |
-
full_response_output = {"error": "Backend API Error", "status_code": e.response.status_code, "details": error_json}
|
142 |
except (json.JSONDecodeError, AttributeError):
|
143 |
-
error_details = f"
|
144 |
-
|
145 |
-
full_response_output = {"error": "Backend API Error", "status_code": e.response.status_code, "details": e.response.text}
|
146 |
-
|
147 |
-
summary_output = f"""
|
148 |
-
**π« Error from Moderation Service (HTTP {e.response.status_code})**
|
149 |
-
---
|
150 |
-
{latency_str}
|
151 |
-
|
152 |
-
{user_message}
|
153 |
-
|
154 |
-
{error_details}
|
155 |
-
"""
|
156 |
logging.error(f"HTTP Status Error: {e.response.status_code} - Response: {e.response.text}")
|
157 |
-
|
158 |
except httpx.RequestError as e:
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
summary_output = f"""
|
163 |
-
**π Connection Error**
|
164 |
-
---
|
165 |
-
Could not connect to the API server at `{API_BASE_URL}`. The request failed after {latency_ms:.0f} ms.
|
166 |
-
|
167 |
-
Please ensure the backend server is running and the URL is configured correctly in your `.env` file.
|
168 |
-
"""
|
169 |
-
# <-- MODIFIED: Latency removed from this dictionary
|
170 |
-
full_response_output = {"error": "Connection Error", "url": API_BASE_URL, "details": str(e)}
|
171 |
logging.error(f"Request Error: Could not connect to {API_BASE_URL}. Details: {e}")
|
172 |
-
|
173 |
except Exception as e:
|
174 |
-
summary_output = f"""
|
175 |
-
**π₯ An Unexpected Application Error Occurred**
|
176 |
-
---
|
177 |
-
An error happened within the Gradio application itself.
|
178 |
-
Please check the application logs for more details.
|
179 |
-
|
180 |
-
**Error Type:** `{type(e).__name__}`
|
181 |
-
"""
|
182 |
-
full_response_output = {"error": "Gradio App Internal Error", "type": type(e).__name__, "details": str(e)}
|
183 |
logging.error(f"Unexpected Error in Gradio App: {e}", exc_info=True)
|
184 |
-
|
185 |
-
return summary_output, full_response_output
|
186 |
|
187 |
# --- Gradio Interface ---
|
188 |
-
with gr.Blocks(theme=gr.themes.Soft(), css="footer {display: none !important}") as demo:
|
189 |
gr.Markdown(
|
190 |
"""
|
191 |
# π€ Multimodal Content Moderation Demo
|
192 |
-
This
|
193 |
-
|
194 |
"""
|
195 |
)
|
196 |
-
|
197 |
-
with gr.Row():
|
198 |
-
with gr.Column(scale=1):
|
199 |
-
gr.Markdown("### 1. Provide Your Content")
|
200 |
-
text_input = gr.Textbox(label="Text Input", lines=4, placeholder="Enter any text here...")
|
201 |
-
image_input = gr.Image(label="Image Input", type="filepath")
|
202 |
-
video_input = gr.Video(label="Video Input")
|
203 |
-
audio_input = gr.Audio(label="Voice/Audio Input", type="filepath")
|
204 |
-
|
205 |
-
language_input = gr.Dropdown(
|
206 |
-
label="Audio Language (if providing audio)",
|
207 |
-
choices=list(SORTED_LANGUAGES.keys()),
|
208 |
-
value="English",
|
209 |
-
interactive=True
|
210 |
-
)
|
211 |
-
|
212 |
-
submit_button = gr.Button("Moderate Content", variant="primary")
|
213 |
-
|
214 |
with gr.Column(scale=2):
|
215 |
-
gr.Markdown("###
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
225 |
gr.Examples(
|
226 |
examples=[
|
227 |
-
["
|
228 |
-
["I
|
|
|
|
|
|
|
|
|
|
|
229 |
],
|
230 |
inputs=[text_input, image_input, video_input, audio_input, language_input],
|
231 |
-
|
232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
)
|
|
|
234 |
|
235 |
if __name__ == "__main__":
|
236 |
logging.info(f"Connecting to API server at: {API_BASE_URL}")
|
|
|
1 |
+
# === Gradio Demo App: gradio_app.py (Backward-Compatible Version) ===
|
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 |
+
# NOTE: This version removes the "Copy" button for compatibility with older Gradio versions.
|
7 |
# --------------------------------------------------------------------
|
8 |
|
9 |
import base64
|
|
|
20 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
21 |
load_dotenv()
|
22 |
|
23 |
+
API_BASE_URL = os.environ.get("API_BASE_URL", "http://127.0.0.1:8000")
|
|
|
|
|
24 |
MODERATION_ENDPOINT = f"{API_BASE_URL}/v3/moderations"
|
25 |
+
# ... (rest of the configuration and helper functions remain the same) ...
|
26 |
# --- Full list of Whisper V3 supported languages ---
|
27 |
# Mapping user-friendly names to ISO 639-1 codes
|
28 |
WHISPER_LANGUAGES = {
|
|
|
47 |
"Tagalog": "tl", "Malagasy": "mg", "Assamese": "as", "Tatar": "tt", "Hawaiian": "haw",
|
48 |
"Lingala": "ln", "Hausa": "ha", "Bashkir": "ba", "Javanese": "jw", "Sundanese": "su",
|
49 |
}
|
|
|
50 |
SORTED_LANGUAGES = dict(sorted(WHISPER_LANGUAGES.items()))
|
|
|
|
|
|
|
51 |
def file_to_base64(filepath: str) -> str:
|
52 |
+
if not filepath: return None
|
|
|
|
|
53 |
try:
|
54 |
with open(filepath, "rb") as f:
|
55 |
+
return base64.b64encode(f.read()).decode("utf-8")
|
|
|
56 |
except Exception as e:
|
57 |
logging.error(f"Failed to convert file {filepath} to base64: {e}")
|
58 |
return None
|
59 |
+
def create_status_banner(status_type, text):
|
60 |
+
colors = {"safe": ("#DFF2BF", "#4F8A10"),"flagged": ("#FFD2D2", "#D8000C"),"error": ("#FEEFB3", "#9F6000"),"info": ("#BDE5F8", "#00529B"),}
|
61 |
+
bg_color, text_color = colors.get(status_type, ("#E0E0E0", "#000000"))
|
62 |
+
return f"<div style='background-color:{bg_color}; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; border: 1px solid {text_color};'><h2 style='color:{text_color}; text-align:center; margin:0; font-size: 1.5rem;'>{text}</h2></div>"
|
63 |
+
def clear_outputs():
|
64 |
+
initial_text = "Results will appear here after submission."
|
65 |
+
return (create_status_banner("info", "SUBMIT CONTENT FOR MODERATION"),"N/A",initial_text,initial_text,initial_text,None,)
|
66 |
def moderate_content(text_input, image_input, video_input, audio_input, language_full_name):
|
|
|
|
|
|
|
67 |
if not any([text_input, image_input, video_input, audio_input]):
|
68 |
+
return (create_status_banner("error", "π« NO INPUT PROVIDED π«"),"N/A","Please provide at least one input (text, image, video, or audio) before submitting.","N/A", "N/A", None)
|
|
|
69 |
logging.info("Preparing payload for moderation API...")
|
70 |
+
payload = {"model": "nai-moderation-latest"}
|
71 |
if text_input: payload["input"] = text_input
|
72 |
if image_b64 := file_to_base64(image_input): payload["image"] = image_b64
|
73 |
if video_b64 := file_to_base64(video_input): payload["video"] = video_b64
|
|
|
76 |
language_code = SORTED_LANGUAGES.get(language_full_name, "en")
|
77 |
payload["language"] = language_code
|
78 |
logging.info(f"Audio detected. Using language: {language_full_name} ({language_code})")
|
|
|
79 |
logging.info(f"Sending request to {MODERATION_ENDPOINT} with inputs: {list(payload.keys())}")
|
|
|
|
|
|
|
80 |
latency_ms = None
|
81 |
+
start_time = time.monotonic()
|
82 |
try:
|
83 |
with httpx.Client(timeout=180.0) as client:
|
|
|
84 |
response = client.post(MODERATION_ENDPOINT, json=payload)
|
85 |
latency_ms = (time.monotonic() - start_time) * 1000
|
86 |
logging.info(f"API response received in {latency_ms:.2f} ms with status code {response.status_code}")
|
|
|
87 |
response.raise_for_status()
|
|
|
88 |
data = response.json()
|
|
|
|
|
89 |
if not data.get("results"):
|
90 |
+
return (create_status_banner("error", "EMPTY API RESPONSE"), f"{latency_ms:.2f} ms", "The API returned an empty result. This can happen if media processing fails (e.g., a video with no valid frames).", "N/A", "N/A", data)
|
|
|
|
|
91 |
result = data["results"][0]
|
92 |
+
status_text, status_type = ("π¨ FLAGGED π¨", "flagged") if result["flagged"] else ("β
SAFE β
", "safe")
|
93 |
+
status_banner = create_status_banner(status_type, status_text)
|
94 |
+
reason = result.get("reason") or "No specific reason provided."
|
95 |
+
transcribed = result.get("transcribed_text") or "No audio was provided or transcription was not applicable."
|
96 |
flagged_categories = [cat for cat, flagged in result.get("categories", {}).items() if flagged]
|
97 |
categories_str = ", ".join(flagged_categories) if flagged_categories else "None"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
logging.info("Successfully parsed moderation response.")
|
99 |
+
return (status_banner,f"{latency_ms:.2f} ms",reason,categories_str,transcribed,data)
|
100 |
except httpx.HTTPStatusError as e:
|
101 |
+
latency_str = f"{latency_ms:.2f} ms" if latency_ms is not None else "N/A"
|
102 |
+
full_response, error_details = {}, ""
|
|
|
|
|
103 |
try:
|
104 |
error_json = e.response.json()
|
105 |
detail = error_json.get("detail", "No specific error detail provided.")
|
106 |
+
error_details = f"Server responded with error: {detail}"
|
107 |
+
full_response = {"error": "Backend API Error", "status_code": e.response.status_code, "details": error_json}
|
|
|
108 |
except (json.JSONDecodeError, AttributeError):
|
109 |
+
error_details = f"Could not decode the server's error response:\n{e.response.text}"
|
110 |
+
full_response = {"error": "Backend API Error", "status_code": e.response.status_code, "details": e.response.text}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
logging.error(f"HTTP Status Error: {e.response.status_code} - Response: {e.response.text}")
|
112 |
+
return (create_status_banner("error", f"π« API ERROR (HTTP {e.response.status_code}) π«"), latency_str, error_details, "N/A", "N/A", full_response)
|
113 |
except httpx.RequestError as e:
|
114 |
+
latency_ms = (time.monotonic() - start_time) * 1000
|
115 |
+
error_msg = f"Could not connect to the API server at `{API_BASE_URL}`. Please ensure the backend server is running and the URL is correctly configured."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
logging.error(f"Request Error: Could not connect to {API_BASE_URL}. Details: {e}")
|
117 |
+
return (create_status_banner("error", "π CONNECTION ERROR π"), f"{latency_ms:.0f} ms", error_msg, "N/A", "N/A", {"error": "Connection Error", "url": API_BASE_URL, "details": str(e)})
|
118 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
logging.error(f"Unexpected Error in Gradio App: {e}", exc_info=True)
|
120 |
+
return (create_status_banner("error", "π₯ UNEXPECTED APP ERROR π₯"),"N/A",f"An unexpected error occurred within the Gradio application itself: {type(e).__name__}","N/A", "N/A",{"error": "Gradio App Internal Error", "type": type(e).__name__, "details": str(e)})
|
|
|
121 |
|
122 |
# --- Gradio Interface ---
|
123 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"), css="footer {display: none !important}") as demo:
|
124 |
gr.Markdown(
|
125 |
"""
|
126 |
# π€ Multimodal Content Moderation Demo
|
127 |
+
This interface demonstrates a powerful, multi-input moderation API.
|
128 |
+
Provide any combination of text, image, video, and audio. The system will analyze all inputs together for a comprehensive result.
|
129 |
"""
|
130 |
)
|
131 |
+
with gr.Row(variant="panel"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
132 |
with gr.Column(scale=2):
|
133 |
+
gr.Markdown("### 1. Provide Your Content")
|
134 |
+
with gr.Tabs():
|
135 |
+
with gr.TabItem("π Text"):
|
136 |
+
text_input = gr.Textbox(label="Text Input", lines=8, placeholder="Enter any text here...")
|
137 |
+
with gr.TabItem("πΌοΈ Image"):
|
138 |
+
image_input = gr.Image(label="Image Input", type="filepath")
|
139 |
+
with gr.TabItem("π¬ Video"):
|
140 |
+
video_input = gr.Video(label="Video Input")
|
141 |
+
with gr.TabItem("π€ Audio"):
|
142 |
+
audio_input = gr.Audio(label="Voice/Audio Input", type="filepath")
|
143 |
+
language_input = gr.Dropdown(label="Audio Language", choices=list(SORTED_LANGUAGES.keys()), value="English", interactive=True)
|
144 |
+
with gr.Row():
|
145 |
+
clear_button = gr.Button("Clear All")
|
146 |
+
submit_button = gr.Button("βΆοΈ Moderate Content", variant="primary")
|
147 |
+
with gr.Column(scale=3):
|
148 |
+
gr.Markdown("### 2. Moderation Results")
|
149 |
+
status_output = gr.Markdown(value=create_status_banner("info", "AWAITING SUBMISSION"))
|
150 |
+
with gr.Group():
|
151 |
+
with gr.Row():
|
152 |
+
latency_output = gr.Textbox(label="β±οΈ API Latency", interactive=False)
|
153 |
+
categories_output = gr.Textbox(label="π·οΈ Flagged Categories", interactive=False)
|
154 |
+
reason_output = gr.Textbox(label="βοΈ Reason", interactive=False, lines=2)
|
155 |
+
# MODIFICATION: The Copy button and its surrounding Row have been removed.
|
156 |
+
transcription_output = gr.Textbox(label="π€ Transcribed Text (from audio)", interactive=False, lines=4)
|
157 |
+
with gr.Accordion("Full API Response (JSON)", open=False):
|
158 |
+
full_response_output = gr.JSON(label="Raw JSON Response")
|
159 |
|
160 |
+
demo.load(fn=clear_outputs, inputs=None, outputs=[status_output, latency_output, reason_output, categories_output, transcription_output, full_response_output])
|
161 |
+
|
162 |
+
gr.Markdown("---")
|
163 |
+
gr.Markdown(
|
164 |
+
"""
|
165 |
+
### π‘ Quick Examples
|
166 |
+
<p style='color: #666; font-size: 0.9rem;'>
|
167 |
+
<b>β οΈ Content Warning:</b> The examples below include text that may be offensive or disturbing (e.g., hate speech, violence, sexual content).
|
168 |
+
They are provided solely to demonstrate the capabilities of the moderation model.
|
169 |
+
</p>
|
170 |
+
"""
|
171 |
+
)
|
172 |
gr.Examples(
|
173 |
examples=[
|
174 |
+
["The sun is shining and the birds are singing. It's a beautiful day for a walk in the park.", None, None, None, "English"],
|
175 |
+
["I'm going to kill the process on my computer because it's using too much memory.", None, None, None, "English"],
|
176 |
+
["If you don't give me what I want, I will hunt you down and hurt you.", None, None, None, "English"],
|
177 |
+
["I can't stand people from that country, they are all lazy and untrustworthy.", None, None, None, "English"],
|
178 |
+
["I feel so hopeless and alone. I don't see the point in going on anymore.", None, None, None, "English"],
|
179 |
+
["Looking for a partner for some wild, no-strings-attached fun tonight. Must be over 18.", None, None, None, "English"],
|
180 |
+
["She looks so young and innocent in that picture, I love it.", None, None, None, "English"],
|
181 |
],
|
182 |
inputs=[text_input, image_input, video_input, audio_input, language_input],
|
183 |
+
fn=moderate_content,
|
184 |
+
outputs=[status_output, latency_output, reason_output, categories_output, transcription_output, full_response_output],
|
185 |
+
cache_examples=False,
|
186 |
+
)
|
187 |
+
|
188 |
+
# --- Event Handlers (Backward-Compatible) ---
|
189 |
+
all_inputs = [text_input, image_input, video_input, audio_input, language_input]
|
190 |
+
all_outputs = [status_output, latency_output, reason_output, categories_output, transcription_output, full_response_output]
|
191 |
+
|
192 |
+
submit_button.click(fn=moderate_content, inputs=all_inputs, outputs=all_outputs)
|
193 |
+
clear_button.click(
|
194 |
+
fn=lambda: (None, None, None, None, *clear_outputs()),
|
195 |
+
inputs=None,
|
196 |
+
outputs=[text_input, image_input, video_input, audio_input, *all_outputs],
|
197 |
+
queue=False
|
198 |
)
|
199 |
+
# MODIFICATION: The copy_button.click() handler has been removed entirely.
|
200 |
|
201 |
if __name__ == "__main__":
|
202 |
logging.info(f"Connecting to API server at: {API_BASE_URL}")
|