Spaces:
Sleeping
Sleeping
| import subprocess | |
| import sys as sys_mod | |
| try: | |
| import spacy | |
| spacy.load("en_core_web_sm") | |
| except OSError: | |
| print("Downloading spacy model...") | |
| subprocess.run([sys_mod.executable, "-m", "spacy", "download", "en_core_web_sm"], check=True) | |
| import streamlit as st | |
| import sys | |
| import json | |
| from pathlib import Path | |
| THIS_FILE = Path(__file__).resolve() | |
| PROJECT_ROOT = THIS_FILE.parent | |
| SRC_DIR = THIS_FILE.parent / "src" | |
| # Docker: backend is at /app/backend (same level as src) | |
| # Local: backend is at PROJECT_ROOT/backend | |
| if (PROJECT_ROOT / "backend").exists(): | |
| BACKEND_DIR = PROJECT_ROOT / "backend" | |
| elif (THIS_FILE.parent.parent / "backend").exists(): | |
| BACKEND_DIR = THIS_FILE.parent.parent / "backend" | |
| else: | |
| BACKEND_DIR = THIS_FILE.parent | |
| PIPELINE_DIR = BACKEND_DIR / "pipeline" | |
| new_path = [str(PROJECT_ROOT), str(BACKEND_DIR)] | |
| if PIPELINE_DIR.exists(): | |
| new_path.append(str(PIPELINE_DIR)) | |
| for p in new_path: | |
| if p in sys.path: | |
| sys.path.remove(p) | |
| sys.path.insert(0, p) | |
| import importlib | |
| for mod_name in ['backend', 'backend.pipeline', | |
| 'backend.pipeline.dictionaries', | |
| 'backend.pipeline.parser', | |
| 'backend.pipeline.scorer']: | |
| if mod_name in sys.modules: | |
| del sys.modules[mod_name] | |
| st.set_page_config( | |
| page_title="Syntactic Morality Analyzer", | |
| page_icon="X", | |
| layout="wide" | |
| ) | |
| def import_pipeline_module(module_filename, module_name): | |
| import importlib.util | |
| spec = importlib.util.spec_from_file_location( | |
| module_name, str(PIPELINE_DIR / module_filename) | |
| ) | |
| mod = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(mod) | |
| return mod | |
| def init_components(): | |
| dicts_mod = import_pipeline_module("dictionaries.py", "pipeline_dictionaries") | |
| parser_mod = import_pipeline_module("syntactic_parser.py", "pipeline_parser") | |
| scorer_mod = import_pipeline_module("scorer.py", "pipeline_scorer") | |
| DictionaryLoader = dicts_mod.DictionaryLoader | |
| SyntacticParser = parser_mod.SyntacticParser | |
| MoralScorer = scorer_mod.MoralScorer | |
| dict_loader = DictionaryLoader(str(BACKEND_DIR / "data")) | |
| dict_loader.load_all() | |
| parser = SyntacticParser() | |
| scorer = MoralScorer(dict_loader, parser) | |
| return dict_loader, parser, scorer | |
| def load_results(): | |
| results_file = BACKEND_DIR / "models" / "multi_dict_results.json" | |
| if results_file.exists(): | |
| with open(results_file) as f: | |
| return json.load(f) | |
| return None | |
| def main(): | |
| dict_loader, parser, scorer = init_components() | |
| st.title("Syntactic Morality Analyzer") | |
| st.markdown("**Extension to eMACDscore** (Malik et al., 2025)") | |
| st.markdown("Adds syntactic weighting to detect negation and grammatical roles.") | |
| st.sidebar.header("Settings") | |
| # All 5 dictionaries - code auto-creates placeholders if files missing | |
| dict_options = { | |
| "mfd": "MFD", | |
| "mfd2": "MFD 2.0", | |
| "emfd": "eMFD", | |
| "emacd": "eMACD", | |
| "macd": "MACD" | |
| } | |
| selected_dict = st.sidebar.selectbox( | |
| "Dictionary", | |
| list(dict_options.keys()), | |
| format_func=lambda x: dict_options[x] | |
| ) | |
| results = load_results() | |
| if results: | |
| st.sidebar.markdown("### Training Results (Macro F1)") | |
| for d, r in results.items(): | |
| b = round(r.get("baseline", {}).get("macro", 0), 3) | |
| s = round(r.get("syntax", {}).get("macro", 0), 3) | |
| diff = round(s - b, 3) | |
| st.sidebar.markdown(f"**{d}**: {b} -> {s} ({diff:+})") | |
| st.header("Input Text") | |
| text_input = st.text_area( | |
| "Enter text to analyze:", | |
| height=80, | |
| placeholder="e.g., I'm not caring about fairness" | |
| ) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| analyze_synx = st.button("Analyze with Syntax", type="primary", use_container_width=True) | |
| with col2: | |
| analyze_baseline = st.button("Analyze Baseline", use_container_width=True) | |
| if text_input and (analyze_synx or analyze_baseline): | |
| st.divider() | |
| st.header("Results") | |
| baseline_scores = scorer.score_baseline(text_input, selected_dict) | |
| syntax_scores = scorer.score(text_input, selected_dict) | |
| domains = dict_loader.get_domains(selected_dict) | |
| text_lower = text_input.lower() | |
| if analyze_baseline: | |
| st.subheader("Baseline (Keyword Only)") | |
| for domain in domains: | |
| score = baseline_scores.get(domain, 0) | |
| if score > 0: | |
| domain_words = dict_loader.get_words(selected_dict, domain) | |
| if isinstance(domain_words, dict): | |
| domain_words = list(domain_words.keys()) | |
| matched = [w for w in domain_words if w.lower() in text_lower] | |
| if matched: | |
| st.markdown(f"**{domain}**: {', '.join(matched)}") | |
| st.progress(float(score), text=f"Score: {score:.3f}") | |
| if analyze_synx: | |
| st.subheader("Syntax-Enhanced Results") | |
| for domain, score in syntax_scores.items(): | |
| delta = score - baseline_scores.get(domain, 0) | |
| st.progress(float(score), text=f"{domain}: {score:.3f} ({delta:+.3f})") | |
| st.subheader("Syntactic Breakdown") | |
| syntactic = parser.parse(text_input) | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.write("**Tokens:**", syntactic["tokens"]) | |
| with col2: | |
| st.write("**Subjects:**", [s["text"] for s in syntactic.get("subjects", [])]) | |
| with col3: | |
| st.write("**Objects:**", [o["text"] for o in syntactic.get("objects", [])]) | |
| if syntactic.get("negation_scopes"): | |
| st.warning("Negation detected! Keywords in negation scope have reduced scores.") | |
| if __name__ == "__main__": | |
| main() |