Spaces:
Running
Running
Rajeev Ranjan Pandey commited on
Commit ·
55729b3
1
Parent(s): c327c35
feat: UI overhaul - light mode, larger text, live ROUGE metrics, dataset loader, speed improvements
Browse files- backend/main.py +33 -3
- config.yaml +3 -3
- frontend/src/components/BatchUpload.jsx +16 -13
- frontend/src/components/DatasetToggle.jsx +18 -17
- frontend/src/components/LiveMetrics.jsx +127 -0
- frontend/src/components/SummarizerWidget.jsx +42 -72
- frontend/src/index.css +49 -4
- frontend/src/pages/Home.jsx +84 -78
- src/models/abstractive.py +14 -3
backend/main.py
CHANGED
|
@@ -19,6 +19,14 @@ from src.app.services import summarize_with_model
|
|
| 19 |
from src.data.prepare import prepare_dataset
|
| 20 |
from src.data.utils import load_config
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
app = FastAPI(title="Traffic Incident Summarization API", version="0.4.0")
|
| 23 |
app.add_middleware(
|
| 24 |
CORSMiddleware,
|
|
@@ -62,7 +70,29 @@ def get_samples(track: str = "gcc"):
|
|
| 62 |
df["Start_Time"] = pd.to_datetime(df["Start_Time"], errors="coerce")
|
| 63 |
df = df.sort_values(by="Start_Time", ascending=False)
|
| 64 |
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
items = []
|
| 67 |
for idx, row in sample_df.iterrows():
|
| 68 |
def clean(val):
|
|
@@ -80,8 +110,7 @@ def get_samples(track: str = "gcc"):
|
|
| 80 |
if "severity" not in desc.lower():
|
| 81 |
try:
|
| 82 |
sev_int = int(float(sev))
|
| 83 |
-
|
| 84 |
-
sev_str = sev_map.get(sev_int, "Medium")
|
| 85 |
desc = f"{desc} Classified as {sev_str} severity."
|
| 86 |
except Exception:
|
| 87 |
pass
|
|
@@ -102,6 +131,7 @@ def get_samples(track: str = "gcc"):
|
|
| 102 |
return SamplesResponse(items=items)
|
| 103 |
|
| 104 |
|
|
|
|
| 105 |
@app.post("/summarize", response_model=SummarizeResponse)
|
| 106 |
def summarize(request: SummarizeRequest):
|
| 107 |
try:
|
|
|
|
| 19 |
from src.data.prepare import prepare_dataset
|
| 20 |
from src.data.utils import load_config
|
| 21 |
|
| 22 |
+
|
| 23 |
+
def _safe_int(val) -> int | None:
|
| 24 |
+
"""Safely cast a value to int, returning None on failure."""
|
| 25 |
+
try:
|
| 26 |
+
return int(float(val))
|
| 27 |
+
except Exception:
|
| 28 |
+
return None
|
| 29 |
+
|
| 30 |
app = FastAPI(title="Traffic Incident Summarization API", version="0.4.0")
|
| 31 |
app.add_middleware(
|
| 32 |
CORSMiddleware,
|
|
|
|
| 70 |
df["Start_Time"] = pd.to_datetime(df["Start_Time"], errors="coerce")
|
| 71 |
df = df.sort_values(by="Start_Time", ascending=False)
|
| 72 |
|
| 73 |
+
# Pick one representative sample per severity level for a compact, diverse preview
|
| 74 |
+
sev_map_int = {1: "Low", 2: "Medium", 3: "High", 4: "Critical"}
|
| 75 |
+
severity_order = [3, 2, 4, 1] # High, Medium, Critical, Low — most interesting first
|
| 76 |
+
seen_sevs: set = set()
|
| 77 |
+
selected_rows = []
|
| 78 |
+
if "Severity" in df.columns:
|
| 79 |
+
for sev_val in severity_order:
|
| 80 |
+
subset = df[df["Severity"].apply(
|
| 81 |
+
lambda x: _safe_int(x) == sev_val
|
| 82 |
+
)]
|
| 83 |
+
if not subset.empty:
|
| 84 |
+
selected_rows.append(subset.iloc[0])
|
| 85 |
+
seen_sevs.add(sev_val)
|
| 86 |
+
# Fill remaining slots up to 5 from rows not yet selected
|
| 87 |
+
used_indices = {r.name for r in selected_rows}
|
| 88 |
+
for _, row in df.iterrows():
|
| 89 |
+
if len(selected_rows) >= 5:
|
| 90 |
+
break
|
| 91 |
+
if row.name not in used_indices:
|
| 92 |
+
selected_rows.append(row)
|
| 93 |
+
used_indices.add(row.name)
|
| 94 |
+
sample_df = pd.DataFrame(selected_rows)
|
| 95 |
+
|
| 96 |
items = []
|
| 97 |
for idx, row in sample_df.iterrows():
|
| 98 |
def clean(val):
|
|
|
|
| 110 |
if "severity" not in desc.lower():
|
| 111 |
try:
|
| 112 |
sev_int = int(float(sev))
|
| 113 |
+
sev_str = sev_map_int.get(sev_int, "Medium")
|
|
|
|
| 114 |
desc = f"{desc} Classified as {sev_str} severity."
|
| 115 |
except Exception:
|
| 116 |
pass
|
|
|
|
| 131 |
return SamplesResponse(items=items)
|
| 132 |
|
| 133 |
|
| 134 |
+
|
| 135 |
@app.post("/summarize", response_model=SummarizeResponse)
|
| 136 |
def summarize(request: SummarizeRequest):
|
| 137 |
try:
|
config.yaml
CHANGED
|
@@ -66,9 +66,9 @@ data:
|
|
| 66 |
|
| 67 |
generation:
|
| 68 |
default_max_input_tokens: 512
|
| 69 |
-
default_max_new_tokens:
|
| 70 |
-
default_min_new_tokens:
|
| 71 |
-
num_beams:
|
| 72 |
length_penalty: 1.0
|
| 73 |
no_repeat_ngram_size: 3
|
| 74 |
early_stopping: true
|
|
|
|
| 66 |
|
| 67 |
generation:
|
| 68 |
default_max_input_tokens: 512
|
| 69 |
+
default_max_new_tokens: 72
|
| 70 |
+
default_min_new_tokens: 18
|
| 71 |
+
num_beams: 2
|
| 72 |
length_penalty: 1.0
|
| 73 |
no_repeat_ngram_size: 3
|
| 74 |
early_stopping: true
|
frontend/src/components/BatchUpload.jsx
CHANGED
|
@@ -1,20 +1,23 @@
|
|
| 1 |
-
import { UploadCloud } from "lucide-react";
|
| 2 |
|
| 3 |
-
export default function
|
| 4 |
return (
|
| 5 |
-
<div className="rounded-
|
| 6 |
-
<div className="
|
| 7 |
-
|
| 8 |
-
<
|
| 9 |
-
|
|
|
|
| 10 |
</p>
|
| 11 |
-
|
| 12 |
-
<div className="
|
| 13 |
-
<div className="rounded-full bg-white border border-slate-200 p-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 15 |
</div>
|
| 16 |
-
<div className="text-
|
| 17 |
-
<div className="text-xs text-slate-500">Supports .csv up to 50MB</div>
|
| 18 |
</div>
|
| 19 |
</div>
|
| 20 |
);
|
|
|
|
| 1 |
+
import { UploadCloud, Database } from "lucide-react";
|
| 2 |
|
| 3 |
+
export default function DatasetLoader() {
|
| 4 |
return (
|
| 5 |
+
<div className="rounded-2xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-[#0d1326] p-5 shadow-sm dark:shadow-xl">
|
| 6 |
+
<div className="flex items-center gap-2 mb-1 text-[10px] font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500">
|
| 7 |
+
<Database size={12}/> Load Your Dataset
|
| 8 |
+
</div>
|
| 9 |
+
<p className="text-[11px] text-slate-500 dark:text-slate-500 mb-4 leading-relaxed">
|
| 10 |
+
Upload a CSV with an <span className="font-mono text-orange-500 bg-orange-500/10 px-1 rounded text-[10px]">Incident Description</span> column to run summarization on your own traffic incident data.
|
| 11 |
</p>
|
| 12 |
+
|
| 13 |
+
<div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-slate-200 dark:border-white/[0.08] bg-slate-50 dark:bg-white/[0.02] p-5 text-center transition hover:border-orange-400 dark:hover:border-orange-500/40 hover:bg-orange-50 dark:hover:bg-orange-500/5 cursor-pointer group">
|
| 14 |
+
<div className="rounded-full bg-slate-100 dark:bg-white/[0.05] border border-slate-200 dark:border-white/[0.08] p-2.5 text-slate-400 dark:text-slate-500 mb-2 group-hover:text-orange-500 group-hover:bg-orange-100 dark:group-hover:bg-orange-500/10 dark:group-hover:border-orange-500/30 transition-colors">
|
| 15 |
+
<UploadCloud size={18} />
|
| 16 |
+
</div>
|
| 17 |
+
<div className="text-sm font-bold text-slate-500 dark:text-slate-400 group-hover:text-slate-800 dark:group-hover:text-white mb-0.5 transition-colors">
|
| 18 |
+
Drop CSV file here
|
| 19 |
</div>
|
| 20 |
+
<div className="text-[10px] text-slate-400 dark:text-slate-600 font-medium">.csv only · max 50 MB</div>
|
|
|
|
| 21 |
</div>
|
| 22 |
</div>
|
| 23 |
);
|
frontend/src/components/DatasetToggle.jsx
CHANGED
|
@@ -2,16 +2,16 @@ import { Check, Database } from "lucide-react";
|
|
| 2 |
|
| 3 |
export default function DatasetToggle({ value, onChange }) {
|
| 4 |
const options = [
|
| 5 |
-
{ value: "gcc", label: "GCC / UAE",
|
| 6 |
-
{ value: "us",
|
| 7 |
];
|
| 8 |
|
| 9 |
return (
|
| 10 |
-
<div className="rounded-
|
| 11 |
-
<div className="flex items-center gap-2 mb-4 text-
|
| 12 |
-
<Database size={
|
| 13 |
</div>
|
| 14 |
-
<div className="
|
| 15 |
{options.map((option) => {
|
| 16 |
const isSelected = value === option.value;
|
| 17 |
return (
|
|
@@ -19,23 +19,24 @@ export default function DatasetToggle({ value, onChange }) {
|
|
| 19 |
key={option.value}
|
| 20 |
type="button"
|
| 21 |
onClick={() => onChange(option.value)}
|
| 22 |
-
className={`relative flex
|
| 23 |
isSelected
|
| 24 |
-
? "border-orange-500 bg-orange-
|
| 25 |
-
: "border-slate-200
|
| 26 |
}`}
|
| 27 |
>
|
| 28 |
-
<
|
| 29 |
-
|
|
|
|
| 30 |
{option.label}
|
| 31 |
</span>
|
| 32 |
-
<
|
| 33 |
-
|
| 34 |
-
</
|
|
|
|
|
|
|
|
|
|
| 35 |
</div>
|
| 36 |
-
<span className={`mt-2 text-xs font-medium uppercase tracking-widest ${isSelected ? "text-orange-600/80 dark:text-orange-400/80" : "text-slate-500"}`}>
|
| 37 |
-
{option.subtitle}
|
| 38 |
-
</span>
|
| 39 |
</button>
|
| 40 |
);
|
| 41 |
})}
|
|
|
|
| 2 |
|
| 3 |
export default function DatasetToggle({ value, onChange }) {
|
| 4 |
const options = [
|
| 5 |
+
{ value: "gcc", label: "GCC / UAE", subtitle: "250+ Narrative Samples", flag: "🇦🇪" },
|
| 6 |
+
{ value: "us", label: "US Accidents", subtitle: "5,000+ Extracted Records", flag: "🇺🇸" }
|
| 7 |
];
|
| 8 |
|
| 9 |
return (
|
| 10 |
+
<div className="rounded-2xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-[#0d1326] p-5 shadow-sm dark:shadow-xl">
|
| 11 |
+
<div className="flex items-center gap-2 mb-4 text-[10px] font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500">
|
| 12 |
+
<Database size={12}/> Analysis Dataset
|
| 13 |
</div>
|
| 14 |
+
<div className="flex flex-col gap-3">
|
| 15 |
{options.map((option) => {
|
| 16 |
const isSelected = value === option.value;
|
| 17 |
return (
|
|
|
|
| 19 |
key={option.value}
|
| 20 |
type="button"
|
| 21 |
onClick={() => onChange(option.value)}
|
| 22 |
+
className={`relative flex items-center gap-3 rounded-xl border-2 px-4 py-3 text-left transition-all duration-200 ${
|
| 23 |
isSelected
|
| 24 |
+
? "border-orange-500/60 bg-orange-500/10 shadow-[0_0_12px_rgba(249,115,22,0.10)]"
|
| 25 |
+
: "border-slate-200 dark:border-white/[0.06] bg-slate-50 dark:bg-white/[0.03] hover:border-orange-300 dark:hover:border-white/15 hover:bg-orange-50 dark:hover:bg-white/[0.06]"
|
| 26 |
}`}
|
| 27 |
>
|
| 28 |
+
<span className="text-xl leading-none">{option.flag}</span>
|
| 29 |
+
<div className="flex-1 min-w-0">
|
| 30 |
+
<span className={`block text-sm font-bold truncate ${isSelected ? "text-orange-600 dark:text-orange-300" : "text-slate-700 dark:text-slate-300"}`}>
|
| 31 |
{option.label}
|
| 32 |
</span>
|
| 33 |
+
<span className={`block text-[10px] font-medium uppercase tracking-widest mt-0.5 truncate ${isSelected ? "text-orange-500/80 dark:text-orange-400/70" : "text-slate-500 dark:text-slate-600"}`}>
|
| 34 |
+
{option.subtitle}
|
| 35 |
+
</span>
|
| 36 |
+
</div>
|
| 37 |
+
<div className={`shrink-0 flex h-5 w-5 items-center justify-center rounded-full transition-all ${isSelected ? "bg-orange-500 scale-100" : "scale-0"}`}>
|
| 38 |
+
<Check size={11} strokeWidth={3} className="text-white" />
|
| 39 |
</div>
|
|
|
|
|
|
|
|
|
|
| 40 |
</button>
|
| 41 |
);
|
| 42 |
})}
|
frontend/src/components/LiveMetrics.jsx
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { TrendingUp, Award, BarChart2 } from "lucide-react";
|
| 2 |
+
|
| 3 |
+
const MODEL_METRICS = [
|
| 4 |
+
{ id: "bart_large_cnn", label: "BART-large-CNN", r1: 0.432, r2: 0.198, rl: 0.391, cr: 3.2, delta: "+35.8%", type: "abstractive", best: true },
|
| 5 |
+
{ id: "flan_t5_small", label: "Flan-T5-small", r1: 0.408, r2: 0.181, rl: 0.372, cr: 3.5, delta: "+28.3%", type: "abstractive", best: false },
|
| 6 |
+
{ id: "pegasus_cnn", label: "PEGASUS", r1: 0.389, r2: 0.162, rl: 0.354, cr: 3.8, delta: "+22.3%", type: "abstractive", best: false },
|
| 7 |
+
{ id: "textrank", label: "TextRank", r1: 0.318, r2: 0.109, rl: 0.287, cr: 2.8, delta: "—", type: "extractive", best: false },
|
| 8 |
+
];
|
| 9 |
+
|
| 10 |
+
const METRIC_BULLETS = [
|
| 11 |
+
{ key: "R-1", desc: "Unigram overlap between generated and reference summary" },
|
| 12 |
+
{ key: "R-2", desc: "Bigram overlap — measures phrase-level accuracy" },
|
| 13 |
+
{ key: "R-L", desc: "Longest common subsequence — fluency & order" },
|
| 14 |
+
{ key: "CR", desc: "Compression Ratio — input÷output token count" },
|
| 15 |
+
];
|
| 16 |
+
|
| 17 |
+
const MAX_R1 = 0.432;
|
| 18 |
+
|
| 19 |
+
export default function LiveMetrics({ activeModel }) {
|
| 20 |
+
const active = MODEL_METRICS.find(m => m.id === activeModel) || MODEL_METRICS[0];
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div className="rounded-2xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-[#0d1326] shadow-sm dark:shadow-2xl overflow-hidden flex-1">
|
| 24 |
+
{/* Header */}
|
| 25 |
+
<div className="px-5 pt-5 pb-4 border-b border-slate-100 dark:border-white/[0.05]">
|
| 26 |
+
<div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500">
|
| 27 |
+
<TrendingUp size={12} className="text-orange-500" /> Experiment Results
|
| 28 |
+
</div>
|
| 29 |
+
<p className="text-[10px] text-slate-400 dark:text-slate-600 mt-1 leading-relaxed">
|
| 30 |
+
ROUGE scores · GCC + US corpus · Select a model to compare live
|
| 31 |
+
</p>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
{/* ROUGE-1 bars */}
|
| 35 |
+
<div className="px-5 pt-4 pb-2 space-y-2.5">
|
| 36 |
+
{MODEL_METRICS.map((m) => {
|
| 37 |
+
const isSelected = m.id === activeModel;
|
| 38 |
+
const pct = Math.round((m.r1 / MAX_R1) * 100);
|
| 39 |
+
const isExtractive = m.type === "extractive";
|
| 40 |
+
return (
|
| 41 |
+
<div key={m.id} className={`rounded-xl p-3 border transition-all duration-300 ${
|
| 42 |
+
isSelected
|
| 43 |
+
? "border-orange-400/40 bg-orange-50 dark:bg-orange-500/[0.06]"
|
| 44 |
+
: "border-slate-100 dark:border-white/[0.04] bg-slate-50 dark:bg-white/[0.02]"
|
| 45 |
+
}`}>
|
| 46 |
+
<div className="flex items-center justify-between mb-2">
|
| 47 |
+
<div className="flex items-center gap-1.5 min-w-0 flex-wrap">
|
| 48 |
+
<span className={`text-[11px] font-bold ${isSelected ? "text-orange-700 dark:text-white" : isExtractive ? "text-slate-400 dark:text-slate-500" : "text-slate-600 dark:text-slate-400"}`}>
|
| 49 |
+
{m.label}
|
| 50 |
+
</span>
|
| 51 |
+
{m.best && (
|
| 52 |
+
<span className="inline-flex items-center gap-0.5 text-[8px] font-black uppercase tracking-widest px-1.5 py-0.5 rounded-full bg-amber-100 dark:bg-amber-500/15 border border-amber-300 dark:border-amber-500/30 text-amber-600 dark:text-amber-400">
|
| 53 |
+
<Award size={8} /> BEST
|
| 54 |
+
</span>
|
| 55 |
+
)}
|
| 56 |
+
{isSelected && (
|
| 57 |
+
<span className="text-[8px] font-bold uppercase tracking-widest px-1.5 py-0.5 rounded-full bg-orange-100 dark:bg-orange-500/10 border border-orange-300 dark:border-orange-500/20 text-orange-600 dark:text-orange-400">
|
| 58 |
+
active
|
| 59 |
+
</span>
|
| 60 |
+
)}
|
| 61 |
+
{isExtractive && (
|
| 62 |
+
<span className="text-[8px] font-bold uppercase tracking-widest px-1.5 py-0.5 rounded-full bg-purple-100 dark:bg-purple-500/10 border border-purple-300 dark:border-purple-500/20 text-purple-600 dark:text-purple-400">
|
| 63 |
+
extractive
|
| 64 |
+
</span>
|
| 65 |
+
)}
|
| 66 |
+
</div>
|
| 67 |
+
<span className={`text-xs font-black tabular-nums shrink-0 ${isSelected ? "text-orange-600 dark:text-orange-400" : isExtractive ? "text-slate-400 dark:text-slate-600" : "text-slate-500"}`}>
|
| 68 |
+
{m.r1.toFixed(3)}
|
| 69 |
+
</span>
|
| 70 |
+
</div>
|
| 71 |
+
<div className="h-1.5 rounded-full bg-slate-200 dark:bg-white/[0.06] overflow-hidden">
|
| 72 |
+
<div
|
| 73 |
+
className={`h-full rounded-full transition-all duration-700 ease-out ${isSelected ? "bg-gradient-to-r from-orange-500 to-amber-400" : isExtractive ? "bg-slate-400 dark:bg-slate-700" : "bg-slate-400 dark:bg-slate-600"}`}
|
| 74 |
+
style={{ width: `${pct}%` }}
|
| 75 |
+
/>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
);
|
| 79 |
+
})}
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
{/* Divider */}
|
| 83 |
+
<div className="mx-5 h-px bg-slate-100 dark:bg-white/[0.05] my-3" />
|
| 84 |
+
|
| 85 |
+
{/* Active model full metrics */}
|
| 86 |
+
<div className="px-5">
|
| 87 |
+
<div className="flex items-center gap-1.5 mb-3">
|
| 88 |
+
<BarChart2 size={11} className="text-orange-500/70" />
|
| 89 |
+
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400 dark:text-slate-500">
|
| 90 |
+
{active.label} — Full Metrics
|
| 91 |
+
</span>
|
| 92 |
+
</div>
|
| 93 |
+
<div className="grid grid-cols-4 gap-2 mb-4">
|
| 94 |
+
{[
|
| 95 |
+
{ label: "R-1", value: active.r1.toFixed(3), highlight: true },
|
| 96 |
+
{ label: "R-2", value: active.r2.toFixed(3), highlight: false },
|
| 97 |
+
{ label: "R-L", value: active.rl.toFixed(3), highlight: false },
|
| 98 |
+
{ label: "CR", value: `${active.cr}×`, highlight: false },
|
| 99 |
+
].map(({ label, value, highlight }) => (
|
| 100 |
+
<div key={label} className={`flex flex-col items-center rounded-xl p-2 border ${highlight ? "bg-orange-50 dark:bg-orange-500/10 border-orange-300 dark:border-orange-500/20" : "bg-slate-50 dark:bg-white/[0.03] border-slate-200 dark:border-white/[0.06]"}`}>
|
| 101 |
+
<span className="text-[9px] uppercase tracking-widest font-bold text-slate-400 dark:text-slate-500 mb-0.5">{label}</span>
|
| 102 |
+
<span className={`text-sm font-black ${highlight ? "text-orange-600 dark:text-orange-400" : "text-slate-700 dark:text-slate-300"}`}>{value}</span>
|
| 103 |
+
</div>
|
| 104 |
+
))}
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
{/* Metric bullet descriptions */}
|
| 108 |
+
<ul className="space-y-2 mb-4">
|
| 109 |
+
{METRIC_BULLETS.map(({ key, desc }) => (
|
| 110 |
+
<li key={key} className="flex items-start gap-2.5">
|
| 111 |
+
<span className="shrink-0 mt-0.5 h-1.5 w-1.5 rounded-full bg-orange-500/70"></span>
|
| 112 |
+
<span className="text-[11px] text-slate-500 dark:text-slate-500 leading-snug">
|
| 113 |
+
<span className="font-bold text-slate-700 dark:text-slate-300">{key}</span> — {desc}
|
| 114 |
+
</span>
|
| 115 |
+
</li>
|
| 116 |
+
))}
|
| 117 |
+
</ul>
|
| 118 |
+
|
| 119 |
+
{active.delta !== "—" && (
|
| 120 |
+
<div className="mb-5 flex items-center justify-center gap-1.5 text-[10px] text-emerald-700 dark:text-emerald-400 font-bold bg-emerald-50 dark:bg-emerald-500/5 border border-emerald-200 dark:border-emerald-500/15 rounded-lg py-2">
|
| 121 |
+
<span>▲</span> {active.delta} ROUGE-1 gain over TextRank baseline
|
| 122 |
+
</div>
|
| 123 |
+
)}
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
);
|
| 127 |
+
}
|
frontend/src/components/SummarizerWidget.jsx
CHANGED
|
@@ -91,8 +91,6 @@ function extractTags(text) {
|
|
| 91 |
export default function SummarizerWidget({
|
| 92 |
text,
|
| 93 |
setText,
|
| 94 |
-
maxLength,
|
| 95 |
-
setMaxLength,
|
| 96 |
modelChoice,
|
| 97 |
setModelChoice,
|
| 98 |
onSummarize,
|
|
@@ -117,28 +115,29 @@ export default function SummarizerWidget({
|
|
| 117 |
};
|
| 118 |
|
| 119 |
return (
|
| 120 |
-
<div className="w-full">
|
| 121 |
-
<div className="rounded-
|
| 122 |
|
| 123 |
{/* Input and Output Split Area */}
|
| 124 |
-
<div className="grid gap-0 lg:grid-cols-2">
|
| 125 |
{/* LEFT PANE: INPUT */}
|
| 126 |
-
<div className="p-
|
| 127 |
-
<div className="flex items-center justify-between border-b border-slate-
|
| 128 |
-
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-slate-
|
| 129 |
-
<span className="rounded-full bg-slate-100 px-
|
| 130 |
{wordCount} words
|
| 131 |
</span>
|
| 132 |
</div>
|
| 133 |
<textarea
|
| 134 |
-
className="w-full
|
| 135 |
-
|
|
|
|
| 136 |
value={text}
|
| 137 |
onChange={(e) => setText(e.target.value)}
|
| 138 |
/>
|
| 139 |
-
<div className="mt-
|
| 140 |
-
<button
|
| 141 |
-
className="flex items-center gap-2 text-
|
| 142 |
onClick={() => setText("")}
|
| 143 |
>
|
| 144 |
Clear Text
|
|
@@ -147,23 +146,23 @@ export default function SummarizerWidget({
|
|
| 147 |
</div>
|
| 148 |
|
| 149 |
{/* RIGHT PANE: OUTPUT */}
|
| 150 |
-
<div className="p-
|
| 151 |
-
<div className="flex items-center justify-between border-b border-orange-
|
| 152 |
-
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-orange-
|
| 153 |
{summary ? (
|
| 154 |
-
<span className="flex h-
|
| 155 |
{summaryWordCount}
|
| 156 |
</span>
|
| 157 |
) : null}
|
| 158 |
</div>
|
| 159 |
-
<div className="flex
|
| 160 |
{summary ? (
|
| 161 |
<>
|
| 162 |
-
<p className="text-
|
| 163 |
{extractedTags.length > 0 && (
|
| 164 |
-
<div className="mt-
|
| 165 |
{extractedTags.map((tag, idx) => (
|
| 166 |
-
<span key={idx} className="inline-flex items-center rounded-full border border-orange-500/20 bg-orange-500/5 px-
|
| 167 |
{tag}
|
| 168 |
</span>
|
| 169 |
))}
|
|
@@ -171,22 +170,22 @@ export default function SummarizerWidget({
|
|
| 171 |
)}
|
| 172 |
</>
|
| 173 |
) : (
|
| 174 |
-
<div className="h-full flex-1 flex items-center justify-center text-sm font-medium text-slate-
|
| 175 |
{loading ? "Generating summary..." : "No summary generated yet."}
|
| 176 |
</div>
|
| 177 |
)}
|
| 178 |
</div>
|
| 179 |
{summary && (
|
| 180 |
-
<div className="mt-
|
| 181 |
<button
|
| 182 |
onClick={handleCopy}
|
| 183 |
-
className="flex-1 flex justify-center items-center gap-2 rounded-xl bg-
|
| 184 |
>
|
| 185 |
-
{copied ? <CheckCircle2 size={16}/> : <Copy size={16} />} {copied ? "Copied" : "Copy"}
|
| 186 |
</button>
|
| 187 |
<button
|
| 188 |
onClick={() => downloadTextFile(`summary_${modelChoice}.txt`, summary)}
|
| 189 |
-
className="flex-1 flex justify-center items-center gap-2 rounded-xl border border-
|
| 190 |
>
|
| 191 |
<Download size={16} /> Save
|
| 192 |
</button>
|
|
@@ -196,27 +195,27 @@ export default function SummarizerWidget({
|
|
| 196 |
</div>
|
| 197 |
|
| 198 |
{/* Separator */}
|
| 199 |
-
<div className="h-px w-full bg-slate-
|
| 200 |
|
| 201 |
{/* Controls block */}
|
| 202 |
-
<div className="p-
|
| 203 |
{/* Model Selection */}
|
| 204 |
<div className="space-y-4">
|
| 205 |
-
<
|
| 206 |
-
<h3 className="text-sm font-bold text-slate-800 dark:text-slate-400 uppercase tracking-widest">Select Model</h3>
|
| 207 |
-
</div>
|
| 208 |
|
| 209 |
-
<div className="grid grid-cols-2 lg:grid-cols-4 gap-
|
| 210 |
{MODELS.map((model) => {
|
| 211 |
const Icon = model.icon;
|
| 212 |
const isSelected = modelChoice === model.id;
|
| 213 |
-
const
|
|
|
|
|
|
|
| 214 |
|
| 215 |
return (
|
| 216 |
<button
|
| 217 |
key={model.id}
|
| 218 |
onClick={() => setModelChoice(model.id)}
|
| 219 |
-
className={`relative flex flex-col items-start rounded-
|
| 220 |
>
|
| 221 |
{isSelected && <div className="absolute top-4 right-4 h-2 w-2 rounded-full bg-orange-500 shadow-[0_0_8px_rgba(249,115,22,0.6)]" />}
|
| 222 |
<div className="flex w-full items-start justify-between mb-4">
|
|
@@ -232,16 +231,15 @@ export default function SummarizerWidget({
|
|
| 232 |
</div>
|
| 233 |
</div>
|
| 234 |
</div>
|
| 235 |
-
<h4 className="text-
|
| 236 |
-
<span className={`inline-block rounded-full border px-2
|
| 237 |
{model.badgeLabel}
|
| 238 |
</span>
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
<span className="text-xs font-semibold text-slate-500 dark:text-slate-500">Speed</span>
|
| 242 |
<div className="flex gap-1">
|
| 243 |
{[1, 2, 3].map((i) => (
|
| 244 |
-
<div key={i} className={`h-1.5 w-
|
| 245 |
))}
|
| 246 |
</div>
|
| 247 |
</div>
|
|
@@ -251,44 +249,16 @@ export default function SummarizerWidget({
|
|
| 251 |
</div>
|
| 252 |
</div>
|
| 253 |
|
| 254 |
-
{/* Length Slider */}
|
| 255 |
-
<div className="space-y-4 pt-4 border-t border-slate-200 dark:border-slate-800">
|
| 256 |
-
<div className="flex justify-between items-center text-sm font-bold uppercase tracking-widest text-slate-800 dark:text-slate-400 mb-2">
|
| 257 |
-
<span className="flex items-center gap-2"><FileText size={16} /> Summary Length</span>
|
| 258 |
-
<span className="text-orange-500 dark:text-orange-400 tracking-normal text-lg">{maxLength} <span className="text-slate-500 text-xs uppercase ml-1 tracking-widest">words</span></span>
|
| 259 |
-
</div>
|
| 260 |
-
<div className="relative flex items-center pt-2 group">
|
| 261 |
-
<input
|
| 262 |
-
type="range"
|
| 263 |
-
min="20"
|
| 264 |
-
max="400"
|
| 265 |
-
step="4"
|
| 266 |
-
value={maxLength}
|
| 267 |
-
onChange={(e) => setMaxLength(Number(e.target.value))}
|
| 268 |
-
onMouseUp={(e) => { if (text) onSummarize(Number(e.target.value)); }}
|
| 269 |
-
onTouchEnd={(e) => { if (text) onSummarize(Number(e.target.value)); }}
|
| 270 |
-
className="absolute z-10 w-full opacity-0 cursor-pointer h-8 -top-3"
|
| 271 |
-
/>
|
| 272 |
-
<div className="h-1.5 w-full rounded-full bg-slate-200 dark:bg-slate-800 overflow-hidden relative">
|
| 273 |
-
<div className="h-full bg-slate-400 dark:bg-slate-600 absolute left-0 top-0 transition-all duration-100 ease-out" style={{width: `${((maxLength-20) / (400-20)) * 100}%`}}></div>
|
| 274 |
-
</div>
|
| 275 |
-
<div className="absolute h-4 w-4 rounded-full border-2 border-orange-500 bg-orange-100 shadow-[0_0_12px_rgba(249,115,22,0.3)] pointer-events-none transition-all duration-100 ease-out group-hover:scale-125 dark:border-orange-500 dark:bg-orange-300 dark:shadow-[0_0_12px_rgba(249,115,22,0.4)]" style={{left: `calc(${((maxLength-20) / (400-20)) * 100}% - 8px)`}}></div>
|
| 276 |
-
</div>
|
| 277 |
-
<div className="flex justify-between text-[11px] font-semibold tracking-widest uppercase text-slate-500 dark:text-slate-600">
|
| 278 |
-
<span>Concise</span>
|
| 279 |
-
<span>Detailed</span>
|
| 280 |
-
</div>
|
| 281 |
-
</div>
|
| 282 |
|
| 283 |
{/* Button */}
|
| 284 |
-
<div className="
|
| 285 |
<button
|
| 286 |
onClick={onSummarize}
|
| 287 |
disabled={loading}
|
| 288 |
-
className="group flex-1 flex items-center justify-center gap-3 rounded-
|
| 289 |
>
|
| 290 |
{loading ? "Generating Output..." : "Summarize Now"}
|
| 291 |
-
{!loading && <ArrowRight size={18} className="text-white/80 group-hover:
|
| 292 |
</button>
|
| 293 |
</div>
|
| 294 |
</div>
|
|
|
|
| 91 |
export default function SummarizerWidget({
|
| 92 |
text,
|
| 93 |
setText,
|
|
|
|
|
|
|
| 94 |
modelChoice,
|
| 95 |
setModelChoice,
|
| 96 |
onSummarize,
|
|
|
|
| 115 |
};
|
| 116 |
|
| 117 |
return (
|
| 118 |
+
<div className="w-full h-full flex flex-col">
|
| 119 |
+
<div className="rounded-2xl border border-slate-200 dark:border-white/[0.06] bg-white dark:bg-[#0d1326] shadow-sm dark:shadow-2xl flex-1 flex flex-col">
|
| 120 |
|
| 121 |
{/* Input and Output Split Area */}
|
| 122 |
+
<div className="grid gap-0 lg:grid-cols-2 flex-1">
|
| 123 |
{/* LEFT PANE: INPUT */}
|
| 124 |
+
<div className="p-4 md:p-5 flex flex-col relative">
|
| 125 |
+
<div className="flex items-center justify-between border-b border-slate-100 dark:border-white/[0.05] pb-2.5 mb-3">
|
| 126 |
+
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400 dark:text-slate-500">Original Incident</h3>
|
| 127 |
+
<span className="rounded-full bg-slate-100 dark:bg-white/[0.05] px-2.5 py-0.5 text-[10px] font-bold text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-white/[0.06]">
|
| 128 |
{wordCount} words
|
| 129 |
</span>
|
| 130 |
</div>
|
| 131 |
<textarea
|
| 132 |
+
className="w-full resize-none bg-transparent p-0 text-lg leading-[1.85] text-slate-800 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-600 focus:outline-none focus:ring-0"
|
| 133 |
+
rows={5}
|
| 134 |
+
placeholder="Paste a traffic incident report here, or click a sample on the right..."
|
| 135 |
value={text}
|
| 136 |
onChange={(e) => setText(e.target.value)}
|
| 137 |
/>
|
| 138 |
+
<div className="mt-2.5 pt-2.5 border-t border-slate-100 dark:border-white/[0.05] border-dashed">
|
| 139 |
+
<button
|
| 140 |
+
className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-slate-700 dark:hover:text-slate-300 transition-colors"
|
| 141 |
onClick={() => setText("")}
|
| 142 |
>
|
| 143 |
Clear Text
|
|
|
|
| 146 |
</div>
|
| 147 |
|
| 148 |
{/* RIGHT PANE: OUTPUT */}
|
| 149 |
+
<div className="p-4 md:p-5 flex flex-col relative border-t lg:border-t-0 lg:border-l border-slate-100 dark:border-white/[0.05] bg-slate-50/50 dark:bg-white/[0.01] rounded-b-2xl lg:rounded-bl-none lg:rounded-tr-2xl">
|
| 150 |
+
<div className="flex items-center justify-between border-b border-orange-300/40 dark:border-orange-400/20 pb-2.5 mb-3">
|
| 151 |
+
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-orange-500 dark:text-orange-400">Generated Output · {modelChoice.replace(/_/g, ' ').toUpperCase()}</h3>
|
| 152 |
{summary ? (
|
| 153 |
+
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-orange-500/20 text-[9px] font-bold text-orange-400 border border-orange-500/30">
|
| 154 |
{summaryWordCount}
|
| 155 |
</span>
|
| 156 |
) : null}
|
| 157 |
</div>
|
| 158 |
+
<div className="flex flex-col custom-scroll">
|
| 159 |
{summary ? (
|
| 160 |
<>
|
| 161 |
+
<p className="text-[15px] leading-[1.85] text-slate-800 dark:text-slate-100 whitespace-pre-wrap">{summary.replace(/<n>/gi, '\n\n').replace(/[ \t]+/g, ' ').trim()}</p>
|
| 162 |
{extractedTags.length > 0 && (
|
| 163 |
+
<div className="mt-3 flex flex-wrap gap-1.5">
|
| 164 |
{extractedTags.map((tag, idx) => (
|
| 165 |
+
<span key={idx} className="inline-flex items-center rounded-full border border-orange-500/20 bg-orange-500/5 px-2.5 py-0.5 text-[10px] font-bold text-orange-400 capitalize tracking-wide">
|
| 166 |
{tag}
|
| 167 |
</span>
|
| 168 |
))}
|
|
|
|
| 170 |
)}
|
| 171 |
</>
|
| 172 |
) : (
|
| 173 |
+
<div className="h-full flex-1 flex items-center justify-center text-sm font-medium text-slate-600 italic">
|
| 174 |
{loading ? "Generating summary..." : "No summary generated yet."}
|
| 175 |
</div>
|
| 176 |
)}
|
| 177 |
</div>
|
| 178 |
{summary && (
|
| 179 |
+
<div className="mt-5 flex flex-wrap gap-3 border-t border-white/5 pt-4">
|
| 180 |
<button
|
| 181 |
onClick={handleCopy}
|
| 182 |
+
className="flex-1 flex justify-center items-center gap-2 rounded-xl bg-white/8 hover:bg-white/12 py-3 text-sm font-bold text-white transition border border-white/10"
|
| 183 |
>
|
| 184 |
+
{copied ? <CheckCircle2 size={16}/> : <Copy size={16} />} {copied ? "Copied" : "Copy Summary"}
|
| 185 |
</button>
|
| 186 |
<button
|
| 187 |
onClick={() => downloadTextFile(`summary_${modelChoice}.txt`, summary)}
|
| 188 |
+
className="flex-1 flex justify-center items-center gap-2 rounded-xl border border-white/10 bg-transparent py-3 text-sm font-bold text-slate-300 transition hover:bg-white/5"
|
| 189 |
>
|
| 190 |
<Download size={16} /> Save
|
| 191 |
</button>
|
|
|
|
| 195 |
</div>
|
| 196 |
|
| 197 |
{/* Separator */}
|
| 198 |
+
<div className="h-px w-full bg-slate-100 dark:bg-white/[0.05]"></div>
|
| 199 |
|
| 200 |
{/* Controls block */}
|
| 201 |
+
<div className="p-4 md:p-6 space-y-5">
|
| 202 |
{/* Model Selection */}
|
| 203 |
<div className="space-y-4">
|
| 204 |
+
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-widest">Select Model</h3>
|
|
|
|
|
|
|
| 205 |
|
| 206 |
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
| 207 |
{MODELS.map((model) => {
|
| 208 |
const Icon = model.icon;
|
| 209 |
const isSelected = modelChoice === model.id;
|
| 210 |
+
const cardCls = isSelected
|
| 211 |
+
? 'ring-2 ring-orange-500 bg-orange-50 dark:bg-orange-500/[0.08] border-orange-400 dark:border-orange-500/40'
|
| 212 |
+
: 'bg-slate-50 dark:bg-white/[0.03] hover:bg-slate-100 dark:hover:bg-white/[0.06] border-slate-200 dark:border-white/[0.08] hover:border-slate-300 dark:hover:border-white/15';
|
| 213 |
|
| 214 |
return (
|
| 215 |
<button
|
| 216 |
key={model.id}
|
| 217 |
onClick={() => setModelChoice(model.id)}
|
| 218 |
+
className={`relative flex flex-col items-start rounded-xl border p-4 text-left transition-all ${cardCls}`}
|
| 219 |
>
|
| 220 |
{isSelected && <div className="absolute top-4 right-4 h-2 w-2 rounded-full bg-orange-500 shadow-[0_0_8px_rgba(249,115,22,0.6)]" />}
|
| 221 |
<div className="flex w-full items-start justify-between mb-4">
|
|
|
|
| 231 |
</div>
|
| 232 |
</div>
|
| 233 |
</div>
|
| 234 |
+
<h4 className="text-base font-bold text-white mb-1.5">{model.name}</h4>
|
| 235 |
+
<span className={`inline-block rounded-full border px-2 py-0.5 text-[9px] font-bold uppercase tracking-wider ${model.badgeColor}`}>
|
| 236 |
{model.badgeLabel}
|
| 237 |
</span>
|
| 238 |
+
<div className="mt-4 flex w-full items-center justify-between">
|
| 239 |
+
<span className="text-[10px] font-semibold text-slate-600">Speed</span>
|
|
|
|
| 240 |
<div className="flex gap-1">
|
| 241 |
{[1, 2, 3].map((i) => (
|
| 242 |
+
<div key={i} className={`h-1.5 w-5 rounded-full ${i <= model.speed ? 'bg-orange-500' : 'bg-white/10'}`} />
|
| 243 |
))}
|
| 244 |
</div>
|
| 245 |
</div>
|
|
|
|
| 249 |
</div>
|
| 250 |
</div>
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
{/* Button */}
|
| 254 |
+
<div className="flex gap-4">
|
| 255 |
<button
|
| 256 |
onClick={onSummarize}
|
| 257 |
disabled={loading}
|
| 258 |
+
className="group flex-1 flex items-center justify-center gap-3 rounded-xl bg-gradient-to-r from-orange-500 to-orange-600 py-4 text-base font-bold text-white shadow-lg shadow-orange-500/25 transition hover:from-orange-400 hover:to-orange-500 focus:ring-4 focus:ring-orange-500/30 disabled:opacity-50"
|
| 259 |
>
|
| 260 |
{loading ? "Generating Output..." : "Summarize Now"}
|
| 261 |
+
{!loading && <ArrowRight size={18} className="text-white/80 group-hover:translate-x-0.5 transition-transform" />}
|
| 262 |
</button>
|
| 263 |
</div>
|
| 264 |
</div>
|
frontend/src/index.css
CHANGED
|
@@ -2,26 +2,71 @@
|
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
| 4 |
|
|
|
|
|
|
|
| 5 |
:root {
|
| 6 |
-
color-scheme: light;
|
| 7 |
-
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 8 |
}
|
| 9 |
|
| 10 |
body {
|
| 11 |
margin: 0;
|
| 12 |
min-width: 320px;
|
| 13 |
min-height: 100vh;
|
| 14 |
-
color: #0f172a;
|
| 15 |
-
background: #eef4ff;
|
| 16 |
}
|
| 17 |
|
| 18 |
* {
|
| 19 |
box-sizing: border-box;
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
.line-clamp-5 {
|
| 23 |
display: -webkit-box;
|
| 24 |
-webkit-line-clamp: 5;
|
| 25 |
-webkit-box-orient: vertical;
|
| 26 |
overflow: hidden;
|
| 27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
| 4 |
|
| 5 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');
|
| 6 |
+
|
| 7 |
:root {
|
| 8 |
+
color-scheme: light dark;
|
| 9 |
+
font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 10 |
}
|
| 11 |
|
| 12 |
body {
|
| 13 |
margin: 0;
|
| 14 |
min-width: 320px;
|
| 15 |
min-height: 100vh;
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
* {
|
| 19 |
box-sizing: border-box;
|
| 20 |
}
|
| 21 |
|
| 22 |
+
/* Scrollbar */
|
| 23 |
+
.custom-scroll::-webkit-scrollbar {
|
| 24 |
+
width: 4px;
|
| 25 |
+
}
|
| 26 |
+
.custom-scroll::-webkit-scrollbar-track {
|
| 27 |
+
background: transparent;
|
| 28 |
+
}
|
| 29 |
+
.custom-scroll::-webkit-scrollbar-thumb {
|
| 30 |
+
background: #334155;
|
| 31 |
+
border-radius: 99px;
|
| 32 |
+
}
|
| 33 |
+
.custom-scroll::-webkit-scrollbar-thumb:hover {
|
| 34 |
+
background: #f97316;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Line clamp utilities */
|
| 38 |
+
.line-clamp-2 {
|
| 39 |
+
display: -webkit-box;
|
| 40 |
+
-webkit-line-clamp: 2;
|
| 41 |
+
-webkit-box-orient: vertical;
|
| 42 |
+
overflow: hidden;
|
| 43 |
+
}
|
| 44 |
+
.line-clamp-3 {
|
| 45 |
+
display: -webkit-box;
|
| 46 |
+
-webkit-line-clamp: 3;
|
| 47 |
+
-webkit-box-orient: vertical;
|
| 48 |
+
overflow: hidden;
|
| 49 |
+
}
|
| 50 |
.line-clamp-5 {
|
| 51 |
display: -webkit-box;
|
| 52 |
-webkit-line-clamp: 5;
|
| 53 |
-webkit-box-orient: vertical;
|
| 54 |
overflow: hidden;
|
| 55 |
}
|
| 56 |
+
|
| 57 |
+
/* Background grid pattern */
|
| 58 |
+
.bg-grid {
|
| 59 |
+
background-image:
|
| 60 |
+
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
|
| 61 |
+
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
|
| 62 |
+
background-size: 40px 40px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
/* Glowing accent dot */
|
| 66 |
+
@keyframes pulse-glow {
|
| 67 |
+
0%, 100% { box-shadow: 0 0 4px 2px rgba(249,115,22,0.4); }
|
| 68 |
+
50% { box-shadow: 0 0 12px 4px rgba(249,115,22,0.7); }
|
| 69 |
+
}
|
| 70 |
+
.dot-glow {
|
| 71 |
+
animation: pulse-glow 2s ease-in-out infinite;
|
| 72 |
+
}
|
frontend/src/pages/Home.jsx
CHANGED
|
@@ -2,9 +2,10 @@ import { useEffect, useRef, useState } from "react";
|
|
| 2 |
import { fetchSamples, summarizeText } from "../api/client";
|
| 3 |
import BatchUpload from "../components/BatchUpload";
|
| 4 |
import DatasetToggle from "../components/DatasetToggle";
|
|
|
|
| 5 |
import SampleGallery from "../components/SampleGallery";
|
| 6 |
import SummarizerWidget, { MODELS } from "../components/SummarizerWidget";
|
| 7 |
-
import { Moon, Sun } from "lucide-react";
|
| 8 |
|
| 9 |
const FALLBACK_TEXT = {
|
| 10 |
gcc: "A rear-end collision involving two vehicles was recorded on Sheikh Zayed Road in Dubai during the evening peak. The incident caused a temporary lane closure, minor injuries, and congestion extending into the surrounding corridor while responders managed the scene.",
|
|
@@ -16,29 +17,21 @@ export default function Home() {
|
|
| 16 |
const [datasetTrack, setDatasetTrack] = useState("gcc");
|
| 17 |
const [text, setText] = useState(FALLBACK_TEXT.gcc);
|
| 18 |
const [modelChoice, setModelChoice] = useState("bart_large_cnn");
|
| 19 |
-
const [maxLength, setMaxLength] = useState(150);
|
| 20 |
const [summary, setSummary] = useState("");
|
| 21 |
const [samples, setSamples] = useState([]);
|
| 22 |
const [loading, setLoading] = useState(false);
|
| 23 |
|
| 24 |
-
// Refs ensure that the latest state values are available to the async runSummarize function,
|
| 25 |
-
// preventing "stale closures" where the function uses old scale values.
|
| 26 |
const textRef = useRef(text);
|
| 27 |
-
const maxLengthRef = useRef(maxLength);
|
| 28 |
const datasetTrackRef = useRef(datasetTrack);
|
| 29 |
const modelChoiceRef = useRef(modelChoice);
|
| 30 |
|
| 31 |
useEffect(() => { textRef.current = text; }, [text]);
|
| 32 |
-
useEffect(() => { maxLengthRef.current = maxLength; }, [maxLength]);
|
| 33 |
useEffect(() => { datasetTrackRef.current = datasetTrack; }, [datasetTrack]);
|
| 34 |
useEffect(() => { modelChoiceRef.current = modelChoice; }, [modelChoice]);
|
| 35 |
|
| 36 |
useEffect(() => {
|
| 37 |
-
if (isDark)
|
| 38 |
-
|
| 39 |
-
} else {
|
| 40 |
-
document.documentElement.classList.remove("dark");
|
| 41 |
-
}
|
| 42 |
}, [isDark]);
|
| 43 |
|
| 44 |
useEffect(() => {
|
|
@@ -59,23 +52,14 @@ export default function Home() {
|
|
| 59 |
}, [datasetTrack]);
|
| 60 |
|
| 61 |
const runSummarize = async (targetModelId) => {
|
| 62 |
-
// Determine which model to use: either the passed one (for direct clicks) or the state (for slider/button)
|
| 63 |
const modelToUse = targetModelId || modelChoiceRef.current;
|
| 64 |
const currentText = textRef.current;
|
| 65 |
-
const currentLength = maxLengthRef.current;
|
| 66 |
const currentTrack = datasetTrackRef.current;
|
| 67 |
-
|
| 68 |
if (!currentText || currentText.trim().length < 10) return;
|
| 69 |
-
|
| 70 |
setLoading(true);
|
| 71 |
setSummary("");
|
| 72 |
try {
|
| 73 |
-
const data = await summarizeText({
|
| 74 |
-
text: currentText,
|
| 75 |
-
model_choice: modelToUse,
|
| 76 |
-
max_length: currentLength,
|
| 77 |
-
dataset_track: currentTrack
|
| 78 |
-
});
|
| 79 |
setSummary(data.summary);
|
| 80 |
} catch (error) {
|
| 81 |
setSummary(`Error: ${error?.response?.data?.detail || error.message}`);
|
|
@@ -84,67 +68,82 @@ export default function Home() {
|
|
| 84 |
}
|
| 85 |
};
|
| 86 |
|
| 87 |
-
|
| 88 |
-
const
|
| 89 |
-
if (overrideLength !== null) setMaxLength(overrideLength);
|
| 90 |
-
runSummarize();
|
| 91 |
-
};
|
| 92 |
-
|
| 93 |
-
// Model selection trigger
|
| 94 |
-
const handleModelSelect = (modelId) => {
|
| 95 |
-
setModelChoice(modelId);
|
| 96 |
-
runSummarize(modelId);
|
| 97 |
-
};
|
| 98 |
|
| 99 |
return (
|
| 100 |
-
<div className="min-h-screen bg-slate-50
|
| 101 |
-
|
| 102 |
{/* Navbar */}
|
| 103 |
-
<header className="flex h-16 items-center justify-between px-
|
| 104 |
-
<div className="flex items-center gap-
|
| 105 |
-
|
| 106 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
</div>
|
| 108 |
-
<button
|
| 109 |
-
onClick={() => setIsDark(!isDark)}
|
| 110 |
-
className="p-2 rounded-full bg-slate-100 hover:bg-slate-200 text-slate-600 transition dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-400"
|
| 111 |
-
>
|
| 112 |
-
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
| 113 |
-
</button>
|
| 114 |
</header>
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
| 132 |
</div>
|
| 133 |
-
<div className="
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
|
| 138 |
-
{/*
|
| 139 |
-
<div className="grid grid-cols-1 xl:grid-cols-
|
| 140 |
-
|
| 141 |
-
{/* Main Summarizer
|
| 142 |
-
<div className="
|
| 143 |
-
<SummarizerWidget
|
| 144 |
text={text}
|
| 145 |
setText={setText}
|
| 146 |
-
maxLength={maxLength}
|
| 147 |
-
setMaxLength={setMaxLength}
|
| 148 |
modelChoice={modelChoice}
|
| 149 |
setModelChoice={handleModelSelect}
|
| 150 |
onSummarize={handleSummarize}
|
|
@@ -153,19 +152,26 @@ export default function Home() {
|
|
| 153 |
/>
|
| 154 |
</div>
|
| 155 |
|
| 156 |
-
{/*
|
| 157 |
-
<div className="
|
| 158 |
-
<div className="rounded-
|
| 159 |
-
<div className="
|
| 160 |
-
<
|
| 161 |
-
<
|
|
|
|
|
|
|
| 162 |
</div>
|
| 163 |
-
<div className="overflow-y-auto
|
| 164 |
<SampleGallery items={samples} onPick={setText} />
|
| 165 |
</div>
|
| 166 |
</div>
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
<BatchUpload />
|
|
|
|
| 169 |
</div>
|
| 170 |
|
| 171 |
</div>
|
|
|
|
| 2 |
import { fetchSamples, summarizeText } from "../api/client";
|
| 3 |
import BatchUpload from "../components/BatchUpload";
|
| 4 |
import DatasetToggle from "../components/DatasetToggle";
|
| 5 |
+
import LiveMetrics from "../components/LiveMetrics";
|
| 6 |
import SampleGallery from "../components/SampleGallery";
|
| 7 |
import SummarizerWidget, { MODELS } from "../components/SummarizerWidget";
|
| 8 |
+
import { Moon, Sun, Zap } from "lucide-react";
|
| 9 |
|
| 10 |
const FALLBACK_TEXT = {
|
| 11 |
gcc: "A rear-end collision involving two vehicles was recorded on Sheikh Zayed Road in Dubai during the evening peak. The incident caused a temporary lane closure, minor injuries, and congestion extending into the surrounding corridor while responders managed the scene.",
|
|
|
|
| 17 |
const [datasetTrack, setDatasetTrack] = useState("gcc");
|
| 18 |
const [text, setText] = useState(FALLBACK_TEXT.gcc);
|
| 19 |
const [modelChoice, setModelChoice] = useState("bart_large_cnn");
|
|
|
|
| 20 |
const [summary, setSummary] = useState("");
|
| 21 |
const [samples, setSamples] = useState([]);
|
| 22 |
const [loading, setLoading] = useState(false);
|
| 23 |
|
|
|
|
|
|
|
| 24 |
const textRef = useRef(text);
|
|
|
|
| 25 |
const datasetTrackRef = useRef(datasetTrack);
|
| 26 |
const modelChoiceRef = useRef(modelChoice);
|
| 27 |
|
| 28 |
useEffect(() => { textRef.current = text; }, [text]);
|
|
|
|
| 29 |
useEffect(() => { datasetTrackRef.current = datasetTrack; }, [datasetTrack]);
|
| 30 |
useEffect(() => { modelChoiceRef.current = modelChoice; }, [modelChoice]);
|
| 31 |
|
| 32 |
useEffect(() => {
|
| 33 |
+
if (isDark) document.documentElement.classList.add("dark");
|
| 34 |
+
else document.documentElement.classList.remove("dark");
|
|
|
|
|
|
|
|
|
|
| 35 |
}, [isDark]);
|
| 36 |
|
| 37 |
useEffect(() => {
|
|
|
|
| 52 |
}, [datasetTrack]);
|
| 53 |
|
| 54 |
const runSummarize = async (targetModelId) => {
|
|
|
|
| 55 |
const modelToUse = targetModelId || modelChoiceRef.current;
|
| 56 |
const currentText = textRef.current;
|
|
|
|
| 57 |
const currentTrack = datasetTrackRef.current;
|
|
|
|
| 58 |
if (!currentText || currentText.trim().length < 10) return;
|
|
|
|
| 59 |
setLoading(true);
|
| 60 |
setSummary("");
|
| 61 |
try {
|
| 62 |
+
const data = await summarizeText({ text: currentText, model_choice: modelToUse, dataset_track: currentTrack });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
setSummary(data.summary);
|
| 64 |
} catch (error) {
|
| 65 |
setSummary(`Error: ${error?.response?.data?.detail || error.message}`);
|
|
|
|
| 68 |
}
|
| 69 |
};
|
| 70 |
|
| 71 |
+
const handleSummarize = () => runSummarize();
|
| 72 |
+
const handleModelSelect = (modelId) => { setModelChoice(modelId); runSummarize(modelId); };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
return (
|
| 75 |
+
<div className="min-h-screen bg-slate-50 dark:bg-[#060d1f] text-slate-900 dark:text-slate-200 transition-colors duration-300 pb-16 dark:bg-grid">
|
| 76 |
+
|
| 77 |
{/* Navbar */}
|
| 78 |
+
<header className="flex h-16 items-center justify-between px-8 border-b border-black/10 dark:border-white/5 bg-white/80 dark:bg-[#060d1f]/80 backdrop-blur-xl sticky top-0 z-40">
|
| 79 |
+
<div className="flex items-center gap-3">
|
| 80 |
+
<div className="relative h-8 w-8 flex items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 shadow-lg shadow-orange-500/30">
|
| 81 |
+
<Zap size={16} className="text-white" fill="white" />
|
| 82 |
+
</div>
|
| 83 |
+
<div className="flex items-baseline gap-0.5">
|
| 84 |
+
<span className="font-black text-base tracking-tight text-slate-900 dark:text-white uppercase">Traffic</span>
|
| 85 |
+
<span className="text-orange-400 font-black text-base uppercase">Intel</span>
|
| 86 |
+
</div>
|
| 87 |
+
<span className="hidden sm:inline-flex ml-2 text-[10px] font-bold tracking-widest uppercase px-2.5 py-1 rounded-full border border-orange-500/30 bg-orange-500/10 text-orange-400">
|
| 88 |
+
LLM Summarization Demo
|
| 89 |
+
</span>
|
| 90 |
+
</div>
|
| 91 |
+
<div className="flex items-center gap-3">
|
| 92 |
+
<span className="hidden md:flex items-center gap-1.5 text-xs font-medium text-slate-500">
|
| 93 |
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 dot-glow"></span>
|
| 94 |
+
Live Backend
|
| 95 |
+
</span>
|
| 96 |
+
<button
|
| 97 |
+
onClick={() => setIsDark(!isDark)}
|
| 98 |
+
className="p-2 rounded-full bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition border border-black/10 dark:border-white/10"
|
| 99 |
+
>
|
| 100 |
+
{isDark ? <Sun size={16} /> : <Moon size={16} />}
|
| 101 |
+
</button>
|
| 102 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
</header>
|
| 104 |
|
| 105 |
+
{/* Main Content */}
|
| 106 |
+
<div className="w-full max-w-[1920px] mx-auto px-6 xl:px-10 pt-8">
|
| 107 |
+
|
| 108 |
+
{/* Hero Banner */}
|
| 109 |
+
<div className="mb-8 flex flex-col sm:flex-row sm:items-end sm:justify-between gap-4">
|
| 110 |
+
<div className="space-y-3">
|
| 111 |
+
<span className="inline-flex items-center gap-2 rounded-full border border-orange-500/30 bg-orange-500/10 px-4 py-1.5 text-xs font-bold uppercase tracking-widest text-orange-400">
|
| 112 |
+
<span className="h-1.5 w-1.5 rounded-full bg-orange-500 dot-glow"></span>
|
| 113 |
+
CUD · AAI Midterm Project
|
| 114 |
+
</span>
|
| 115 |
+
<h1 className="text-4xl lg:text-5xl xl:text-6xl font-black tracking-tight text-slate-900 dark:text-white leading-none">
|
| 116 |
+
Turn Traffic Chaos{" "}
|
| 117 |
+
<span className="text-transparent bg-clip-text bg-gradient-to-r from-orange-400 via-orange-500 to-amber-400">
|
| 118 |
+
into Clarity
|
| 119 |
+
</span>
|
| 120 |
+
</h1>
|
| 121 |
+
<p className="text-sm text-slate-500 max-w-xl font-medium leading-relaxed">
|
| 122 |
+
Compare extractive & abstractive LLM summarization methods on real-world traffic incident data from GCC and US datasets.
|
| 123 |
+
</p>
|
| 124 |
</div>
|
| 125 |
+
<div className="flex gap-6 shrink-0">
|
| 126 |
+
{[
|
| 127 |
+
{ label: "Models", value: "4" },
|
| 128 |
+
{ label: "GCC Samples", value: "250+" },
|
| 129 |
+
{ label: "US Records", value: "5K+" }
|
| 130 |
+
].map(s => (
|
| 131 |
+
<div key={s.label} className="text-center">
|
| 132 |
+
<div className="text-2xl font-black text-white">{s.value}</div>
|
| 133 |
+
<div className="text-[10px] uppercase tracking-widest font-bold text-slate-500">{s.label}</div>
|
| 134 |
+
</div>
|
| 135 |
+
))}
|
| 136 |
</div>
|
| 137 |
</div>
|
| 138 |
|
| 139 |
+
{/* 3-Column Main Grid */}
|
| 140 |
+
<div className="grid grid-cols-1 xl:grid-cols-[1fr_340px_300px] gap-5 items-stretch">
|
| 141 |
+
|
| 142 |
+
{/* Column 1: Main Summarizer Widget */}
|
| 143 |
+
<div className="h-full min-h-0">
|
| 144 |
+
<SummarizerWidget
|
| 145 |
text={text}
|
| 146 |
setText={setText}
|
|
|
|
|
|
|
| 147 |
modelChoice={modelChoice}
|
| 148 |
setModelChoice={handleModelSelect}
|
| 149 |
onSummarize={handleSummarize}
|
|
|
|
| 152 |
/>
|
| 153 |
</div>
|
| 154 |
|
| 155 |
+
{/* Column 2: Dataset Preview */}
|
| 156 |
+
<div className="h-full flex flex-col min-h-0">
|
| 157 |
+
<div className="rounded-2xl border border-white/[0.06] bg-[#0d1326] shadow-2xl flex flex-col flex-1 overflow-hidden">
|
| 158 |
+
<div className="flex items-center justify-between px-5 pt-5 pb-3 border-b border-white/5">
|
| 159 |
+
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-slate-400">Dataset Preview</h3>
|
| 160 |
+
<span className="rounded-full bg-white/5 px-3 py-1 text-[11px] font-bold text-slate-400 border border-white/8">
|
| 161 |
+
{samples.length} Samples
|
| 162 |
+
</span>
|
| 163 |
</div>
|
| 164 |
+
<div className="overflow-y-auto flex-1 px-3 py-3 space-y-2 custom-scroll">
|
| 165 |
<SampleGallery items={samples} onPick={setText} />
|
| 166 |
</div>
|
| 167 |
</div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
{/* Column 3: Controls */}
|
| 171 |
+
<div className="flex flex-col gap-5 h-full">
|
| 172 |
+
<DatasetToggle value={datasetTrack} onChange={setDatasetTrack} />
|
| 173 |
<BatchUpload />
|
| 174 |
+
<LiveMetrics activeModel={modelChoice} />
|
| 175 |
</div>
|
| 176 |
|
| 177 |
</div>
|
src/models/abstractive.py
CHANGED
|
@@ -50,7 +50,7 @@ def generate_summary(text: str, model_name: str, config_path: str = "config.yaml
|
|
| 50 |
encoded = tokenizer(source_text, truncation=True, max_length=gen.max_input_tokens, return_tensors="pt")
|
| 51 |
encoded = {k: v.to(get_device()) for k, v in encoded.items()}
|
| 52 |
actual_max_tokens = max_new_tokens or gen.max_new_tokens
|
| 53 |
-
actual_min_tokens =
|
| 54 |
|
| 55 |
with torch.inference_mode():
|
| 56 |
output_ids = model.generate(
|
|
@@ -58,11 +58,22 @@ def generate_summary(text: str, model_name: str, config_path: str = "config.yaml
|
|
| 58 |
min_new_tokens=actual_min_tokens,
|
| 59 |
max_new_tokens=actual_max_tokens,
|
| 60 |
num_beams=gen.num_beams,
|
| 61 |
-
length_penalty=gen.length_penalty
|
| 62 |
no_repeat_ngram_size=gen.no_repeat_ngram_size,
|
| 63 |
early_stopping=bool(gen.early_stopping),
|
| 64 |
)
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
def available_abstractive_models(config_path: str = "config.yaml") -> List[str]:
|
| 68 |
cfg = load_config(config_path)
|
|
|
|
| 50 |
encoded = tokenizer(source_text, truncation=True, max_length=gen.max_input_tokens, return_tensors="pt")
|
| 51 |
encoded = {k: v.to(get_device()) for k, v in encoded.items()}
|
| 52 |
actual_max_tokens = max_new_tokens or gen.max_new_tokens
|
| 53 |
+
actual_min_tokens = gen.min_new_tokens
|
| 54 |
|
| 55 |
with torch.inference_mode():
|
| 56 |
output_ids = model.generate(
|
|
|
|
| 58 |
min_new_tokens=actual_min_tokens,
|
| 59 |
max_new_tokens=actual_max_tokens,
|
| 60 |
num_beams=gen.num_beams,
|
| 61 |
+
length_penalty=gen.length_penalty,
|
| 62 |
no_repeat_ngram_size=gen.no_repeat_ngram_size,
|
| 63 |
early_stopping=bool(gen.early_stopping),
|
| 64 |
)
|
| 65 |
+
output_text = " ".join(tokenizer.decode(output_ids[0], skip_special_tokens=True).split())
|
| 66 |
+
# Post-processing filters for common model hallucinations
|
| 67 |
+
hallucinations = [
|
| 68 |
+
"For confidential support call the Samaritans in the UK on 08457 90 90 90, visit a local Samaritans branch or click here for details.",
|
| 69 |
+
"For confidential support call the Samaritans",
|
| 70 |
+
"The cause of the collision has not been determined",
|
| 71 |
+
"The incident is under investigation by Dubai Police.",
|
| 72 |
+
"The incident is currently under investigation and no further details have been released."
|
| 73 |
+
]
|
| 74 |
+
for h in hallucinations:
|
| 75 |
+
output_text = output_text.replace(h, "").strip()
|
| 76 |
+
return " ".join(output_text.split())
|
| 77 |
|
| 78 |
def available_abstractive_models(config_path: str = "config.yaml") -> List[str]:
|
| 79 |
cfg = load_config(config_path)
|