Spaces:
Running
Running
update-demo
#1
by
gabrielchua
- opened
- .DS_Store +0 -0
- .gitattributes +0 -1
- Dockerfile +1 -1
- README.md +5 -3
- app.py +450 -0
- app/.DS_Store +0 -0
- app/backend/__pycache__/models.cpython-313.pyc +0 -0
- app/backend/__pycache__/services.cpython-313.pyc +0 -0
- app/frontend/index.html +40 -184
- app/frontend/logo.png +0 -3
- app/frontend/script.js +7 -73
- app/frontend/style.css +22 -186
- requirements.txt +2 -1
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.gitattributes
CHANGED
|
@@ -33,4 +33,3 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
-
app/frontend/logo.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
Dockerfile
CHANGED
|
@@ -32,4 +32,4 @@ EXPOSE 7860
|
|
| 32 |
# Run the FastAPI app from the backend directory so local imports resolve
|
| 33 |
WORKDIR $HOME/app/app/backend
|
| 34 |
|
| 35 |
-
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 32 |
# Run the FastAPI app from the backend directory so local imports resolve
|
| 33 |
WORKDIR $HOME/app/app/backend
|
| 34 |
|
| 35 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🦁
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: yellow
|
| 6 |
-
sdk:
|
| 7 |
-
|
|
|
|
| 8 |
pinned: false
|
|
|
|
| 9 |
short_description: Localised Multilingual Moderation Classifier for Singapore
|
| 10 |
---
|
| 11 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Lionguard 2 Demo
|
| 3 |
emoji: 🦁
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: yellow
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.34.2
|
| 8 |
+
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
header: mini
|
| 11 |
short_description: Localised Multilingual Moderation Classifier for Singapore
|
| 12 |
---
|
| 13 |
|
app.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
# Standard imports
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import uuid
|
| 10 |
+
import asyncio
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
# Third party imports
|
| 14 |
+
import openai
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import gspread
|
| 17 |
+
from google.oauth2 import service_account
|
| 18 |
+
from transformers import AutoModel
|
| 19 |
+
|
| 20 |
+
# Local imports
|
| 21 |
+
from utils import get_embeddings
|
| 22 |
+
|
| 23 |
+
# --- Categories
|
| 24 |
+
CATEGORIES = {
|
| 25 |
+
"binary": ["binary"],
|
| 26 |
+
"hateful": ["hateful_l1", "hateful_l2"],
|
| 27 |
+
"insults": ["insults"],
|
| 28 |
+
"sexual": [
|
| 29 |
+
"sexual_l1",
|
| 30 |
+
"sexual_l2",
|
| 31 |
+
],
|
| 32 |
+
"physical_violence": ["physical_violence"],
|
| 33 |
+
"self_harm": ["self_harm_l1", "self_harm_l2"],
|
| 34 |
+
"all_other_misconduct": [
|
| 35 |
+
"all_other_misconduct_l1",
|
| 36 |
+
"all_other_misconduct_l2",
|
| 37 |
+
],
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# --- OpenAI Setup ---
|
| 41 |
+
# Create both sync and async clients
|
| 42 |
+
client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 43 |
+
async_client = openai.AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 44 |
+
|
| 45 |
+
# --- Model Loading ---
|
| 46 |
+
def load_lionguard2():
|
| 47 |
+
model = AutoModel.from_pretrained("govtech/lionguard-2", trust_remote_code=True)
|
| 48 |
+
return model
|
| 49 |
+
|
| 50 |
+
model = load_lionguard2()
|
| 51 |
+
|
| 52 |
+
# --- Google Sheets Config ---
|
| 53 |
+
GOOGLE_SHEET_URL = os.environ.get("GOOGLE_SHEET_URL")
|
| 54 |
+
GOOGLE_CREDENTIALS = os.environ.get("GCP_SERVICE_ACCOUNT")
|
| 55 |
+
RESULTS_SHEET_NAME = "results"
|
| 56 |
+
VOTES_SHEET_NAME = "votes"
|
| 57 |
+
CHATBOT_SHEET_NAME = "chatbot"
|
| 58 |
+
|
| 59 |
+
def get_gspread_client():
|
| 60 |
+
credentials = service_account.Credentials.from_service_account_info(
|
| 61 |
+
json.loads(GOOGLE_CREDENTIALS),
|
| 62 |
+
scopes=[
|
| 63 |
+
"https://www.googleapis.com/auth/spreadsheets",
|
| 64 |
+
"https://www.googleapis.com/auth/drive",
|
| 65 |
+
],
|
| 66 |
+
)
|
| 67 |
+
return gspread.authorize(credentials)
|
| 68 |
+
|
| 69 |
+
def save_results_data(row):
|
| 70 |
+
try:
|
| 71 |
+
gc = get_gspread_client()
|
| 72 |
+
sheet = gc.open_by_url(GOOGLE_SHEET_URL)
|
| 73 |
+
ws = sheet.worksheet(RESULTS_SHEET_NAME)
|
| 74 |
+
ws.append_row(list(row.values()))
|
| 75 |
+
except Exception as e:
|
| 76 |
+
print(f"Error saving results data: {e}")
|
| 77 |
+
|
| 78 |
+
def save_vote_data(text_id, agree):
|
| 79 |
+
try:
|
| 80 |
+
gc = get_gspread_client()
|
| 81 |
+
sheet = gc.open_by_url(GOOGLE_SHEET_URL)
|
| 82 |
+
ws = sheet.worksheet(VOTES_SHEET_NAME)
|
| 83 |
+
vote_row = {
|
| 84 |
+
"datetime": datetime.now().isoformat(),
|
| 85 |
+
"text_id": text_id,
|
| 86 |
+
"agree": agree
|
| 87 |
+
}
|
| 88 |
+
ws.append_row(list(vote_row.values()))
|
| 89 |
+
except Exception as e:
|
| 90 |
+
print(f"Error saving vote data: {e}")
|
| 91 |
+
|
| 92 |
+
def log_chatbot_data(row):
|
| 93 |
+
try:
|
| 94 |
+
gc = get_gspread_client()
|
| 95 |
+
sheet = gc.open_by_url(GOOGLE_SHEET_URL)
|
| 96 |
+
ws = sheet.worksheet(CHATBOT_SHEET_NAME)
|
| 97 |
+
ws.append_row([
|
| 98 |
+
row["datetime"], row["text_id"], row["text"], row["binary_score"],
|
| 99 |
+
row["hateful_l1_score"], row["hateful_l2_score"], row["insults_score"],
|
| 100 |
+
row["sexual_l1_score"], row["sexual_l2_score"], row["physical_violence_score"],
|
| 101 |
+
row["self_harm_l1_score"], row["self_harm_l2_score"], row["aom_l1_score"],
|
| 102 |
+
row["aom_l2_score"], row["openai_score"]
|
| 103 |
+
])
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"Error saving chatbot data: {e}")
|
| 106 |
+
|
| 107 |
+
# --- Classifier logic ---
|
| 108 |
+
|
| 109 |
+
def format_score_with_style(score_str):
|
| 110 |
+
if score_str == "-":
|
| 111 |
+
return '<span style="color: #9ca3af;">-</span>'
|
| 112 |
+
try:
|
| 113 |
+
score = float(score_str)
|
| 114 |
+
percentage = int(score * 100)
|
| 115 |
+
if score < 0.4:
|
| 116 |
+
return f'<span style="color: #34d399; font-weight:600;">👌 {percentage}%</span>'
|
| 117 |
+
elif 0.4 <= score < 0.7:
|
| 118 |
+
return f'<span style="color: #fbbf24; font-weight:600;">⚠️ {percentage}%</span>'
|
| 119 |
+
else:
|
| 120 |
+
return f'<span style="color: #fca5a5; font-weight:600;">🚨 {percentage}%</span>'
|
| 121 |
+
except:
|
| 122 |
+
return score_str
|
| 123 |
+
|
| 124 |
+
def format_binary_score(score):
|
| 125 |
+
percentage = int(score * 100)
|
| 126 |
+
if score < 0.4:
|
| 127 |
+
return f'<div style="background:linear-gradient(135deg, #065f46 0%, #047857 100%); color:#34d399; padding:24px 0; border-radius:20px; text-align:center; font-weight:900; border:3px solid #10b981; font-size:24px; margin:24px 0; box-shadow:0 4px 24px rgba(0,0,0,0.3);">✅ Pass ({percentage}/100)</div>'
|
| 128 |
+
elif 0.4 <= score < 0.7:
|
| 129 |
+
return f'<div style="background:linear-gradient(135deg, #92400e 0%, #b45309 100%); color:#fbbf24; padding:24px 0; border-radius:20px; text-align:center; font-weight:900; border:3px solid #f59e0b; font-size:24px; margin:24px 0; box-shadow:0 4px 24px rgba(0,0,0,0.3);">⚠️ Warning ({percentage}/100)</div>'
|
| 130 |
+
else:
|
| 131 |
+
return f'<div style="background:linear-gradient(135deg, #991b1b 0%, #b91c1c 100%); color:#fca5a5; padding:24px 0; border-radius:20px; text-align:center; font-weight:900; border:3px solid #ef4444; font-size:24px; margin:24px 0; box-shadow:0 4px 24px rgba(0,0,0,0.3);">🚨 Fail ({percentage}/100)</div>'
|
| 132 |
+
|
| 133 |
+
def analyze_text(text):
|
| 134 |
+
if not text.strip():
|
| 135 |
+
empty_html = '<div style="text-align: center; color: #9ca3af; padding: 30px; font-style: italic;">Enter text to analyze</div>'
|
| 136 |
+
return empty_html, empty_html, "", ""
|
| 137 |
+
try:
|
| 138 |
+
text_id = str(uuid.uuid4())
|
| 139 |
+
embeddings = get_embeddings([text])
|
| 140 |
+
results = model.predict(embeddings)
|
| 141 |
+
binary_score = results.get('binary', [0.0])[0]
|
| 142 |
+
|
| 143 |
+
main_categories = ['hateful', 'insults', 'sexual', 'physical_violence', 'self_harm', 'all_other_misconduct']
|
| 144 |
+
categories_html = []
|
| 145 |
+
max_scores = {}
|
| 146 |
+
for category in main_categories:
|
| 147 |
+
subcategories = CATEGORIES[category]
|
| 148 |
+
category_name = category.replace('_', ' ').title()
|
| 149 |
+
category_emojis = {
|
| 150 |
+
'Hateful': '🤬',
|
| 151 |
+
'Insults': '💢',
|
| 152 |
+
'Sexual': '🔞',
|
| 153 |
+
'Physical Violence': '⚔️',
|
| 154 |
+
'Self Harm': '☹️',
|
| 155 |
+
'All Other Misconduct': '🙅♀️'
|
| 156 |
+
}
|
| 157 |
+
category_display = f"{category_emojis.get(category_name, '📝')} {category_name}"
|
| 158 |
+
level_scores = [results.get(subcategory_key, [0.0])[0] for subcategory_key in subcategories]
|
| 159 |
+
max_score = max(level_scores) if level_scores else 0.0
|
| 160 |
+
max_scores[category] = max_score
|
| 161 |
+
categories_html.append(f'''
|
| 162 |
+
<tr>
|
| 163 |
+
<td>{category_display}</td>
|
| 164 |
+
<td style="text-align: center;">{format_score_with_style(f"{max_score:.4f}")}</td>
|
| 165 |
+
</tr>
|
| 166 |
+
''')
|
| 167 |
+
|
| 168 |
+
html_table = f'''
|
| 169 |
+
<table style="width:100%">
|
| 170 |
+
<thead>
|
| 171 |
+
<tr><th>Category</th><th>Score</th></tr>
|
| 172 |
+
</thead>
|
| 173 |
+
<tbody>
|
| 174 |
+
{''.join(categories_html)}
|
| 175 |
+
</tbody>
|
| 176 |
+
</table>
|
| 177 |
+
'''
|
| 178 |
+
|
| 179 |
+
# Save to Google Sheets if enabled
|
| 180 |
+
if GOOGLE_SHEET_URL and GOOGLE_CREDENTIALS:
|
| 181 |
+
results_row = {
|
| 182 |
+
"datetime": datetime.now().isoformat(),
|
| 183 |
+
"text_id": text_id,
|
| 184 |
+
"text": text,
|
| 185 |
+
"binary_score": binary_score,
|
| 186 |
+
}
|
| 187 |
+
for category in main_categories:
|
| 188 |
+
results_row[f"{category}_max"] = max_scores[category]
|
| 189 |
+
save_results_data(results_row)
|
| 190 |
+
|
| 191 |
+
voting_html = '<div>Help improve LionGuard2! Rate the analysis below.</div>'
|
| 192 |
+
return format_binary_score(binary_score), html_table, text_id, voting_html
|
| 193 |
+
|
| 194 |
+
except Exception as e:
|
| 195 |
+
error_msg = f"Error analyzing text: {str(e)}"
|
| 196 |
+
return f'<div style="color: #fca5a5;">❌ {error_msg}</div>', '', '', ''
|
| 197 |
+
|
| 198 |
+
def vote_thumbs_up(text_id):
|
| 199 |
+
if text_id and GOOGLE_SHEET_URL and GOOGLE_CREDENTIALS:
|
| 200 |
+
save_vote_data(text_id, True)
|
| 201 |
+
return '<div style="color: #34d399; font-weight:700;">🎉 Thank you!</div>'
|
| 202 |
+
return '<div>Voting not available or analysis not yet run.</div>'
|
| 203 |
+
|
| 204 |
+
def vote_thumbs_down(text_id):
|
| 205 |
+
if text_id and GOOGLE_SHEET_URL and GOOGLE_CREDENTIALS:
|
| 206 |
+
save_vote_data(text_id, False)
|
| 207 |
+
return '<div style="color: #fca5a5; font-weight:700;">📝 Thanks for the feedback!</div>'
|
| 208 |
+
return '<div>Voting not available or analysis not yet run.</div>'
|
| 209 |
+
|
| 210 |
+
# --- Guardrail Comparison logic (ASYNC VERSION) ---
|
| 211 |
+
|
| 212 |
+
async def get_openai_response_async(message, system_prompt="You are a helpful assistant."):
|
| 213 |
+
"""Async version of OpenAI API call"""
|
| 214 |
+
try:
|
| 215 |
+
response = await async_client.chat.completions.create(
|
| 216 |
+
model="gpt-4.1-nano",
|
| 217 |
+
messages=[
|
| 218 |
+
{"role": "system", "content": system_prompt},
|
| 219 |
+
{"role": "user", "content": message}
|
| 220 |
+
],
|
| 221 |
+
max_tokens=500,
|
| 222 |
+
temperature=0,
|
| 223 |
+
seed=42,
|
| 224 |
+
)
|
| 225 |
+
return response.choices[0].message.content
|
| 226 |
+
except Exception as e:
|
| 227 |
+
return f"Error: {str(e)}. Please check your OpenAI API key."
|
| 228 |
+
|
| 229 |
+
async def openai_moderation_async(message):
|
| 230 |
+
"""Async version of OpenAI moderation"""
|
| 231 |
+
try:
|
| 232 |
+
response = await async_client.moderations.create(input=message)
|
| 233 |
+
return response.results[0].flagged
|
| 234 |
+
except Exception as e:
|
| 235 |
+
print(f"Error in OpenAI moderation: {e}")
|
| 236 |
+
return False
|
| 237 |
+
|
| 238 |
+
def lionguard_2_sync(message, threshold=0.5):
|
| 239 |
+
"""LionGuard remains sync as it's using a local model"""
|
| 240 |
+
try:
|
| 241 |
+
embeddings = get_embeddings([message])
|
| 242 |
+
results = model.predict(embeddings)
|
| 243 |
+
binary_prob = results['binary'][0]
|
| 244 |
+
return binary_prob > threshold, binary_prob
|
| 245 |
+
except Exception as e:
|
| 246 |
+
print(f"Error in LionGuard 2: {e}")
|
| 247 |
+
return False, 0.0
|
| 248 |
+
|
| 249 |
+
async def process_no_moderation(message, history_no_mod):
|
| 250 |
+
"""Process message without moderation"""
|
| 251 |
+
no_mod_response = await get_openai_response_async(message)
|
| 252 |
+
history_no_mod.append({"role": "user", "content": message})
|
| 253 |
+
history_no_mod.append({"role": "assistant", "content": no_mod_response})
|
| 254 |
+
return history_no_mod
|
| 255 |
+
|
| 256 |
+
async def process_openai_moderation(message, history_openai):
|
| 257 |
+
"""Process message with OpenAI moderation"""
|
| 258 |
+
openai_flagged = await openai_moderation_async(message)
|
| 259 |
+
history_openai.append({"role": "user", "content": message})
|
| 260 |
+
if openai_flagged:
|
| 261 |
+
openai_response = "🚫 This message has been flagged by OpenAI moderation"
|
| 262 |
+
history_openai.append({"role": "assistant", "content": openai_response})
|
| 263 |
+
else:
|
| 264 |
+
openai_response = await get_openai_response_async(message)
|
| 265 |
+
history_openai.append({"role": "assistant", "content": openai_response})
|
| 266 |
+
return history_openai
|
| 267 |
+
|
| 268 |
+
async def process_lionguard(message, history_lg):
|
| 269 |
+
"""Process message with LionGuard 2"""
|
| 270 |
+
# Run LionGuard sync check in thread pool to not block
|
| 271 |
+
loop = asyncio.get_event_loop()
|
| 272 |
+
lg_flagged, lg_score = await loop.run_in_executor(None, lionguard_2_sync, message, 0.5)
|
| 273 |
+
|
| 274 |
+
history_lg.append({"role": "user", "content": message})
|
| 275 |
+
if lg_flagged:
|
| 276 |
+
lg_response = "🚫 This message has been flagged by LionGuard 2"
|
| 277 |
+
history_lg.append({"role": "assistant", "content": lg_response})
|
| 278 |
+
else:
|
| 279 |
+
lg_response = await get_openai_response_async(message)
|
| 280 |
+
history_lg.append({"role": "assistant", "content": lg_response})
|
| 281 |
+
return history_lg, lg_score
|
| 282 |
+
|
| 283 |
+
async def process_message_async(message, history_no_mod, history_openai, history_lg):
|
| 284 |
+
"""Process message concurrently across all three guardrails"""
|
| 285 |
+
if not message.strip():
|
| 286 |
+
return history_no_mod, history_openai, history_lg, ""
|
| 287 |
+
|
| 288 |
+
# Run all three processes concurrently using asyncio.gather
|
| 289 |
+
results = await asyncio.gather(
|
| 290 |
+
process_no_moderation(message, history_no_mod),
|
| 291 |
+
process_openai_moderation(message, history_openai),
|
| 292 |
+
process_lionguard(message, history_lg),
|
| 293 |
+
return_exceptions=True # Continue even if one fails
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
# Unpack results
|
| 297 |
+
history_no_mod = results[0] if not isinstance(results[0], Exception) else history_no_mod
|
| 298 |
+
history_openai = results[1] if not isinstance(results[1], Exception) else history_openai
|
| 299 |
+
history_lg_result = results[2] if not isinstance(results[2], Exception) else (history_lg, 0.0)
|
| 300 |
+
history_lg = history_lg_result[0]
|
| 301 |
+
lg_score = history_lg_result[1] if isinstance(history_lg_result, tuple) else 0.0
|
| 302 |
+
|
| 303 |
+
# --- Logging for chatbot worksheet (runs in background) ---
|
| 304 |
+
if GOOGLE_SHEET_URL and GOOGLE_CREDENTIALS:
|
| 305 |
+
try:
|
| 306 |
+
loop = asyncio.get_event_loop()
|
| 307 |
+
# Run logging in thread pool so it doesn't block
|
| 308 |
+
loop.run_in_executor(None, _log_chatbot_sync, message, lg_score)
|
| 309 |
+
except Exception as e:
|
| 310 |
+
print(f"Chatbot logging failed: {e}")
|
| 311 |
+
|
| 312 |
+
return history_no_mod, history_openai, history_lg, ""
|
| 313 |
+
|
| 314 |
+
def _log_chatbot_sync(message, lg_score):
|
| 315 |
+
"""Sync helper for logging - runs in thread pool"""
|
| 316 |
+
try:
|
| 317 |
+
embeddings = get_embeddings([message])
|
| 318 |
+
results = model.predict(embeddings)
|
| 319 |
+
now = datetime.now().isoformat()
|
| 320 |
+
text_id = str(uuid.uuid4())
|
| 321 |
+
row = {
|
| 322 |
+
"datetime": now,
|
| 323 |
+
"text_id": text_id,
|
| 324 |
+
"text": message,
|
| 325 |
+
"binary_score": results.get("binary", [None])[0],
|
| 326 |
+
"hateful_l1_score": results.get(CATEGORIES['hateful'][0], [None])[0],
|
| 327 |
+
"hateful_l2_score": results.get(CATEGORIES['hateful'][1], [None])[0],
|
| 328 |
+
"insults_score": results.get(CATEGORIES['insults'][0], [None])[0],
|
| 329 |
+
"sexual_l1_score": results.get(CATEGORIES['sexual'][0], [None])[0],
|
| 330 |
+
"sexual_l2_score": results.get(CATEGORIES['sexual'][1], [None])[0],
|
| 331 |
+
"physical_violence_score": results.get(CATEGORIES['physical_violence'][0], [None])[0],
|
| 332 |
+
"self_harm_l1_score": results.get(CATEGORIES['self_harm'][0], [None])[0],
|
| 333 |
+
"self_harm_l2_score": results.get(CATEGORIES['self_harm'][1], [None])[0],
|
| 334 |
+
"aom_l1_score": results.get(CATEGORIES['all_other_misconduct'][0], [None])[0],
|
| 335 |
+
"aom_l2_score": results.get(CATEGORIES['all_other_misconduct'][1], [None])[0],
|
| 336 |
+
"openai_score": None
|
| 337 |
+
}
|
| 338 |
+
try:
|
| 339 |
+
openai_result = client.moderations.create(input=message)
|
| 340 |
+
row["openai_score"] = float(openai_result.results[0].category_scores.get("hate", 0.0))
|
| 341 |
+
except Exception:
|
| 342 |
+
row["openai_score"] = None
|
| 343 |
+
|
| 344 |
+
log_chatbot_data(row)
|
| 345 |
+
except Exception as e:
|
| 346 |
+
print(f"Error in sync logging: {e}")
|
| 347 |
+
|
| 348 |
+
def process_message(message, history_no_mod, history_openai, history_lg):
|
| 349 |
+
"""Wrapper function for Gradio (converts async to sync)"""
|
| 350 |
+
return asyncio.run(process_message_async(message, history_no_mod, history_openai, history_lg))
|
| 351 |
+
|
| 352 |
+
def clear_all_chats():
|
| 353 |
+
return [], [], []
|
| 354 |
+
|
| 355 |
+
# ---- MAIN GRADIO UI ----
|
| 356 |
+
|
| 357 |
+
DISCLAIMER = """
|
| 358 |
+
<div style='background: #fbbf24; color: #1e293b; border-radius: 8px; padding: 14px; margin-bottom: 12px; font-size: 15px; font-weight:500;'>
|
| 359 |
+
⚠️ LionGuard 2 may make mistakes. All entries are logged (anonymised) to improve the model.
|
| 360 |
+
</div>
|
| 361 |
+
"""
|
| 362 |
+
|
| 363 |
+
with gr.Blocks(title="LionGuard 2 Demo", theme=gr.themes.Soft()) as demo:
|
| 364 |
+
gr.HTML("<h1 style='text-align:center'>LionGuard 2 Demo</h1>")
|
| 365 |
+
|
| 366 |
+
with gr.Tabs():
|
| 367 |
+
with gr.Tab("Classifier"):
|
| 368 |
+
gr.HTML(DISCLAIMER)
|
| 369 |
+
with gr.Row():
|
| 370 |
+
with gr.Column(scale=1, min_width=400):
|
| 371 |
+
text_input = gr.Textbox(
|
| 372 |
+
label="Enter text to analyze:",
|
| 373 |
+
placeholder="Type your text here...",
|
| 374 |
+
lines=8,
|
| 375 |
+
max_lines=16,
|
| 376 |
+
container=True
|
| 377 |
+
)
|
| 378 |
+
analyze_btn = gr.Button("Analyze", variant="primary")
|
| 379 |
+
with gr.Column(scale=1, min_width=400):
|
| 380 |
+
binary_output = gr.HTML(
|
| 381 |
+
value='<div style="text-align: center; color: #9ca3af; padding: 30px; font-style: italic; font-size:36px;">Enter text to analyze</div>'
|
| 382 |
+
)
|
| 383 |
+
category_table = gr.HTML(
|
| 384 |
+
value='<div style="text-align: center; color: #9ca3af; padding: 30px; font-style: italic;">Category scores will appear here after analysis</div>'
|
| 385 |
+
)
|
| 386 |
+
voting_feedback = gr.HTML(value="")
|
| 387 |
+
current_text_id = gr.Textbox(value="", visible=False)
|
| 388 |
+
|
| 389 |
+
with gr.Row(visible=False) as voting_buttons_row:
|
| 390 |
+
thumbs_up_btn = gr.Button("👍 Looks Accurate", variant="primary")
|
| 391 |
+
thumbs_down_btn = gr.Button("👎 Looks Wrong", variant="secondary")
|
| 392 |
+
|
| 393 |
+
def analyze_and_show_voting(text):
|
| 394 |
+
binary_score, category_table_val, text_id, voting_html = analyze_text(text)
|
| 395 |
+
show_vote = gr.update(visible=True) if text_id else gr.update(visible=False)
|
| 396 |
+
return binary_score, category_table_val, text_id, show_vote, "", ""
|
| 397 |
+
|
| 398 |
+
analyze_btn.click(
|
| 399 |
+
analyze_and_show_voting,
|
| 400 |
+
inputs=[text_input],
|
| 401 |
+
outputs=[binary_output, category_table, current_text_id, voting_buttons_row, voting_feedback, voting_feedback]
|
| 402 |
+
)
|
| 403 |
+
text_input.submit(
|
| 404 |
+
analyze_and_show_voting,
|
| 405 |
+
inputs=[text_input],
|
| 406 |
+
outputs=[binary_output, category_table, current_text_id, voting_buttons_row, voting_feedback, voting_feedback]
|
| 407 |
+
)
|
| 408 |
+
thumbs_up_btn.click(vote_thumbs_up, inputs=[current_text_id], outputs=[voting_feedback])
|
| 409 |
+
thumbs_down_btn.click(vote_thumbs_down, inputs=[current_text_id], outputs=[voting_feedback])
|
| 410 |
+
|
| 411 |
+
with gr.Tab("Guardrail Comparison"):
|
| 412 |
+
gr.HTML(DISCLAIMER)
|
| 413 |
+
with gr.Row():
|
| 414 |
+
with gr.Column(scale=1):
|
| 415 |
+
gr.Markdown("#### 🔵 No Moderation")
|
| 416 |
+
chatbot_no_mod = gr.Chatbot(height=650, label="No Moderation", show_label=False, bubble_full_width=False, type='messages')
|
| 417 |
+
with gr.Column(scale=1):
|
| 418 |
+
gr.Markdown("#### 🟠 OpenAI Moderation")
|
| 419 |
+
chatbot_openai = gr.Chatbot(height=650, label="OpenAI Moderation", show_label=False, bubble_full_width=False, type='messages')
|
| 420 |
+
with gr.Column(scale=1):
|
| 421 |
+
gr.Markdown("#### 🛡️ LionGuard 2")
|
| 422 |
+
chatbot_lg = gr.Chatbot(height=650, label="LionGuard 2", show_label=False, bubble_full_width=False, type='messages')
|
| 423 |
+
gr.Markdown("##### 💬 Send Message to All Models")
|
| 424 |
+
with gr.Row():
|
| 425 |
+
message_input = gr.Textbox(
|
| 426 |
+
placeholder="Type your message to compare responses...",
|
| 427 |
+
show_label=False,
|
| 428 |
+
scale=4
|
| 429 |
+
)
|
| 430 |
+
send_btn = gr.Button("Send", variant="primary", scale=1)
|
| 431 |
+
with gr.Row():
|
| 432 |
+
clear_btn = gr.Button("Clear All Chats", variant="stop")
|
| 433 |
+
|
| 434 |
+
send_btn.click(
|
| 435 |
+
process_message,
|
| 436 |
+
inputs=[message_input, chatbot_no_mod, chatbot_openai, chatbot_lg],
|
| 437 |
+
outputs=[chatbot_no_mod, chatbot_openai, chatbot_lg, message_input]
|
| 438 |
+
)
|
| 439 |
+
message_input.submit(
|
| 440 |
+
process_message,
|
| 441 |
+
inputs=[message_input, chatbot_no_mod, chatbot_openai, chatbot_lg],
|
| 442 |
+
outputs=[chatbot_no_mod, chatbot_openai, chatbot_lg, message_input]
|
| 443 |
+
)
|
| 444 |
+
clear_btn.click(
|
| 445 |
+
clear_all_chats,
|
| 446 |
+
outputs=[chatbot_no_mod, chatbot_openai, chatbot_lg]
|
| 447 |
+
)
|
| 448 |
+
|
| 449 |
+
if __name__ == "__main__":
|
| 450 |
+
demo.launch()
|
app/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
app/backend/__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (3.63 kB). View file
|
|
|
app/backend/__pycache__/services.cpython-313.pyc
ADDED
|
Binary file (16.4 kB). View file
|
|
|
app/frontend/index.html
CHANGED
|
@@ -3,22 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<
|
| 7 |
-
<meta name="theme-color" content="#E14746">
|
| 8 |
-
<title>LionGuard - Content Moderation</title>
|
| 9 |
-
<link rel="icon" type="image/png" href="/static/logo.png">
|
| 10 |
-
|
| 11 |
-
<!-- Fonts -->
|
| 12 |
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 13 |
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 14 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@500;600;700&display=swap" rel="stylesheet">
|
| 15 |
-
|
| 16 |
-
<!-- Icons -->
|
| 17 |
-
<link href='https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css' rel='stylesheet'>
|
| 18 |
-
|
| 19 |
-
<!-- Syntax Highlighting -->
|
| 20 |
-
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
|
| 21 |
-
|
| 22 |
<link rel="stylesheet" href="/static/style.css">
|
| 23 |
</head>
|
| 24 |
<body>
|
|
@@ -27,46 +12,42 @@
|
|
| 27 |
<div class="container">
|
| 28 |
<div class="header-content">
|
| 29 |
<div class="logo-section">
|
| 30 |
-
<img src="/static/logo.png" alt="
|
| 31 |
<div class="logo-text">
|
| 32 |
-
<h1>
|
| 33 |
<p>A content moderation tool designed for Singapore</p>
|
| 34 |
</div>
|
| 35 |
</div>
|
| 36 |
<div class="header-controls">
|
| 37 |
<nav class="tabs" aria-label="Primary navigation">
|
| 38 |
-
<button class="tab nav-link" data-tab="get-started">
|
| 39 |
-
<i class='bx bx-rocket tab-icon'></i>
|
| 40 |
-
Get Started
|
| 41 |
-
</button>
|
| 42 |
<div class="nav-dropdown">
|
| 43 |
<button
|
| 44 |
class="tab dropdown-toggle"
|
| 45 |
aria-haspopup="true"
|
| 46 |
aria-expanded="false"
|
| 47 |
>
|
| 48 |
-
<
|
| 49 |
Demos
|
| 50 |
-
<
|
| 51 |
</button>
|
| 52 |
<div class="dropdown-menu" role="menu">
|
| 53 |
<button class="tab dropdown-item active" data-tab="detector" role="menuitem">
|
| 54 |
-
<
|
| 55 |
Detector
|
| 56 |
</button>
|
| 57 |
<button class="tab dropdown-item" data-tab="chat" role="menuitem">
|
| 58 |
-
<
|
| 59 |
Chatbot Guardrail
|
| 60 |
</button>
|
| 61 |
</div>
|
| 62 |
</div>
|
| 63 |
<button class="tab nav-link" data-tab="about">
|
| 64 |
-
<
|
| 65 |
About
|
| 66 |
</button>
|
| 67 |
</nav>
|
| 68 |
<button id="theme-toggle" class="theme-icon-button" aria-label="Toggle theme">
|
| 69 |
-
<
|
| 70 |
</button>
|
| 71 |
</div>
|
| 72 |
</div>
|
|
@@ -79,7 +60,7 @@
|
|
| 79 |
<div id="detector-content" class="tab-content active">
|
| 80 |
<!-- Disclaimer -->
|
| 81 |
<div class="warning-card">
|
| 82 |
-
|
| 83 |
</div>
|
| 84 |
|
| 85 |
<!-- Model Selector -->
|
|
@@ -89,9 +70,9 @@
|
|
| 89 |
</div>
|
| 90 |
<div class="model-dropdown">
|
| 91 |
<select id="model-select" class="model-select" aria-label="Detector guardrail model">
|
| 92 |
-
<option value="lionguard-2.1" selected>
|
| 93 |
-
<option value="lionguard-2">
|
| 94 |
-
<option value="lionguard-2-lite">
|
| 95 |
</select>
|
| 96 |
</div>
|
| 97 |
</div>
|
|
@@ -107,7 +88,7 @@
|
|
| 107 |
rows="10"
|
| 108 |
></textarea>
|
| 109 |
<button id="analyze-btn" class="btn btn-primary">
|
| 110 |
-
<
|
| 111 |
Analyze
|
| 112 |
</button>
|
| 113 |
</div>
|
|
@@ -118,7 +99,7 @@
|
|
| 118 |
|
| 119 |
<!-- Binary Score -->
|
| 120 |
<div id="binary-result" class="binary-placeholder">
|
| 121 |
-
<
|
| 122 |
<p>Enter text to analyze</p>
|
| 123 |
</div>
|
| 124 |
|
|
@@ -132,11 +113,11 @@
|
|
| 132 |
<p class="feedback-prompt">Does this look correct?</p>
|
| 133 |
<div class="feedback-buttons">
|
| 134 |
<button id="thumbs-up" class="btn btn-success">
|
| 135 |
-
<
|
| 136 |
Yes
|
| 137 |
</button>
|
| 138 |
<button id="thumbs-down" class="btn btn-secondary">
|
| 139 |
-
<
|
| 140 |
No
|
| 141 |
</button>
|
| 142 |
</div>
|
|
@@ -150,7 +131,7 @@
|
|
| 150 |
<div id="chat-content" class="tab-content full-width-section">
|
| 151 |
<!-- Disclaimer -->
|
| 152 |
<div class="warning-card">
|
| 153 |
-
|
| 154 |
</div>
|
| 155 |
|
| 156 |
<!-- Model Selector for Guardrail -->
|
|
@@ -160,9 +141,9 @@
|
|
| 160 |
</div>
|
| 161 |
<div class="model-dropdown">
|
| 162 |
<select id="model-select-gc" class="model-select" aria-label="Chat guardrail model">
|
| 163 |
-
<option value="lionguard-2.1" selected>
|
| 164 |
-
<option value="lionguard-2">
|
| 165 |
-
<option value="lionguard-2-lite">
|
| 166 |
</select>
|
| 167 |
</div>
|
| 168 |
</div>
|
|
@@ -172,7 +153,7 @@
|
|
| 172 |
<!-- No Moderation -->
|
| 173 |
<div class="chat-panel">
|
| 174 |
<div class="chat-header">
|
| 175 |
-
<
|
| 176 |
<h4>No Moderation</h4>
|
| 177 |
</div>
|
| 178 |
<div id="chat-no-mod" class="chat-messages"></div>
|
|
@@ -181,17 +162,17 @@
|
|
| 181 |
<!-- OpenAI Moderation -->
|
| 182 |
<div class="chat-panel">
|
| 183 |
<div class="chat-header">
|
| 184 |
-
<
|
| 185 |
<h4>OpenAI Moderation</h4>
|
| 186 |
</div>
|
| 187 |
<div id="chat-openai" class="chat-messages"></div>
|
| 188 |
</div>
|
| 189 |
|
| 190 |
-
<!--
|
| 191 |
<div class="chat-panel">
|
| 192 |
<div class="chat-header">
|
| 193 |
-
<
|
| 194 |
-
<h4>
|
| 195 |
</div>
|
| 196 |
<div id="chat-lionguard" class="chat-messages"></div>
|
| 197 |
</div>
|
|
@@ -215,7 +196,7 @@
|
|
| 215 |
<div id="about-content" class="tab-content">
|
| 216 |
<!-- Hero Section -->
|
| 217 |
<section class="about-intro-section">
|
| 218 |
-
<p class="lead">
|
| 219 |
<p class="lead" style="font-style: italic;">Developed by <a href="https://www.tech.gov.sg/" target="_blank" style="color: var(--primary-red); text-decoration: none; font-weight: 600;">GovTech Singapore</a>.</p>
|
| 220 |
</section>
|
| 221 |
|
|
@@ -223,18 +204,18 @@
|
|
| 223 |
<section class="about-resources-grid">
|
| 224 |
<!-- Models -->
|
| 225 |
<div class="resource-card">
|
| 226 |
-
<h3
|
| 227 |
<div class="resource-list">
|
| 228 |
-
<a href="https://huggingface.co/govtech/lionguard-2.1" target="_blank">
|
| 229 |
-
<a href="https://huggingface.co/govtech/lionguard-2" target="_blank">
|
| 230 |
-
<a href="https://huggingface.co/govtech/lionguard-2-lite" target="_blank">
|
| 231 |
-
<a href="https://huggingface.co/govtech/lionguard-v1" target="_blank">
|
| 232 |
</div>
|
| 233 |
</div>
|
| 234 |
|
| 235 |
<!-- Datasets -->
|
| 236 |
<div class="resource-card">
|
| 237 |
-
<h3
|
| 238 |
<div class="resource-list">
|
| 239 |
<a href="https://huggingface.co/datasets/govtech/lionguard-2-synthetic-instruct" target="_blank">Subset of Training Data</a>
|
| 240 |
<a href="https://huggingface.co/datasets/govtech/RabakBench" target="_blank">RabakBench</a>
|
|
@@ -243,158 +224,33 @@
|
|
| 243 |
|
| 244 |
<!-- Blog Posts -->
|
| 245 |
<div class="resource-card">
|
| 246 |
-
<h3
|
| 247 |
<div class="resource-list">
|
| 248 |
-
<a href="https://medium.com/dsaid-govtech/lionguard-2-8066d4e20d16" target="_blank">
|
| 249 |
-
<a href="https://medium.com/dsaid-govtech/building-lionguard-a-contextualised-moderation-classifier-to-tackle-local-unsafe-content-8f68c8f13179" target="_blank">
|
| 250 |
</div>
|
| 251 |
</div>
|
| 252 |
|
| 253 |
<!-- Papers -->
|
| 254 |
<div class="resource-card">
|
| 255 |
-
<h3
|
| 256 |
<div class="resource-list">
|
| 257 |
-
<a href="https://arxiv.org/abs/2507.05980" target="_blank">
|
| 258 |
<a href="https://arxiv.org/abs/2507.15339" target="_blank">RabakBench (arXiv:2507.15339)</a>
|
| 259 |
-
<a href="https://arxiv.org/abs/2407.10995" target="_blank">
|
| 260 |
</div>
|
| 261 |
</div>
|
| 262 |
</section>
|
| 263 |
</div>
|
| 264 |
-
|
| 265 |
-
<!-- Get Started Tab Content -->
|
| 266 |
-
<div id="get-started-content" class="tab-content">
|
| 267 |
-
<div class="get-started-container">
|
| 268 |
-
<!-- Option 1 -->
|
| 269 |
-
<div class="option-card full-width-card">
|
| 270 |
-
<div class="option-header">
|
| 271 |
-
<i class='bx bx-server option-icon'></i>
|
| 272 |
-
<div>
|
| 273 |
-
<h2>Option 1: Self-Host the Classifier</h2>
|
| 274 |
-
<p>Integrate LionGuard directly into your applications. Toggle between models to see the implementation details.</p>
|
| 275 |
-
</div>
|
| 276 |
-
</div>
|
| 277 |
-
|
| 278 |
-
<div class="code-section">
|
| 279 |
-
<div class="code-tabs-header">
|
| 280 |
-
<div class="code-tabs">
|
| 281 |
-
<button class="code-tab active" data-code="lg2.1">LionGuard 2.1</button>
|
| 282 |
-
<button class="code-tab" data-code="lg2">LionGuard 2</button>
|
| 283 |
-
<button class="code-tab" data-code="lg2-lite">LionGuard 2 Lite</button>
|
| 284 |
-
</div>
|
| 285 |
-
<button id="copy-code-btn" class="copy-btn" title="Copy to clipboard">
|
| 286 |
-
<i class='bx bx-copy'></i> Copy
|
| 287 |
-
</button>
|
| 288 |
-
</div>
|
| 289 |
-
|
| 290 |
-
<div class="code-display">
|
| 291 |
-
<!-- LionGuard 2.1 Snippet -->
|
| 292 |
-
<div id="code-lg2.1" class="code-block active">
|
| 293 |
-
<pre><code class="language-python">import os
|
| 294 |
-
import numpy as np
|
| 295 |
-
from transformers import AutoModel
|
| 296 |
-
from google import genai
|
| 297 |
-
|
| 298 |
-
# Load model directly from HF
|
| 299 |
-
model = AutoModel.from_pretrained("govtech/lionguard-2.1", trust_remote_code=True)
|
| 300 |
-
|
| 301 |
-
# Text to classify
|
| 302 |
-
texts = ["hello", "world"]
|
| 303 |
-
|
| 304 |
-
# Get embeddings (users to input their own Gemini API key)
|
| 305 |
-
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
|
| 306 |
-
response = client.models.embed_content(
|
| 307 |
-
model="gemini-embedding-001",
|
| 308 |
-
contents=texts
|
| 309 |
-
)
|
| 310 |
-
embeddings = np.array([emb.values for emb in response.embeddings])
|
| 311 |
-
|
| 312 |
-
# Run inference
|
| 313 |
-
results = model.predict(embeddings)</code></pre>
|
| 314 |
-
</div>
|
| 315 |
-
|
| 316 |
-
<!-- LionGuard 2 Snippet -->
|
| 317 |
-
<div id="code-lg2" class="code-block">
|
| 318 |
-
<pre><code class="language-python">import os
|
| 319 |
-
import numpy as np
|
| 320 |
-
from transformers import AutoModel
|
| 321 |
-
from openai import OpenAI
|
| 322 |
-
|
| 323 |
-
# Load model directly from HF
|
| 324 |
-
model = AutoModel.from_pretrained(
|
| 325 |
-
"govtech/lionguard-2",
|
| 326 |
-
trust_remote_code=True
|
| 327 |
-
)
|
| 328 |
-
|
| 329 |
-
# Get OpenAI embeddings (users to input their own OpenAI API key)
|
| 330 |
-
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 331 |
-
response = client.embeddings.create(
|
| 332 |
-
input="Hello, world!", # users to input their own text
|
| 333 |
-
model="text-embedding-3-large",
|
| 334 |
-
dimensions=3072 # dimensions of the embedding
|
| 335 |
-
)
|
| 336 |
-
embeddings = np.array([data.embedding for data in response.data])
|
| 337 |
-
|
| 338 |
-
# Run LionGuard 2
|
| 339 |
-
results = model.predict(embeddings)</code></pre>
|
| 340 |
-
</div>
|
| 341 |
-
|
| 342 |
-
<!-- LionGuard 2 Lite Snippet -->
|
| 343 |
-
<div id="code-lg2-lite" class="code-block">
|
| 344 |
-
<pre><code class="language-python">import numpy as np
|
| 345 |
-
from sentence_transformers import SentenceTransformer
|
| 346 |
-
from transformers import AutoModel
|
| 347 |
-
|
| 348 |
-
# Load model directly from Hub
|
| 349 |
-
model = AutoModel.from_pretrained("govtech/lionguard-2-lite", trust_remote_code=True)
|
| 350 |
-
|
| 351 |
-
# Download model from the 🤗 Hub
|
| 352 |
-
embedding_model = SentenceTransformer("google/embeddinggemma-300m")
|
| 353 |
-
|
| 354 |
-
# Text to classify
|
| 355 |
-
texts = ["hello", "world"]
|
| 356 |
-
|
| 357 |
-
# Add prompt instructions to generate embeddings that are optimized to classify texts according to preset labels
|
| 358 |
-
formatted_texts = [f"task: classification | query: {c}" for c in texts]
|
| 359 |
-
embeddings = embedding_model.encode(formatted_texts) # NOTE: use encode() instead of encode_documents()
|
| 360 |
-
|
| 361 |
-
# Run inference
|
| 362 |
-
results = model.predict(embeddings)</code></pre>
|
| 363 |
-
</div>
|
| 364 |
-
</div>
|
| 365 |
-
</div>
|
| 366 |
-
</div>
|
| 367 |
-
|
| 368 |
-
<!-- Option 2 -->
|
| 369 |
-
<div class="option-card full-width-card">
|
| 370 |
-
<div class="option-header">
|
| 371 |
-
<i class='bx bx-shield-quarter option-icon'></i>
|
| 372 |
-
<div>
|
| 373 |
-
<h2>Option 2: via AI Guardians - Sentinel</h2>
|
| 374 |
-
<p>Managed service for Singapore public sector agencies.</p>
|
| 375 |
-
</div>
|
| 376 |
-
</div>
|
| 377 |
-
<div class="option-action">
|
| 378 |
-
<a href="https://www.aiguardian.gov.sg/" target="_blank" class="btn btn-primary">
|
| 379 |
-
Visit AI Guardians
|
| 380 |
-
</a>
|
| 381 |
-
</div>
|
| 382 |
-
</div>
|
| 383 |
-
</div>
|
| 384 |
-
</div>
|
| 385 |
</main>
|
| 386 |
|
| 387 |
<!-- Footer -->
|
| 388 |
<footer class="footer">
|
| 389 |
<div class="container">
|
| 390 |
-
<p>
|
| 391 |
</div>
|
| 392 |
</footer>
|
| 393 |
|
| 394 |
<script src="/static/script.js"></script>
|
| 395 |
-
|
| 396 |
-
<!-- Syntax Highlighting -->
|
| 397 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
| 398 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
| 399 |
</body>
|
| 400 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Lionguard</title>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
<link rel="stylesheet" href="/static/style.css">
|
| 8 |
</head>
|
| 9 |
<body>
|
|
|
|
| 12 |
<div class="container">
|
| 13 |
<div class="header-content">
|
| 14 |
<div class="logo-section">
|
| 15 |
+
<img src="/static/logo.png" alt="Lionguard Logo" class="logo">
|
| 16 |
<div class="logo-text">
|
| 17 |
+
<h1>Lionguard</h1>
|
| 18 |
<p>A content moderation tool designed for Singapore</p>
|
| 19 |
</div>
|
| 20 |
</div>
|
| 21 |
<div class="header-controls">
|
| 22 |
<nav class="tabs" aria-label="Primary navigation">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
<div class="nav-dropdown">
|
| 24 |
<button
|
| 25 |
class="tab dropdown-toggle"
|
| 26 |
aria-haspopup="true"
|
| 27 |
aria-expanded="false"
|
| 28 |
>
|
| 29 |
+
<span class="tab-icon">🛠️</span>
|
| 30 |
Demos
|
| 31 |
+
<span class="dropdown-caret">▾</span>
|
| 32 |
</button>
|
| 33 |
<div class="dropdown-menu" role="menu">
|
| 34 |
<button class="tab dropdown-item active" data-tab="detector" role="menuitem">
|
| 35 |
+
<span class="tab-icon">🔍</span>
|
| 36 |
Detector
|
| 37 |
</button>
|
| 38 |
<button class="tab dropdown-item" data-tab="chat" role="menuitem">
|
| 39 |
+
<span class="tab-icon">💬</span>
|
| 40 |
Chatbot Guardrail
|
| 41 |
</button>
|
| 42 |
</div>
|
| 43 |
</div>
|
| 44 |
<button class="tab nav-link" data-tab="about">
|
| 45 |
+
<span class="tab-icon">ℹ️</span>
|
| 46 |
About
|
| 47 |
</button>
|
| 48 |
</nav>
|
| 49 |
<button id="theme-toggle" class="theme-icon-button" aria-label="Toggle theme">
|
| 50 |
+
<span class="theme-icon" aria-hidden="true">🌞</span>
|
| 51 |
</button>
|
| 52 |
</div>
|
| 53 |
</div>
|
|
|
|
| 60 |
<div id="detector-content" class="tab-content active">
|
| 61 |
<!-- Disclaimer -->
|
| 62 |
<div class="warning-card">
|
| 63 |
+
⚠️ Inputs are anonymised and logged to improve Lionguard's moderation models.
|
| 64 |
</div>
|
| 65 |
|
| 66 |
<!-- Model Selector -->
|
|
|
|
| 70 |
</div>
|
| 71 |
<div class="model-dropdown">
|
| 72 |
<select id="model-select" class="model-select" aria-label="Detector guardrail model">
|
| 73 |
+
<option value="lionguard-2.1" selected>Lionguard 2.1 (Gemini Embeddings, API)</option>
|
| 74 |
+
<option value="lionguard-2">Lionguard 2 (OpenAI Embeddings, API)</option>
|
| 75 |
+
<option value="lionguard-2-lite">Lionguard 2 Lite (Gemma Embeddings, Local)</option>
|
| 76 |
</select>
|
| 77 |
</div>
|
| 78 |
</div>
|
|
|
|
| 88 |
rows="10"
|
| 89 |
></textarea>
|
| 90 |
<button id="analyze-btn" class="btn btn-primary">
|
| 91 |
+
<span class="btn-icon">🔍</span>
|
| 92 |
Analyze
|
| 93 |
</button>
|
| 94 |
</div>
|
|
|
|
| 99 |
|
| 100 |
<!-- Binary Score -->
|
| 101 |
<div id="binary-result" class="binary-placeholder">
|
| 102 |
+
<div class="placeholder-icon">📝</div>
|
| 103 |
<p>Enter text to analyze</p>
|
| 104 |
</div>
|
| 105 |
|
|
|
|
| 113 |
<p class="feedback-prompt">Does this look correct?</p>
|
| 114 |
<div class="feedback-buttons">
|
| 115 |
<button id="thumbs-up" class="btn btn-success">
|
| 116 |
+
<span>👍</span>
|
| 117 |
Yes
|
| 118 |
</button>
|
| 119 |
<button id="thumbs-down" class="btn btn-secondary">
|
| 120 |
+
<span>👎</span>
|
| 121 |
No
|
| 122 |
</button>
|
| 123 |
</div>
|
|
|
|
| 131 |
<div id="chat-content" class="tab-content full-width-section">
|
| 132 |
<!-- Disclaimer -->
|
| 133 |
<div class="warning-card">
|
| 134 |
+
⚠️ Inputs are anonymised and logged to improve Lionguard's moderation models.
|
| 135 |
</div>
|
| 136 |
|
| 137 |
<!-- Model Selector for Guardrail -->
|
|
|
|
| 141 |
</div>
|
| 142 |
<div class="model-dropdown">
|
| 143 |
<select id="model-select-gc" class="model-select" aria-label="Chat guardrail model">
|
| 144 |
+
<option value="lionguard-2.1" selected>Lionguard 2.1 (Gemini Embeddings, API)</option>
|
| 145 |
+
<option value="lionguard-2">Lionguard 2 (OpenAI Embeddings, API)</option>
|
| 146 |
+
<option value="lionguard-2-lite">Lionguard 2 Lite (Gemma Embeddings, Local)</option>
|
| 147 |
</select>
|
| 148 |
</div>
|
| 149 |
</div>
|
|
|
|
| 153 |
<!-- No Moderation -->
|
| 154 |
<div class="chat-panel">
|
| 155 |
<div class="chat-header">
|
| 156 |
+
<span class="chat-icon">🔵</span>
|
| 157 |
<h4>No Moderation</h4>
|
| 158 |
</div>
|
| 159 |
<div id="chat-no-mod" class="chat-messages"></div>
|
|
|
|
| 162 |
<!-- OpenAI Moderation -->
|
| 163 |
<div class="chat-panel">
|
| 164 |
<div class="chat-header">
|
| 165 |
+
<span class="chat-icon">🟠</span>
|
| 166 |
<h4>OpenAI Moderation</h4>
|
| 167 |
</div>
|
| 168 |
<div id="chat-openai" class="chat-messages"></div>
|
| 169 |
</div>
|
| 170 |
|
| 171 |
+
<!-- Lionguard -->
|
| 172 |
<div class="chat-panel">
|
| 173 |
<div class="chat-header">
|
| 174 |
+
<span class="chat-icon">🛡️</span>
|
| 175 |
+
<h4>Lionguard</h4>
|
| 176 |
</div>
|
| 177 |
<div id="chat-lionguard" class="chat-messages"></div>
|
| 178 |
</div>
|
|
|
|
| 196 |
<div id="about-content" class="tab-content">
|
| 197 |
<!-- Hero Section -->
|
| 198 |
<section class="about-intro-section">
|
| 199 |
+
<p class="lead">Lionguard is a family of open-source content moderation models specifically designed for Singapore's multilingual environment. Optimized for Singapore’s linguistic mix, including Singlish, Mandarin, Malay, and Tamil, Lionguard delivers accurate moderation grounded in local usage and cultural nuance.</p>
|
| 200 |
<p class="lead" style="font-style: italic;">Developed by <a href="https://www.tech.gov.sg/" target="_blank" style="color: var(--primary-red); text-decoration: none; font-weight: 600;">GovTech Singapore</a>.</p>
|
| 201 |
</section>
|
| 202 |
|
|
|
|
| 204 |
<section class="about-resources-grid">
|
| 205 |
<!-- Models -->
|
| 206 |
<div class="resource-card">
|
| 207 |
+
<h3>🤗 Open-Sourced Models</h3>
|
| 208 |
<div class="resource-list">
|
| 209 |
+
<a href="https://huggingface.co/govtech/lionguard-2.1" target="_blank">Lionguard 2.1</a>
|
| 210 |
+
<a href="https://huggingface.co/govtech/lionguard-2" target="_blank">Lionguard 2</a>
|
| 211 |
+
<a href="https://huggingface.co/govtech/lionguard-2-lite" target="_blank">Lionguard 2 Lite</a>
|
| 212 |
+
<a href="https://huggingface.co/govtech/lionguard-v1" target="_blank">Lionguard 1</a>
|
| 213 |
</div>
|
| 214 |
</div>
|
| 215 |
|
| 216 |
<!-- Datasets -->
|
| 217 |
<div class="resource-card">
|
| 218 |
+
<h3>📊 Open-Sourced Datasets</h3>
|
| 219 |
<div class="resource-list">
|
| 220 |
<a href="https://huggingface.co/datasets/govtech/lionguard-2-synthetic-instruct" target="_blank">Subset of Training Data</a>
|
| 221 |
<a href="https://huggingface.co/datasets/govtech/RabakBench" target="_blank">RabakBench</a>
|
|
|
|
| 224 |
|
| 225 |
<!-- Blog Posts -->
|
| 226 |
<div class="resource-card">
|
| 227 |
+
<h3>📝 Blog Posts</h3>
|
| 228 |
<div class="resource-list">
|
| 229 |
+
<a href="https://medium.com/dsaid-govtech/lionguard-2-8066d4e20d16" target="_blank">Lionguard 2</a>
|
| 230 |
+
<a href="https://medium.com/dsaid-govtech/building-lionguard-a-contextualised-moderation-classifier-to-tackle-local-unsafe-content-8f68c8f13179" target="_blank">Lionguard</a>
|
| 231 |
</div>
|
| 232 |
</div>
|
| 233 |
|
| 234 |
<!-- Papers -->
|
| 235 |
<div class="resource-card">
|
| 236 |
+
<h3>📄 Research Papers</h3>
|
| 237 |
<div class="resource-list">
|
| 238 |
+
<a href="https://arxiv.org/abs/2507.05980" target="_blank">Lionguard 2 (arXiv:2507.05980)</a>
|
| 239 |
<a href="https://arxiv.org/abs/2507.15339" target="_blank">RabakBench (arXiv:2507.15339)</a>
|
| 240 |
+
<a href="https://arxiv.org/abs/2407.10995" target="_blank">Lionguard 1 (arXiv:2407.10995)</a>
|
| 241 |
</div>
|
| 242 |
</div>
|
| 243 |
</section>
|
| 244 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
</main>
|
| 246 |
|
| 247 |
<!-- Footer -->
|
| 248 |
<footer class="footer">
|
| 249 |
<div class="container">
|
| 250 |
+
<p>Lionguard · Powered by <a href="https://www.tech.gov.sg/" target="_blank">GovTech</a></p>
|
| 251 |
</div>
|
| 252 |
</footer>
|
| 253 |
|
| 254 |
<script src="/static/script.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
</body>
|
| 256 |
</html>
|
app/frontend/logo.png
DELETED
Git LFS Details
|
app/frontend/script.js
CHANGED
|
@@ -29,12 +29,12 @@ function hideLoading(button, originalText) {
|
|
| 29 |
|
| 30 |
function getScoreLevel(score) {
|
| 31 |
if (score < 0.4) {
|
| 32 |
-
return { className: 'good', icon: '
|
| 33 |
}
|
| 34 |
if (score < 0.7) {
|
| 35 |
-
return { className: 'warn', icon: '
|
| 36 |
}
|
| 37 |
-
return { className: 'bad', icon: '
|
| 38 |
}
|
| 39 |
|
| 40 |
function formatScore(score) {
|
|
@@ -197,9 +197,9 @@ async function analyzeText() {
|
|
| 197 |
const verdictClass = data.binary_verdict;
|
| 198 |
const verdictText = verdictClass.charAt(0).toUpperCase() + verdictClass.slice(1);
|
| 199 |
const verdictIcons = {
|
| 200 |
-
'pass': '
|
| 201 |
-
'warn': '
|
| 202 |
-
'fail': '
|
| 203 |
};
|
| 204 |
|
| 205 |
binaryResult.innerHTML = `
|
|
@@ -403,9 +403,7 @@ function initThemeToggle() {
|
|
| 403 |
const updateIcon = (isDark) => {
|
| 404 |
themeToggle.setAttribute('aria-pressed', isDark ? 'true' : 'false');
|
| 405 |
if (themeIcon) {
|
| 406 |
-
|
| 407 |
-
themeIcon.className = isDark ? 'bx bx-moon theme-icon' : 'bx bx-sun theme-icon';
|
| 408 |
-
themeIcon.textContent = ''; // clear text content
|
| 409 |
}
|
| 410 |
};
|
| 411 |
|
|
@@ -424,68 +422,6 @@ function initThemeToggle() {
|
|
| 424 |
});
|
| 425 |
}
|
| 426 |
|
| 427 |
-
// Code snippet tabs for Get Started
|
| 428 |
-
function initCodeTabs() {
|
| 429 |
-
const tabs = document.querySelectorAll('.code-tab');
|
| 430 |
-
const blocks = document.querySelectorAll('.code-block');
|
| 431 |
-
|
| 432 |
-
if (!tabs.length) return;
|
| 433 |
-
|
| 434 |
-
tabs.forEach(tab => {
|
| 435 |
-
tab.addEventListener('click', () => {
|
| 436 |
-
// Remove active class from all tabs
|
| 437 |
-
tabs.forEach(t => t.classList.remove('active'));
|
| 438 |
-
// Add active class to clicked tab
|
| 439 |
-
tab.classList.add('active');
|
| 440 |
-
|
| 441 |
-
// Hide all blocks
|
| 442 |
-
blocks.forEach(b => b.classList.remove('active'));
|
| 443 |
-
|
| 444 |
-
// Show target block
|
| 445 |
-
const targetId = `code-${tab.dataset.code}`;
|
| 446 |
-
const targetBlock = document.getElementById(targetId);
|
| 447 |
-
if (targetBlock) {
|
| 448 |
-
targetBlock.classList.add('active');
|
| 449 |
-
}
|
| 450 |
-
});
|
| 451 |
-
});
|
| 452 |
-
}
|
| 453 |
-
|
| 454 |
-
// Copy Code Functionality
|
| 455 |
-
function initCopyButton() {
|
| 456 |
-
const copyBtn = document.getElementById('copy-code-btn');
|
| 457 |
-
if (!copyBtn) return;
|
| 458 |
-
|
| 459 |
-
copyBtn.addEventListener('click', async () => {
|
| 460 |
-
// Find active code block
|
| 461 |
-
const activeBlock = document.querySelector('.code-block.active code');
|
| 462 |
-
if (!activeBlock) return;
|
| 463 |
-
|
| 464 |
-
const textToCopy = activeBlock.textContent;
|
| 465 |
-
|
| 466 |
-
try {
|
| 467 |
-
await navigator.clipboard.writeText(textToCopy);
|
| 468 |
-
|
| 469 |
-
// Provide feedback
|
| 470 |
-
const originalHtml = copyBtn.innerHTML;
|
| 471 |
-
copyBtn.innerHTML = `<i class='bx bx-check'></i> Copied!`;
|
| 472 |
-
copyBtn.classList.add('success');
|
| 473 |
-
|
| 474 |
-
setTimeout(() => {
|
| 475 |
-
copyBtn.innerHTML = originalHtml;
|
| 476 |
-
copyBtn.classList.remove('success');
|
| 477 |
-
}, 2000);
|
| 478 |
-
} catch (err) {
|
| 479 |
-
console.error('Failed to copy:', err);
|
| 480 |
-
copyBtn.innerHTML = `<i class='bx bx-x'></i> Failed`;
|
| 481 |
-
|
| 482 |
-
setTimeout(() => {
|
| 483 |
-
copyBtn.innerHTML = `<i class='bx bx-copy'></i> Copy`;
|
| 484 |
-
}, 2000);
|
| 485 |
-
}
|
| 486 |
-
});
|
| 487 |
-
}
|
| 488 |
-
|
| 489 |
// Initialize app
|
| 490 |
document.addEventListener('DOMContentLoaded', () => {
|
| 491 |
initTabs();
|
|
@@ -494,8 +430,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 494 |
initModelSelectorGC();
|
| 495 |
initEventListeners();
|
| 496 |
initThemeToggle();
|
| 497 |
-
initCodeTabs();
|
| 498 |
-
initCopyButton();
|
| 499 |
|
| 500 |
console.log('LionGuard 2 app initialized');
|
| 501 |
});
|
|
|
|
| 29 |
|
| 30 |
function getScoreLevel(score) {
|
| 31 |
if (score < 0.4) {
|
| 32 |
+
return { className: 'good', icon: '👌', title: 'Low risk' };
|
| 33 |
}
|
| 34 |
if (score < 0.7) {
|
| 35 |
+
return { className: 'warn', icon: '⚠️', title: 'Needs review' };
|
| 36 |
}
|
| 37 |
+
return { className: 'bad', icon: '🚨', title: 'High risk' };
|
| 38 |
}
|
| 39 |
|
| 40 |
function formatScore(score) {
|
|
|
|
| 197 |
const verdictClass = data.binary_verdict;
|
| 198 |
const verdictText = verdictClass.charAt(0).toUpperCase() + verdictClass.slice(1);
|
| 199 |
const verdictIcons = {
|
| 200 |
+
'pass': '✅',
|
| 201 |
+
'warn': '⚠️',
|
| 202 |
+
'fail': '🚨'
|
| 203 |
};
|
| 204 |
|
| 205 |
binaryResult.innerHTML = `
|
|
|
|
| 403 |
const updateIcon = (isDark) => {
|
| 404 |
themeToggle.setAttribute('aria-pressed', isDark ? 'true' : 'false');
|
| 405 |
if (themeIcon) {
|
| 406 |
+
themeIcon.textContent = isDark ? '🌙' : '🌞';
|
|
|
|
|
|
|
| 407 |
}
|
| 408 |
};
|
| 409 |
|
|
|
|
| 422 |
});
|
| 423 |
}
|
| 424 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
// Initialize app
|
| 426 |
document.addEventListener('DOMContentLoaded', () => {
|
| 427 |
initTabs();
|
|
|
|
| 430 |
initModelSelectorGC();
|
| 431 |
initEventListeners();
|
| 432 |
initThemeToggle();
|
|
|
|
|
|
|
| 433 |
|
| 434 |
console.log('LionGuard 2 app initialized');
|
| 435 |
});
|
app/frontend/style.css
CHANGED
|
@@ -1,10 +1,5 @@
|
|
| 1 |
/* LionGuard 2 - Warm, Inviting, and Bright Design */
|
| 2 |
-
|
| 3 |
-
/* Icons alignment */
|
| 4 |
-
.bx {
|
| 5 |
-
vertical-align: middle;
|
| 6 |
-
line-height: 1;
|
| 7 |
-
}
|
| 8 |
|
| 9 |
:root {
|
| 10 |
/* Modern color palette inspired by logo */
|
|
@@ -110,7 +105,7 @@ main {
|
|
| 110 |
margin-left: auto;
|
| 111 |
display: flex;
|
| 112 |
align-items: center;
|
| 113 |
-
gap:
|
| 114 |
flex-wrap: wrap;
|
| 115 |
justify-content: flex-end;
|
| 116 |
background: transparent;
|
|
@@ -135,10 +130,8 @@ main {
|
|
| 135 |
|
| 136 |
.logo-text h1 {
|
| 137 |
font-family: 'Poppins', sans-serif;
|
| 138 |
-
font-size: 1.
|
| 139 |
-
|
| 140 |
-
color: var(--primary-dark);
|
| 141 |
-
letter-spacing: -0.02em;
|
| 142 |
margin-bottom: 0px;
|
| 143 |
line-height: 1.2;
|
| 144 |
}
|
|
@@ -328,7 +321,7 @@ body.dark-mode .toggle-icon-moon {
|
|
| 328 |
display: inline-flex;
|
| 329 |
align-items: center;
|
| 330 |
flex-wrap: wrap;
|
| 331 |
-
gap:
|
| 332 |
margin-bottom: 0;
|
| 333 |
background: transparent;
|
| 334 |
border: none;
|
|
@@ -380,12 +373,11 @@ body.dark-mode .toggle-icon-moon {
|
|
| 380 |
}
|
| 381 |
|
| 382 |
.tab-icon {
|
| 383 |
-
font-size: 1.
|
| 384 |
-
margin-bottom: 1px;
|
| 385 |
}
|
| 386 |
|
| 387 |
.dropdown-toggle {
|
| 388 |
-
|
| 389 |
}
|
| 390 |
|
| 391 |
.dropdown-caret {
|
|
@@ -950,45 +942,39 @@ body.dark-mode .score-chip.bad {
|
|
| 950 |
background: var(--warm-cream);
|
| 951 |
border: 1px solid var(--warm-tan);
|
| 952 |
border-radius: var(--border-radius);
|
| 953 |
-
padding:
|
| 954 |
-
margin-top:
|
| 955 |
text-align: center;
|
| 956 |
}
|
| 957 |
|
| 958 |
.feedback-prompt {
|
| 959 |
color: var(--text-secondary);
|
| 960 |
-
margin-bottom:
|
| 961 |
font-weight: 500;
|
| 962 |
-
font-size: 0.
|
| 963 |
}
|
| 964 |
|
| 965 |
.feedback-buttons {
|
| 966 |
display: flex;
|
| 967 |
-
gap:
|
| 968 |
-
margin-bottom:
|
| 969 |
flex-wrap: wrap;
|
| 970 |
justify-content: center;
|
| 971 |
}
|
| 972 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
.feedback-message {
|
| 974 |
-
|
| 975 |
border-radius: var(--border-radius-sm);
|
| 976 |
font-weight: 600;
|
| 977 |
text-align: center;
|
| 978 |
font-size: 0.85rem;
|
| 979 |
-
margin-top: 0;
|
| 980 |
-
transition: all 0.3s ease;
|
| 981 |
-
opacity: 0;
|
| 982 |
-
height: 0;
|
| 983 |
-
overflow: hidden;
|
| 984 |
-
}
|
| 985 |
-
|
| 986 |
-
.feedback-message.success,
|
| 987 |
-
.feedback-message.info {
|
| 988 |
-
padding: 10px;
|
| 989 |
-
margin-top: 12px; /* Add space only when message appears */
|
| 990 |
-
opacity: 1;
|
| 991 |
-
height: auto;
|
| 992 |
}
|
| 993 |
|
| 994 |
.feedback-message.success {
|
|
@@ -1054,7 +1040,7 @@ body.dark-mode .score-chip.bad {
|
|
| 1054 |
.chat-header h4 {
|
| 1055 |
font-family: 'Poppins', sans-serif;
|
| 1056 |
color: var(--primary-dark);
|
| 1057 |
-
font-size:
|
| 1058 |
}
|
| 1059 |
|
| 1060 |
.chat-messages {
|
|
@@ -1301,153 +1287,3 @@ body.dark-mode .score-chip.bad {
|
|
| 1301 |
grid-template-columns: 1fr;
|
| 1302 |
}
|
| 1303 |
}
|
| 1304 |
-
|
| 1305 |
-
/* Get Started Tab Styles */
|
| 1306 |
-
.get-started-container {
|
| 1307 |
-
max-width: 1000px;
|
| 1308 |
-
margin: 0 auto;
|
| 1309 |
-
display: flex;
|
| 1310 |
-
flex-direction: column;
|
| 1311 |
-
gap: 20px;
|
| 1312 |
-
}
|
| 1313 |
-
|
| 1314 |
-
.option-card.full-width-card {
|
| 1315 |
-
width: 100%;
|
| 1316 |
-
background: var(--soft-white);
|
| 1317 |
-
border: 1px solid var(--warm-tan);
|
| 1318 |
-
border-radius: var(--border-radius);
|
| 1319 |
-
padding: 16px; /* Restored to a slightly larger padding */
|
| 1320 |
-
box-shadow: var(--shadow-soft);
|
| 1321 |
-
display: flex;
|
| 1322 |
-
flex-direction: column;
|
| 1323 |
-
gap: 12px; /* Slightly reduced gap */
|
| 1324 |
-
}
|
| 1325 |
-
|
| 1326 |
-
.option-header {
|
| 1327 |
-
display: flex;
|
| 1328 |
-
gap: 12px;
|
| 1329 |
-
align-items: flex-start; /* Reverted to align-items: flex-start */
|
| 1330 |
-
}
|
| 1331 |
-
|
| 1332 |
-
.option-icon {
|
| 1333 |
-
font-size: 1.5rem;
|
| 1334 |
-
color: var(--primary-red);
|
| 1335 |
-
background: var(--warm-cream);
|
| 1336 |
-
padding: 8px;
|
| 1337 |
-
border-radius: 50%;
|
| 1338 |
-
}
|
| 1339 |
-
|
| 1340 |
-
.option-header h2 {
|
| 1341 |
-
font-family: 'Poppins', sans-serif;
|
| 1342 |
-
font-size: 1.4rem;
|
| 1343 |
-
color: var(--primary-dark);
|
| 1344 |
-
margin-bottom: 4px;
|
| 1345 |
-
}
|
| 1346 |
-
|
| 1347 |
-
.option-header p {
|
| 1348 |
-
color: var(--text-secondary);
|
| 1349 |
-
font-size: 0.9rem;
|
| 1350 |
-
}
|
| 1351 |
-
|
| 1352 |
-
.code-section {
|
| 1353 |
-
background: #2d2d2d; /* Prism Tomorrow bg match */
|
| 1354 |
-
border-radius: var(--border-radius);
|
| 1355 |
-
overflow: hidden;
|
| 1356 |
-
margin-top: 4px;
|
| 1357 |
-
}
|
| 1358 |
-
|
| 1359 |
-
.code-tabs-header {
|
| 1360 |
-
display: flex;
|
| 1361 |
-
justify-content: space-between;
|
| 1362 |
-
align-items: center;
|
| 1363 |
-
background: #1f1f1f; /* Slightly darker */
|
| 1364 |
-
border-bottom: 1px solid #3d3d3d;
|
| 1365 |
-
padding-right: 10px;
|
| 1366 |
-
}
|
| 1367 |
-
|
| 1368 |
-
.code-tabs {
|
| 1369 |
-
display: flex;
|
| 1370 |
-
overflow-x: auto;
|
| 1371 |
-
}
|
| 1372 |
-
|
| 1373 |
-
.code-tab {
|
| 1374 |
-
background: transparent;
|
| 1375 |
-
border: none;
|
| 1376 |
-
color: #94A3B8;
|
| 1377 |
-
padding: 8px 16px;
|
| 1378 |
-
cursor: pointer;
|
| 1379 |
-
font-size: 0.8rem;
|
| 1380 |
-
font-family: 'Inter', sans-serif;
|
| 1381 |
-
font-weight: 500;
|
| 1382 |
-
border-bottom: 2px solid transparent;
|
| 1383 |
-
transition: all 0.2s ease;
|
| 1384 |
-
white-space: nowrap;
|
| 1385 |
-
}
|
| 1386 |
-
|
| 1387 |
-
.code-tab:hover {
|
| 1388 |
-
color: #E2E8F0;
|
| 1389 |
-
background: rgba(255, 255, 255, 0.05);
|
| 1390 |
-
}
|
| 1391 |
-
|
| 1392 |
-
.code-tab.active {
|
| 1393 |
-
color: #FFFFFF;
|
| 1394 |
-
border-bottom-color: var(--primary-red);
|
| 1395 |
-
background: rgba(255, 255, 255, 0.08);
|
| 1396 |
-
}
|
| 1397 |
-
|
| 1398 |
-
.copy-btn {
|
| 1399 |
-
background: transparent;
|
| 1400 |
-
border: 1px solid #475569;
|
| 1401 |
-
color: #cbd5e1;
|
| 1402 |
-
padding: 4px 10px;
|
| 1403 |
-
border-radius: 4px;
|
| 1404 |
-
font-size: 0.75rem;
|
| 1405 |
-
cursor: pointer;
|
| 1406 |
-
display: flex;
|
| 1407 |
-
align-items: center;
|
| 1408 |
-
gap: 6px;
|
| 1409 |
-
transition: all 0.2s ease;
|
| 1410 |
-
}
|
| 1411 |
-
|
| 1412 |
-
.copy-btn:hover {
|
| 1413 |
-
background: rgba(255, 255, 255, 0.1);
|
| 1414 |
-
border-color: #94a3b8;
|
| 1415 |
-
color: white;
|
| 1416 |
-
}
|
| 1417 |
-
|
| 1418 |
-
.code-display {
|
| 1419 |
-
padding: 16px;
|
| 1420 |
-
background: #2d2d2d; /* Match Prism theme */
|
| 1421 |
-
overflow-x: auto;
|
| 1422 |
-
}
|
| 1423 |
-
|
| 1424 |
-
.code-block {
|
| 1425 |
-
display: none;
|
| 1426 |
-
}
|
| 1427 |
-
|
| 1428 |
-
.code-block.active {
|
| 1429 |
-
display: block;
|
| 1430 |
-
animation: fadeIn 0.3s ease;
|
| 1431 |
-
}
|
| 1432 |
-
|
| 1433 |
-
/* Override Prism margins */
|
| 1434 |
-
.code-block pre[class*="language-"] {
|
| 1435 |
-
margin: 0 !important;
|
| 1436 |
-
padding: 0 !important;
|
| 1437 |
-
background: transparent !important;
|
| 1438 |
-
text-shadow: none !important;
|
| 1439 |
-
border-radius: 0 !important;
|
| 1440 |
-
box-shadow: none !important;
|
| 1441 |
-
}
|
| 1442 |
-
|
| 1443 |
-
.code-block code[class*="language-"] {
|
| 1444 |
-
font-family: 'Menlo', 'Monaco', 'Courier New', monospace !important;
|
| 1445 |
-
font-size: 0.85rem !important;
|
| 1446 |
-
line-height: 1.5 !important;
|
| 1447 |
-
}
|
| 1448 |
-
|
| 1449 |
-
.option-action {
|
| 1450 |
-
display: flex;
|
| 1451 |
-
justify-content: flex-start; /* Align button to the left */
|
| 1452 |
-
margin-top: 0; /* Relies on parent gap */
|
| 1453 |
-
}
|
|
|
|
| 1 |
/* LionGuard 2 - Warm, Inviting, and Bright Design */
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@600;700&display=swap');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
:root {
|
| 5 |
/* Modern color palette inspired by logo */
|
|
|
|
| 105 |
margin-left: auto;
|
| 106 |
display: flex;
|
| 107 |
align-items: center;
|
| 108 |
+
gap: 16px;
|
| 109 |
flex-wrap: wrap;
|
| 110 |
justify-content: flex-end;
|
| 111 |
background: transparent;
|
|
|
|
| 130 |
|
| 131 |
.logo-text h1 {
|
| 132 |
font-family: 'Poppins', sans-serif;
|
| 133 |
+
font-size: 1.4rem;
|
| 134 |
+
color: var(--primary-red);
|
|
|
|
|
|
|
| 135 |
margin-bottom: 0px;
|
| 136 |
line-height: 1.2;
|
| 137 |
}
|
|
|
|
| 321 |
display: inline-flex;
|
| 322 |
align-items: center;
|
| 323 |
flex-wrap: wrap;
|
| 324 |
+
gap: 18px;
|
| 325 |
margin-bottom: 0;
|
| 326 |
background: transparent;
|
| 327 |
border: none;
|
|
|
|
| 373 |
}
|
| 374 |
|
| 375 |
.tab-icon {
|
| 376 |
+
font-size: 1.2rem;
|
|
|
|
| 377 |
}
|
| 378 |
|
| 379 |
.dropdown-toggle {
|
| 380 |
+
padding-right: 22px;
|
| 381 |
}
|
| 382 |
|
| 383 |
.dropdown-caret {
|
|
|
|
| 942 |
background: var(--warm-cream);
|
| 943 |
border: 1px solid var(--warm-tan);
|
| 944 |
border-radius: var(--border-radius);
|
| 945 |
+
padding: 10px 14px;
|
| 946 |
+
margin-top: 8px;
|
| 947 |
text-align: center;
|
| 948 |
}
|
| 949 |
|
| 950 |
.feedback-prompt {
|
| 951 |
color: var(--text-secondary);
|
| 952 |
+
margin-bottom: 4px;
|
| 953 |
font-weight: 500;
|
| 954 |
+
font-size: 0.78rem;
|
| 955 |
}
|
| 956 |
|
| 957 |
.feedback-buttons {
|
| 958 |
display: flex;
|
| 959 |
+
gap: 8px;
|
| 960 |
+
margin-bottom: 6px;
|
| 961 |
flex-wrap: wrap;
|
| 962 |
justify-content: center;
|
| 963 |
}
|
| 964 |
|
| 965 |
+
.feedback-buttons .btn {
|
| 966 |
+
flex: 0 0 auto;
|
| 967 |
+
padding: 6px 12px;
|
| 968 |
+
font-size: 0.8rem;
|
| 969 |
+
justify-content: center;
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
.feedback-message {
|
| 973 |
+
padding: 8px;
|
| 974 |
border-radius: var(--border-radius-sm);
|
| 975 |
font-weight: 600;
|
| 976 |
text-align: center;
|
| 977 |
font-size: 0.85rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
}
|
| 979 |
|
| 980 |
.feedback-message.success {
|
|
|
|
| 1040 |
.chat-header h4 {
|
| 1041 |
font-family: 'Poppins', sans-serif;
|
| 1042 |
color: var(--primary-dark);
|
| 1043 |
+
font-size: 0.95rem;
|
| 1044 |
}
|
| 1045 |
|
| 1046 |
.chat-messages {
|
|
|
|
| 1287 |
grid-template-columns: 1fr;
|
| 1288 |
}
|
| 1289 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -8,4 +8,5 @@ transformers==4.57.1
|
|
| 8 |
google-genai==1.51.0
|
| 9 |
sentence-transformers==5.1.2
|
| 10 |
fastapi==0.115.0
|
| 11 |
-
uvicorn[standard]==0.32.0
|
|
|
|
|
|
| 8 |
google-genai==1.51.0
|
| 9 |
sentence-transformers==5.1.2
|
| 10 |
fastapi==0.115.0
|
| 11 |
+
uvicorn[standard]==0.32.0
|
| 12 |
+
gradio==5.34.2
|