Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,161 +1,201 @@
|
|
| 1 |
-
"""Gradio app for Maritime Intelligence Classifier."""
|
| 2 |
import gradio as gr
|
| 3 |
from setfit import SetFitModel
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
import os
|
| 6 |
|
| 7 |
-
#
|
| 8 |
-
#
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
#
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
print(f"LOCAL_MODEL_PATH: {LOCAL_MODEL_PATH}")
|
| 16 |
-
model = None
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
try:
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
print(f"
|
| 24 |
-
|
| 25 |
-
elif Path(LOCAL_MODEL_PATH).exists():
|
| 26 |
-
print(f"Loading from local path: {LOCAL_MODEL_PATH}")
|
| 27 |
-
model = SetFitModel.from_pretrained(LOCAL_MODEL_PATH)
|
| 28 |
-
print(f"β Successfully loaded model from local path: {LOCAL_MODEL_PATH}")
|
| 29 |
-
# If MODEL_PATH is a local path that exists
|
| 30 |
-
elif Path(MODEL_PATH).exists():
|
| 31 |
-
print(f"Loading from local path: {MODEL_PATH}")
|
| 32 |
-
model = SetFitModel.from_pretrained(MODEL_PATH)
|
| 33 |
-
print(f"β Successfully loaded model from local path: {MODEL_PATH}")
|
| 34 |
-
# Default: try MODEL_PATH as Hugging Face repo
|
| 35 |
else:
|
| 36 |
-
print(f"
|
| 37 |
-
|
| 38 |
-
|
| 39 |
except Exception as e:
|
| 40 |
-
print(f"β
|
| 41 |
-
print(f" Attempted paths:")
|
| 42 |
-
print(f" - Hugging Face: {MODEL_PATH}")
|
| 43 |
-
print(f" - Local: {LOCAL_MODEL_PATH}")
|
| 44 |
-
import traceback
|
| 45 |
-
print("\nFull traceback:")
|
| 46 |
-
traceback.print_exc()
|
| 47 |
-
model = None
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
print("
|
| 52 |
-
print(f" 1. Model exists at: https://huggingface.co/{MODEL_PATH}")
|
| 53 |
-
print(" 2. Internet connection is available")
|
| 54 |
-
print(" 3. All dependencies are installed (setfit, sentence-transformers, etc.)")
|
| 55 |
else:
|
| 56 |
-
print("
|
|
|
|
| 57 |
|
|
|
|
|
|
|
|
|
|
| 58 |
def truncate_text(text, max_tokens=256):
|
| 59 |
-
"""
|
| 60 |
-
Truncate text to approximately max_tokens.
|
| 61 |
-
Uses a simple word-based approximation (roughly 1 token = 0.75 words).
|
| 62 |
-
"""
|
| 63 |
if not text:
|
| 64 |
return text
|
| 65 |
|
| 66 |
-
# Rough approximation: 1 token β 0.75 words (conservative estimate)
|
| 67 |
max_words = int(max_tokens * 0.75)
|
| 68 |
words = text.split()
|
| 69 |
|
| 70 |
if len(words) <= max_words:
|
| 71 |
return text
|
| 72 |
|
| 73 |
-
# Truncate and add ellipsis
|
| 74 |
truncated = " ".join(words[:max_words])
|
| 75 |
return truncated + "... [truncated]"
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
def predict_text(text):
|
| 78 |
-
"""Predict whether text is actionable
|
| 79 |
-
if
|
| 80 |
-
return "Error:
|
| 81 |
|
| 82 |
if not text or not text.strip():
|
| 83 |
return "Please enter some text to classify.", 0.0, "neutral"
|
| 84 |
|
| 85 |
try:
|
| 86 |
-
#
|
| 87 |
-
# The model will automatically truncate longer texts, but we can pre-truncate
|
| 88 |
-
# to ensure we're using the most relevant part (beginning of text)
|
| 89 |
-
# For longer articles, the beginning usually contains the most important info
|
| 90 |
-
|
| 91 |
-
# Check approximate length (rough estimate: 1 token β 0.75 words)
|
| 92 |
word_count = len(text.split())
|
| 93 |
token_estimate = int(word_count / 0.75)
|
| 94 |
|
| 95 |
-
|
| 96 |
-
# (SetFit will truncate anyway, but we can control which part)
|
| 97 |
-
if token_estimate > 300: # Give some buffer
|
| 98 |
-
# For news articles, the beginning usually has the key info
|
| 99 |
-
# But we could also try: beginning + end, or just beginning
|
| 100 |
processed_text = truncate_text(text, max_tokens=256)
|
| 101 |
-
print(f"β οΈ Text truncated from ~{token_estimate} tokens to ~256 tokens")
|
| 102 |
else:
|
| 103 |
processed_text = text
|
| 104 |
|
| 105 |
# Make prediction
|
| 106 |
-
prediction =
|
| 107 |
|
| 108 |
-
# Get probabilities
|
| 109 |
try:
|
| 110 |
-
probabilities =
|
| 111 |
confidence = probabilities[prediction] * 100
|
| 112 |
-
except AttributeError
|
| 113 |
-
|
| 114 |
-
# Use a simple confidence estimate based on prediction
|
| 115 |
-
print(f"Warning: predict_proba failed ({e}), using fallback confidence")
|
| 116 |
-
# For binary classification, we can estimate confidence from the decision function
|
| 117 |
-
# or just use a default high confidence
|
| 118 |
-
confidence = 85.0 # Default confidence when we can't get probabilities
|
| 119 |
-
|
| 120 |
-
# Convert to labels
|
| 121 |
-
label = "YES (Actionable)" if prediction == 1 else "NO (Not Actionable)"
|
| 122 |
|
| 123 |
-
|
| 124 |
status = "actionable" if prediction == 1 else "not_actionable"
|
| 125 |
|
| 126 |
return label, confidence, status
|
| 127 |
except Exception as e:
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
def get_explanation(status):
|
| 135 |
"""Get explanation based on prediction status."""
|
| 136 |
explanations = {
|
| 137 |
-
"actionable": "β This text contains actionable vessel-specific evidence
|
| 138 |
-
"not_actionable": "β This text does not contain actionable vessel-specific evidence
|
| 139 |
"error": "β οΈ An error occurred. Please check the model is properly loaded.",
|
| 140 |
"neutral": ""
|
| 141 |
}
|
| 142 |
return explanations.get(status, "")
|
| 143 |
|
| 144 |
-
#
|
| 145 |
-
#
|
|
|
|
| 146 |
with gr.Blocks(title="Maritime Intelligence Classifier") as app:
|
| 147 |
gr.Markdown(
|
| 148 |
"""
|
| 149 |
# π’ Maritime Intelligence Classifier
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
**
|
| 154 |
-
- Specific vessel names
|
| 155 |
-
- Specific crimes or incidents
|
| 156 |
-
- Evidence that can be used for investigation
|
| 157 |
-
|
| 158 |
-
**Non-actionable articles** are general maritime news without specific vessel details.
|
| 159 |
"""
|
| 160 |
)
|
| 161 |
|
|
@@ -168,9 +208,11 @@ with gr.Blocks(title="Maritime Intelligence Classifier") as app:
|
|
| 168 |
max_lines=20
|
| 169 |
)
|
| 170 |
|
| 171 |
-
submit_btn = gr.Button("
|
| 172 |
|
| 173 |
with gr.Column(scale=1):
|
|
|
|
|
|
|
| 174 |
prediction_output = gr.Label(
|
| 175 |
label="Prediction",
|
| 176 |
value={"YES (Actionable)": 0.0, "NO (Not Actionable)": 0.0}
|
|
@@ -183,6 +225,13 @@ with gr.Blocks(title="Maritime Intelligence Classifier") as app:
|
|
| 183 |
)
|
| 184 |
|
| 185 |
explanation_output = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
# Example texts
|
| 188 |
gr.Markdown("### π Example Texts")
|
|
@@ -190,10 +239,10 @@ with gr.Blocks(title="Maritime Intelligence Classifier") as app:
|
|
| 190 |
example_yes = gr.Examples(
|
| 191 |
examples=[
|
| 192 |
["The fishing vessel Marine 707 was involved in the disappearance of fisheries observer Samuel Abayateye in Ghanaian waters. The observer's decapitated body was found weeks later."],
|
| 193 |
-
["Authorities detained the Meng Xin 15 after discovering evidence of illegal saiko transshipment
|
| 194 |
],
|
| 195 |
inputs=text_input,
|
| 196 |
-
label="
|
| 197 |
)
|
| 198 |
|
| 199 |
example_no = gr.Examples(
|
|
@@ -202,14 +251,15 @@ with gr.Blocks(title="Maritime Intelligence Classifier") as app:
|
|
| 202 |
["Marine scientists are studying the effects of ocean acidification on coral reefs in tropical waters."],
|
| 203 |
],
|
| 204 |
inputs=text_input,
|
| 205 |
-
label="
|
| 206 |
)
|
| 207 |
|
| 208 |
-
#
|
| 209 |
-
def
|
|
|
|
| 210 |
label, confidence, status = predict_text(text)
|
| 211 |
|
| 212 |
-
# Create label dict
|
| 213 |
if status == "actionable":
|
| 214 |
label_dict = {"YES (Actionable)": confidence / 100, "NO (Not Actionable)": (100 - confidence) / 100}
|
| 215 |
elif status == "not_actionable":
|
|
@@ -219,18 +269,22 @@ with gr.Blocks(title="Maritime Intelligence Classifier") as app:
|
|
| 219 |
|
| 220 |
explanation = get_explanation(status)
|
| 221 |
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
submit_btn.click(
|
| 225 |
-
fn=
|
| 226 |
inputs=text_input,
|
| 227 |
-
outputs=[prediction_output, confidence_output, explanation_output]
|
| 228 |
)
|
| 229 |
|
| 230 |
text_input.submit(
|
| 231 |
-
fn=
|
| 232 |
inputs=text_input,
|
| 233 |
-
outputs=[prediction_output, confidence_output, explanation_output]
|
| 234 |
)
|
| 235 |
|
| 236 |
gr.Markdown(
|
|
@@ -238,15 +292,13 @@ with gr.Blocks(title="Maritime Intelligence Classifier") as app:
|
|
| 238 |
---
|
| 239 |
### βΉοΈ About
|
| 240 |
|
| 241 |
-
|
| 242 |
-
|
|
|
|
| 243 |
|
| 244 |
-
|
| 245 |
"""
|
| 246 |
)
|
| 247 |
|
| 248 |
if __name__ == "__main__":
|
| 249 |
-
app.launch(share=False, theme=gr.themes.Soft())
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
| 1 |
+
"""Gradio app for Maritime Intelligence Classifier + Entity Extraction."""
|
| 2 |
import gradio as gr
|
| 3 |
from setfit import SetFitModel
|
| 4 |
+
from transformers import pipeline
|
| 5 |
from pathlib import Path
|
| 6 |
import os
|
| 7 |
|
| 8 |
+
# ============================================================
|
| 9 |
+
# MODEL PATHS
|
| 10 |
+
# ============================================================
|
| 11 |
+
# Classification model (SetFit)
|
| 12 |
+
CLASSIFIER_PATH = os.getenv("CLASSIFIER_PATH", "gamaly/maritime-intelligence-classifier")
|
| 13 |
+
LOCAL_CLASSIFIER_PATH = "./maritime_classifier"
|
| 14 |
|
| 15 |
+
# NER model (BERT) - UPDATE THIS WITH YOUR HF REPO
|
| 16 |
+
NER_PATH = os.getenv("NER_PATH", "gamaly/bert-vessel-ner") # β Change to your repo!
|
| 17 |
+
LOCAL_NER_PATH = "./models/bert-vessel-ner"
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
# ============================================================
|
| 20 |
+
# LOAD MODELS
|
| 21 |
+
# ============================================================
|
| 22 |
+
print("="*60)
|
| 23 |
+
print("Loading models...")
|
| 24 |
+
print("="*60)
|
| 25 |
+
|
| 26 |
+
# Load Classification Model
|
| 27 |
+
classifier = None
|
| 28 |
+
try:
|
| 29 |
+
if "/" in CLASSIFIER_PATH and not Path(CLASSIFIER_PATH).exists():
|
| 30 |
+
print(f"Loading classifier from HuggingFace: {CLASSIFIER_PATH}")
|
| 31 |
+
classifier = SetFitModel.from_pretrained(CLASSIFIER_PATH)
|
| 32 |
+
elif Path(LOCAL_CLASSIFIER_PATH).exists():
|
| 33 |
+
print(f"Loading classifier from local: {LOCAL_CLASSIFIER_PATH}")
|
| 34 |
+
classifier = SetFitModel.from_pretrained(LOCAL_CLASSIFIER_PATH)
|
| 35 |
+
else:
|
| 36 |
+
print(f"Loading classifier from HuggingFace: {CLASSIFIER_PATH}")
|
| 37 |
+
classifier = SetFitModel.from_pretrained(CLASSIFIER_PATH)
|
| 38 |
+
print(f"β Classifier loaded")
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print(f"β Classifier failed to load: {e}")
|
| 41 |
+
|
| 42 |
+
# Load NER Model
|
| 43 |
+
ner_model = None
|
| 44 |
try:
|
| 45 |
+
if "/" in NER_PATH and not Path(NER_PATH).exists():
|
| 46 |
+
print(f"Loading NER from HuggingFace: {NER_PATH}")
|
| 47 |
+
ner_model = pipeline("ner", model=NER_PATH, aggregation_strategy="simple")
|
| 48 |
+
elif Path(LOCAL_NER_PATH).exists():
|
| 49 |
+
print(f"Loading NER from local: {LOCAL_NER_PATH}")
|
| 50 |
+
ner_model = pipeline("ner", model=LOCAL_NER_PATH, aggregation_strategy="simple")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
else:
|
| 52 |
+
print(f"Loading NER from HuggingFace: {NER_PATH}")
|
| 53 |
+
ner_model = pipeline("ner", model=NER_PATH, aggregation_strategy="simple")
|
| 54 |
+
print(f"β NER model loaded")
|
| 55 |
except Exception as e:
|
| 56 |
+
print(f"β NER model failed to load: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
print("="*60)
|
| 59 |
+
if classifier and ner_model:
|
| 60 |
+
print("β
All models loaded successfully!")
|
|
|
|
|
|
|
|
|
|
| 61 |
else:
|
| 62 |
+
print("β οΈ Some models failed to load. Check logs above.")
|
| 63 |
+
print("="*60)
|
| 64 |
|
| 65 |
+
# ============================================================
|
| 66 |
+
# HELPER FUNCTIONS
|
| 67 |
+
# ============================================================
|
| 68 |
def truncate_text(text, max_tokens=256):
|
| 69 |
+
"""Truncate text to approximately max_tokens."""
|
|
|
|
|
|
|
|
|
|
| 70 |
if not text:
|
| 71 |
return text
|
| 72 |
|
|
|
|
| 73 |
max_words = int(max_tokens * 0.75)
|
| 74 |
words = text.split()
|
| 75 |
|
| 76 |
if len(words) <= max_words:
|
| 77 |
return text
|
| 78 |
|
|
|
|
| 79 |
truncated = " ".join(words[:max_words])
|
| 80 |
return truncated + "... [truncated]"
|
| 81 |
|
| 82 |
+
def extract_entities(text):
|
| 83 |
+
"""Extract VESSEL and ORG entities from text."""
|
| 84 |
+
if ner_model is None:
|
| 85 |
+
return [], []
|
| 86 |
+
|
| 87 |
+
if not text or not text.strip():
|
| 88 |
+
return [], []
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
entities = ner_model(text)
|
| 92 |
+
|
| 93 |
+
vessels = []
|
| 94 |
+
orgs = []
|
| 95 |
+
|
| 96 |
+
for e in entities:
|
| 97 |
+
entity_text = e['word'].strip()
|
| 98 |
+
score = e['score']
|
| 99 |
+
entity_type = e['entity_group']
|
| 100 |
+
|
| 101 |
+
# Skip low confidence
|
| 102 |
+
if score < 0.5:
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
# Clean up tokenization artifacts
|
| 106 |
+
entity_text = entity_text.replace(" ##", "").replace("##", "")
|
| 107 |
+
|
| 108 |
+
if entity_type == 'VESSEL':
|
| 109 |
+
vessels.append({"text": entity_text, "score": score})
|
| 110 |
+
elif entity_type == 'ORG':
|
| 111 |
+
orgs.append({"text": entity_text, "score": score})
|
| 112 |
+
|
| 113 |
+
# Deduplicate
|
| 114 |
+
vessels = list({v['text']: v for v in vessels}.values())
|
| 115 |
+
orgs = list({o['text']: o for o in orgs}.values())
|
| 116 |
+
|
| 117 |
+
return vessels, orgs
|
| 118 |
+
except Exception as e:
|
| 119 |
+
print(f"NER error: {e}")
|
| 120 |
+
return [], []
|
| 121 |
+
|
| 122 |
def predict_text(text):
|
| 123 |
+
"""Predict whether text is actionable and extract entities."""
|
| 124 |
+
if classifier is None:
|
| 125 |
+
return "Error: Classifier not loaded.", 0.0, "error"
|
| 126 |
|
| 127 |
if not text or not text.strip():
|
| 128 |
return "Please enter some text to classify.", 0.0, "neutral"
|
| 129 |
|
| 130 |
try:
|
| 131 |
+
# Truncate if needed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
word_count = len(text.split())
|
| 133 |
token_estimate = int(word_count / 0.75)
|
| 134 |
|
| 135 |
+
if token_estimate > 300:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
processed_text = truncate_text(text, max_tokens=256)
|
|
|
|
| 137 |
else:
|
| 138 |
processed_text = text
|
| 139 |
|
| 140 |
# Make prediction
|
| 141 |
+
prediction = classifier.predict([processed_text])[0]
|
| 142 |
|
| 143 |
+
# Get probabilities
|
| 144 |
try:
|
| 145 |
+
probabilities = classifier.predict_proba([processed_text])[0]
|
| 146 |
confidence = probabilities[prediction] * 100
|
| 147 |
+
except AttributeError:
|
| 148 |
+
confidence = 85.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
+
label = "YES (Actionable)" if prediction == 1 else "NO (Not Actionable)"
|
| 151 |
status = "actionable" if prediction == 1 else "not_actionable"
|
| 152 |
|
| 153 |
return label, confidence, status
|
| 154 |
except Exception as e:
|
| 155 |
+
print(f"Classification error: {e}")
|
| 156 |
+
return f"Error: {str(e)}", 0.0, "error"
|
| 157 |
+
|
| 158 |
+
def format_entities(vessels, orgs):
|
| 159 |
+
"""Format extracted entities as markdown."""
|
| 160 |
+
if not vessels and not orgs:
|
| 161 |
+
return "No entities detected."
|
| 162 |
+
|
| 163 |
+
output = ""
|
| 164 |
+
|
| 165 |
+
if vessels:
|
| 166 |
+
output += "### π’ Vessels\n"
|
| 167 |
+
for v in vessels:
|
| 168 |
+
output += f"- **{v['text']}** ({v['score']:.0%})\n"
|
| 169 |
+
output += "\n"
|
| 170 |
+
|
| 171 |
+
if orgs:
|
| 172 |
+
output += "### π’ Organizations\n"
|
| 173 |
+
for o in orgs:
|
| 174 |
+
output += f"- **{o['text']}** ({o['score']:.0%})\n"
|
| 175 |
+
|
| 176 |
+
return output
|
| 177 |
|
| 178 |
def get_explanation(status):
|
| 179 |
"""Get explanation based on prediction status."""
|
| 180 |
explanations = {
|
| 181 |
+
"actionable": "β This text contains actionable vessel-specific evidence.",
|
| 182 |
+
"not_actionable": "β This text does not contain actionable vessel-specific evidence.",
|
| 183 |
"error": "β οΈ An error occurred. Please check the model is properly loaded.",
|
| 184 |
"neutral": ""
|
| 185 |
}
|
| 186 |
return explanations.get(status, "")
|
| 187 |
|
| 188 |
+
# ============================================================
|
| 189 |
+
# GRADIO APP
|
| 190 |
+
# ============================================================
|
| 191 |
with gr.Blocks(title="Maritime Intelligence Classifier") as app:
|
| 192 |
gr.Markdown(
|
| 193 |
"""
|
| 194 |
# π’ Maritime Intelligence Classifier
|
| 195 |
|
| 196 |
+
**Two-stage analysis:**
|
| 197 |
+
1. **Classification** - Is this article actionable?
|
| 198 |
+
2. **Entity Extraction** - What vessels and organizations are mentioned?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
"""
|
| 200 |
)
|
| 201 |
|
|
|
|
| 208 |
max_lines=20
|
| 209 |
)
|
| 210 |
|
| 211 |
+
submit_btn = gr.Button("Analyze", variant="primary", size="lg")
|
| 212 |
|
| 213 |
with gr.Column(scale=1):
|
| 214 |
+
# Classification results
|
| 215 |
+
gr.Markdown("### π Classification")
|
| 216 |
prediction_output = gr.Label(
|
| 217 |
label="Prediction",
|
| 218 |
value={"YES (Actionable)": 0.0, "NO (Not Actionable)": 0.0}
|
|
|
|
| 225 |
)
|
| 226 |
|
| 227 |
explanation_output = gr.Markdown()
|
| 228 |
+
|
| 229 |
+
# Entity extraction results
|
| 230 |
+
gr.Markdown("---")
|
| 231 |
+
entities_output = gr.Markdown(
|
| 232 |
+
label="Extracted Entities",
|
| 233 |
+
value="### π Extracted Entities\nNo entities detected yet."
|
| 234 |
+
)
|
| 235 |
|
| 236 |
# Example texts
|
| 237 |
gr.Markdown("### π Example Texts")
|
|
|
|
| 239 |
example_yes = gr.Examples(
|
| 240 |
examples=[
|
| 241 |
["The fishing vessel Marine 707 was involved in the disappearance of fisheries observer Samuel Abayateye in Ghanaian waters. The observer's decapitated body was found weeks later."],
|
| 242 |
+
["Authorities detained the Meng Xin 15 after discovering evidence of illegal saiko transshipment. Pacific Seafood Inc. was identified as the vessel operator."],
|
| 243 |
],
|
| 244 |
inputs=text_input,
|
| 245 |
+
label="Actionable Examples"
|
| 246 |
)
|
| 247 |
|
| 248 |
example_no = gr.Examples(
|
|
|
|
| 251 |
["Marine scientists are studying the effects of ocean acidification on coral reefs in tropical waters."],
|
| 252 |
],
|
| 253 |
inputs=text_input,
|
| 254 |
+
label="Non-Actionable Examples"
|
| 255 |
)
|
| 256 |
|
| 257 |
+
# Main analysis function
|
| 258 |
+
def analyze_text(text):
|
| 259 |
+
# Classification
|
| 260 |
label, confidence, status = predict_text(text)
|
| 261 |
|
| 262 |
+
# Create label dict
|
| 263 |
if status == "actionable":
|
| 264 |
label_dict = {"YES (Actionable)": confidence / 100, "NO (Not Actionable)": (100 - confidence) / 100}
|
| 265 |
elif status == "not_actionable":
|
|
|
|
| 269 |
|
| 270 |
explanation = get_explanation(status)
|
| 271 |
|
| 272 |
+
# Entity extraction
|
| 273 |
+
vessels, orgs = extract_entities(text)
|
| 274 |
+
entities_md = "### π Extracted Entities\n" + format_entities(vessels, orgs)
|
| 275 |
+
|
| 276 |
+
return label_dict, confidence, explanation, entities_md
|
| 277 |
|
| 278 |
submit_btn.click(
|
| 279 |
+
fn=analyze_text,
|
| 280 |
inputs=text_input,
|
| 281 |
+
outputs=[prediction_output, confidence_output, explanation_output, entities_output]
|
| 282 |
)
|
| 283 |
|
| 284 |
text_input.submit(
|
| 285 |
+
fn=analyze_text,
|
| 286 |
inputs=text_input,
|
| 287 |
+
outputs=[prediction_output, confidence_output, explanation_output, entities_output]
|
| 288 |
)
|
| 289 |
|
| 290 |
gr.Markdown(
|
|
|
|
| 292 |
---
|
| 293 |
### βΉοΈ About
|
| 294 |
|
| 295 |
+
**Classification**: SetFit model identifies actionable maritime intelligence.
|
| 296 |
+
|
| 297 |
+
**Entity Extraction**: BERT-NER model extracts vessel names and organizations.
|
| 298 |
|
| 299 |
+
Built for The Outlaw Ocean Project.
|
| 300 |
"""
|
| 301 |
)
|
| 302 |
|
| 303 |
if __name__ == "__main__":
|
| 304 |
+
app.launch(share=False, theme=gr.themes.Soft())
|
|
|
|
|
|
|
|
|