Spaces:
Paused
Paused
| import os | |
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| import re | |
| import plotly.express as px | |
| from huggingface_hub import InferenceClient | |
| def load_data(file_obj): | |
| """Safely loads CSV, Excel, or TXT file into a Pandas DataFrame.""" | |
| if file_obj is None: | |
| return None, gr.update(choices=[], visible=False), "Please upload a file." | |
| file_path = file_obj.name | |
| ext = os.path.splitext(file_path)[1].lower() | |
| try: | |
| if ext == '.csv': | |
| df = pd.read_csv(file_path) | |
| elif ext in ['.xls', '.xlsx']: | |
| df = pd.read_excel(file_path) | |
| elif ext == '.txt': | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| df = pd.DataFrame({'text': [content]}) | |
| else: | |
| return None, gr.update(choices=[], visible=False), "Unsupported file format. Please upload .csv, .xlsx, or .txt." | |
| string_cols = [col for col in df.columns if df[col].dtype == 'object' or df[col].astype(str).str.len().mean() > 5] | |
| if not string_cols: | |
| string_cols = list(df.columns) | |
| return df, gr.update(choices=string_cols, value=string_cols[0], visible=True), f"Successfully loaded dataset with {len(df)} rows." | |
| except Exception as e: | |
| return None, gr.update(choices=[], visible=False), f"Error loading file: {str(e)}" | |
| # A rigorous dictionary of English rhetorical discourse markers | |
| RHETORICAL_MARKERS = { | |
| "Contrast (Counterargument)": [ | |
| "however", "but", "yet", "nevertheless", "nonetheless", "on the other hand", | |
| "although", "though", "even though", "conversely", "meanwhile", "in contrast", | |
| "instead", "whereas", "despite", "in spite of", "alternatively" | |
| ], | |
| "Causation (Cause/Effect)": [ | |
| "because", "therefore", "since", "consequently", "as a result", "thus", | |
| "so", "hence", "accordingly", "because of", "due to", "leads to", | |
| "thereby", "for this reason", "so that", "if" | |
| ], | |
| "Addition (Elaboration)": [ | |
| "furthermore", "in addition", "moreover", "besides", "also", "additionally", | |
| "further", "not only", "firstly", "secondly", "finally", "next", | |
| "what is more", "indeed", "similarly", "likewise", "for example", "for instance" | |
| ], | |
| "Conclusion (Synthesis)": [ | |
| "overall", "to conclude", "in conclusion", "summarize", "in summary", | |
| "ultimately", "essentially", "in short", "all in all", "briefly", "concluding" | |
| ] | |
| } | |
| def get_highlighted_tokens(text, matches): | |
| """Helper to highlight recognized discourse connectors in Gradio.""" | |
| # matches: list of dicts: {"start": int, "end": int, "label": str} | |
| matches = sorted(matches, key=lambda x: x["start"]) | |
| highlighted = [] | |
| last_idx = 0 | |
| for m in matches: | |
| start, end, label = m["start"], m["end"], m["label"] | |
| if start < last_idx: | |
| continue | |
| if start > last_idx: | |
| highlighted.append((text[last_idx:start], None)) | |
| highlighted.append((text[start:end], label)) | |
| last_idx = end | |
| if last_idx < len(text): | |
| highlighted.append((text[last_idx:], None)) | |
| return highlighted | |
| def run_local_discourse(text): | |
| """Rule-based local parser extracting exact discourse markers and categorizing rhetorical moves.""" | |
| matches = [] | |
| # We iterate over every connector and find matches using boundaries | |
| for category, markers in RHETORICAL_MARKERS.items(): | |
| for marker in markers: | |
| # We match markers as exact words/phrases | |
| pattern = re.compile(r'\b' + re.escape(marker) + r'\b', re.IGNORECASE) | |
| for m in pattern.finditer(text): | |
| matches.append({ | |
| "start": m.start(), | |
| "end": m.end(), | |
| "marker": m.group(), | |
| "label": category | |
| }) | |
| # Sort matches and filter out overlapping indexes | |
| matches = sorted(matches, key=lambda x: x["start"]) | |
| clean_matches = [] | |
| last_end = 0 | |
| for m in matches: | |
| if m["start"] >= last_end: | |
| clean_matches.append(m) | |
| last_end = m["end"] | |
| # Format to table | |
| results = [] | |
| sentences = re.split(r'(?<=[.!?])\s+', text) | |
| for m in clean_matches: | |
| # Find which sentence contains this match for context | |
| marker_context = "" | |
| for sent in sentences: | |
| if m["marker"] in sent: | |
| marker_context = sent.strip() | |
| break | |
| results.append({ | |
| "Connector": m["marker"], | |
| "Rhetorical Category": m["label"], | |
| "Context Sentence": marker_context | |
| }) | |
| df_res = pd.DataFrame(results) | |
| # Format highlighted text | |
| highlighted = get_highlighted_tokens(text, [{"start": m["start"], "end": m["end"], "label": m["label"]} for m in clean_matches]) | |
| return df_res, highlighted | |
| def run_neural_discourse(text, hf_token, model_name): | |
| """Uses advanced generative instruction models to extract claim, evidence, and fallacy trees.""" | |
| if not hf_token: | |
| raise ValueError("Hugging Face API Token is required for Transformers mode.") | |
| client = InferenceClient(token=hf_token) | |
| prompt = f"""[INST] Analyze the argument structure, rhetorical patterns, and reasoning flow of this persuasive text. | |
| Identify the main CLAIM, the key pieces of EVIDENCE/premises, and list any LOGICAL FALLACIES detected. | |
| Keep the analysis highly structured, bulleted, and professional. | |
| Text to analyze: | |
| "{text}" [/INST]""" | |
| try: | |
| response = client.text_generation( | |
| prompt, | |
| model=model_name, | |
| max_new_tokens=600, | |
| temperature=0.3 | |
| ) | |
| return response | |
| except Exception as e: | |
| raise RuntimeError(f"Hugging Face API error: {str(e)}") | |
| def analyze_discourse(text_input, file_obj, text_col, method, hf_token, hf_model): | |
| docs = [] | |
| if file_obj is not None: | |
| df, _, _ = load_data(file_obj) | |
| if df is not None and text_col in df.columns: | |
| docs = df[text_col].astype(str).fillna("").tolist() | |
| elif text_input and text_input.strip(): | |
| docs = [text_input] | |
| if not docs: | |
| return None, None, None, "Please enter text or upload a valid dataset first." | |
| try: | |
| if method == "Local Cue-Based (CPU & Fast)": | |
| df_res, highlighted = run_local_discourse(docs[0]) | |
| if df_res.empty: | |
| return ( | |
| [("No rhetorical connectors detected in the text.", None)], | |
| pd.DataFrame(), | |
| None, | |
| "Finished analysis: No standard discourse markers were detected." | |
| ) | |
| # Plotly Pie Chart | |
| counts = df_res["Rhetorical Category"].value_counts().reset_index() | |
| counts.columns = ["Rhetorical Category", "Count"] | |
| fig = px.pie( | |
| counts, | |
| values="Count", | |
| names="Rhetorical Category", | |
| color="Rhetorical Category", | |
| title="Distribution of Rhetorical Moves", | |
| template="plotly_dark", | |
| color_discrete_sequence=px.colors.qualitative.Pastel | |
| ) | |
| fig.update_layout(height=350, margin=dict(l=20, r=20, t=40, b=20)) | |
| csv_path = "discourse_connectors_report.csv" | |
| df_res.to_csv(csv_path, index=False) | |
| return highlighted, df_res, fig, f"Analysis complete: Extracted **{len(df_res)}** rhetorical connectives." | |
| else: | |
| # Neural Mode | |
| raw_analysis = run_neural_discourse(docs[0], hf_token, hf_model) | |
| # Format neural output as text markdown | |
| # Return dummy table and chart for compatibility | |
| return [("See the Argument Tree & Logical Fallacies report in the text output.", None)], pd.DataFrame(), None, raw_analysis | |
| except Exception as e: | |
| return None, None, None, f"Execution failed: {str(e)}" | |
| custom_css = """ | |
| body { | |
| background-color: #0b0f19; | |
| color: #f3f4f6; | |
| } | |
| .gradio-container { | |
| font-family: 'Inter', sans-serif !important; | |
| } | |
| h1, h2 { | |
| color: #6366f1 !important; | |
| } | |
| """ | |
| with gr.Blocks(theme=gr.themes.Default(primary_hue="indigo", secondary_hue="slate"), css=custom_css) as demo: | |
| df_state = gr.State() | |
| gr.HTML(""" | |
| <div style="text-align: center; margin-bottom: 2rem;"> | |
| <h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; background: linear-gradient(to right, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Discourse & Rhetorical Analyzer</h1> | |
| <p style="font-size: 1.1rem; color: #94a3b8; max-width: 800px; margin: 0 auto;"> | |
| Deconstruct argument structures, track rhetorical connector networks, and detect logical fallacies. | |
| Evaluate local transition cues or unlock deep AI semantic trees using your personal Hugging Face Token. | |
| </p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 1. Upload Source Text") | |
| with gr.Tabs(): | |
| with gr.TabItem("Paste Raw Text"): | |
| text_input = gr.Textbox( | |
| label="Source Text", | |
| placeholder="Paste persuasive text, political speech, or academic draft here...", | |
| lines=12 | |
| ) | |
| with gr.TabItem("Upload Dataset File"): | |
| file_input = gr.File(label="Upload (.csv, .xlsx, .txt)", file_types=[".csv", ".xlsx", ".txt"]) | |
| text_column_selector = gr.Dropdown( | |
| label="Target Text Column", | |
| choices=[], | |
| visible=False, | |
| interactive=True | |
| ) | |
| status_text = gr.Markdown("No file uploaded yet.") | |
| gr.Markdown("### 2. Configure Model") | |
| method_selector = gr.Radio( | |
| choices=["Local Cue-Based (CPU & Fast)", "Transformers (AI Mode)"], | |
| value="Local Cue-Based (CPU & Fast)", | |
| label="Discourse Parser" | |
| ) | |
| with gr.Group() as token_group: | |
| hf_token_input = gr.Textbox( | |
| label="Hugging Face API Token", | |
| placeholder="hf_...", | |
| type="password", | |
| visible=False, | |
| info="Required to run claim/fallacy arguments extraction. Get one free at huggingface.co." | |
| ) | |
| hf_model_input = gr.Dropdown( | |
| choices=[ | |
| "Qwen/Qwen2.5-7B-Instruct", | |
| "meta-llama/Llama-3-8b-instruct", | |
| "mistralai/Mistral-7B-Instruct-v0.3" | |
| ], | |
| value="Qwen/Qwen2.5-7B-Instruct", | |
| label="Transformer Model (HF API)", | |
| visible=False | |
| ) | |
| run_btn = gr.Button("Analyze Discourse", variant="primary") | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 3. Argument Structure & Rhetorical Analysis") | |
| status_markdown = gr.Markdown("Enter text and click 'Analyze Discourse' to run.") | |
| with gr.Tabs(): | |
| with gr.TabItem("Transition Color-Highlighting"): | |
| highlighted_output = gr.HighlightedText( | |
| label="Rhetorical Connectives Highlight", | |
| combine_adjacent=False | |
| ) | |
| with gr.TabItem("Rhetorical Moves Table"): | |
| table_output = gr.Dataframe( | |
| headers=["Connector", "Rhetorical Category", "Context Sentence"], | |
| datatype=["str", "str", "str"], | |
| interactive=False, | |
| wrap=True | |
| ) | |
| with gr.TabItem("Rhetorical Moves Chart"): | |
| chart_output = gr.Plot(label="Discourse Moves Distribution") | |
| gr.Markdown("### 4. Export") | |
| download_csv = gr.File(label="Download Rhetorical Moves Report (CSV)") | |
| # Show/hide token field depending on model | |
| def toggle_method_fields(method): | |
| if method == "Transformers (AI Mode)": | |
| return gr.update(visible=True), gr.update(visible=True) | |
| else: | |
| return gr.update(visible=False), gr.update(visible=False) | |
| method_selector.change( | |
| fn=toggle_method_fields, | |
| inputs=method_selector, | |
| outputs=[hf_token_input, hf_model_input] | |
| ) | |
| file_input.change( | |
| fn=load_data, | |
| inputs=file_input, | |
| outputs=[df_state, text_column_selector, status_text] | |
| ) | |
| run_btn.click( | |
| fn=analyze_discourse, | |
| inputs=[text_input, file_input, text_column_selector, method_selector, hf_token_input, hf_model_input], | |
| outputs=[highlighted_output, table_output, chart_output, download_csv, status_markdown] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |