Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| import difflib | |
| import re | |
| def clean_and_tokenize(text): | |
| # Normalize whitespaces | |
| return re.findall(r'\b\w+\b|[^\w\s]', text) | |
| def calculate_drift_metrics(text_a, text_b): | |
| words_a = text_a.split() | |
| words_b = text_b.split() | |
| set_a = set(words_a) | |
| set_b = set(words_b) | |
| # Jaccard similarity | |
| union = len(set_a.union(set_b)) | |
| intersection = len(set_a.intersection(set_b)) | |
| jaccard_sim = (intersection / union) * 100 if union > 0 else 100.0 | |
| # SequenceMatcher | |
| matcher = difflib.SequenceMatcher(None, words_a, words_b) | |
| ratio = matcher.ratio() * 100 | |
| # Edits breakdown | |
| opcodes = matcher.get_opcodes() | |
| insertions = 0 | |
| deletions = 0 | |
| replacements = 0 | |
| for tag, i1, i2, j1, j2 in opcodes: | |
| if tag == 'insert': | |
| insertions += (j2 - j1) | |
| elif tag == 'delete': | |
| deletions += (i2 - i1) | |
| elif tag == 'replace': | |
| replacements += max(i2 - i1, j2 - j1) | |
| drift_percent = 100 - ratio | |
| return { | |
| "jaccard": round(jaccard_sim, 2), | |
| "similarity": round(ratio, 2), | |
| "drift": round(drift_percent, 2), | |
| "insertions": insertions, | |
| "deletions": deletions, | |
| "replacements": replacements, | |
| "words_a": len(words_a), | |
| "words_b": len(words_b) | |
| } | |
| def generate_word_diff(text_a, text_b): | |
| # Clean texts | |
| words_a = text_a.splitlines() | |
| words_b = text_b.splitlines() | |
| # We will generate a beautiful side-by-side or inline HTML diff | |
| # difflib.HtmlDiff is nice, but we want a highly premium custom HTML view | |
| diff = difflib.HtmlDiff(wrapcolumn=60) | |
| html_table = diff.make_table(words_a, words_b, context=True, numlines=3) | |
| # Style the table to fit our glowing dark theme | |
| styled_html = f""" | |
| <style> | |
| .diff_table {{ font-family: 'Inter', monospace; font-size: 13px; border-collapse: collapse; width: 100%; color: #e2e8f0; background: #0f172a; border-radius: 8px; overflow: hidden; }} | |
| .diff_header {{ background: #1e293b; color: #94a3b8; font-weight: 600; padding: 8px; border-bottom: 2px solid #334155; }} | |
| td.diff_header {{ text-align: right; background: #1e293b; color: #64748b; padding: 4px 8px; user-select: none; width: 40px; border-right: 1px solid #334155; }} | |
| .diff_next {{ display: none; }} | |
| .diff_add {{ background-color: rgba(16, 185, 129, 0.25) !important; color: #34d399 !important; text-decoration: none; }} | |
| .diff_chg {{ background-color: rgba(245, 158, 11, 0.25) !important; color: #fbbf24 !important; text-decoration: none; }} | |
| .diff_sub {{ background-color: rgba(239, 68, 68, 0.25) !important; color: #f87171 !important; text-decoration: line-through; }} | |
| td {{ padding: 4px 12px; word-break: break-all; white-space: pre-wrap; }} | |
| tr:hover td {{ background: #1e293b; }} | |
| </style> | |
| <div style="overflow-x: auto; border-radius: 8px; border: 1px solid #334155;"> | |
| {html_table} | |
| </div> | |
| """ | |
| return styled_html | |
| def handle_compare(text_a, file_a, text_b, file_b): | |
| # Retrieve values | |
| final_a = text_a | |
| if file_a is not None: | |
| try: | |
| with open(file_a.name, 'r', encoding='utf-8', errors='ignore') as f: | |
| final_a = f.read() | |
| except Exception as e: | |
| final_a = f"Error reading File A: {str(e)}" | |
| final_b = text_b | |
| if file_b is not None: | |
| try: | |
| with open(file_b.name, 'r', encoding='utf-8', errors='ignore') as f: | |
| final_b = f.read() | |
| except Exception as e: | |
| final_b = f"Error reading File B: {str(e)}" | |
| if not final_a.strip() and not final_b.strip(): | |
| return "<div style='color: #ef4444; padding: 1rem;'>Please enter or upload text for both versions to compare.</div>", "" | |
| # Calculate stats | |
| stats = calculate_drift_metrics(final_a, final_b) | |
| # HTML stats card | |
| stats_html = f""" | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;"> | |
| <div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #94a3b8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Document Similarity</div> | |
| <div style="font-size: 2rem; font-weight: 700; color: #10b981; margin-top: 0.25rem;">{stats['similarity']}%</div> | |
| </div> | |
| <div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #94a3b8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Lexical Drift Spread</div> | |
| <div style="font-size: 2rem; font-weight: 700; color: #ef4444; margin-top: 0.25rem;">{stats['drift']}%</div> | |
| </div> | |
| <div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #94a3b8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Word Additions</div> | |
| <div style="font-size: 2rem; font-weight: 700; color: #3b82f6; margin-top: 0.25rem;">+{stats['insertions']}</div> | |
| </div> | |
| <div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center;"> | |
| <div style="font-size: 0.8rem; color: #94a3b8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Word Deletions</div> | |
| <div style="font-size: 2rem; font-weight: 700; color: #f43f5e; margin-top: 0.25rem;">-{stats['deletions']}</div> | |
| </div> | |
| </div> | |
| <div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; color: #cbd5e1;"> | |
| <h4 style="margin: 0 0 0.5rem 0; color: #e2e8f0;">Provenance Analysis Summary</h4> | |
| <p style="margin: 0; font-size: 0.95rem; line-height: 1.5;"> | |
| Version A contains <strong>{stats['words_a']}</strong> words, and Version B contains <strong>{stats['words_b']}</strong> words. | |
| There are <strong>{stats['replacements']}</strong> words modified in place. | |
| The vocabulary overlap index (Jaccard similarity) is <strong>{stats['jaccard']}%</strong>. | |
| This indicates a <strong>{"high" if stats['drift'] > 30 else "moderate" if stats['drift'] > 10 else "minimal"} degree of document mutation</strong> over successive drafts. | |
| </p> | |
| </div> | |
| """ | |
| # Generate interactive table | |
| diff_table = generate_word_diff(final_a, final_b) | |
| return stats_html, diff_table | |
| # Setup CSS styling for premium look | |
| custom_css = """ | |
| body, .gradio-container { background-color: #0b0f19 !important; font-family: 'Inter', sans-serif !important; } | |
| h1, h2, h3 { color: #f8fafc !important; } | |
| .gr-button-primary { background: linear-gradient(135deg, #059669 0%, #10b981 100%) !important; border: none !important; } | |
| .gr-button-primary:hover { opacity: 0.9 !important; } | |
| """ | |
| # App Interface | |
| with gr.Blocks(theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="slate"), css=custom_css) as demo: | |
| gr.HTML(""" | |
| <div style="text-align: center; margin-bottom: 2rem; border-bottom: 1px solid #1e293b; padding-bottom: 1.5rem;"> | |
| <h1 style="font-size: 2.2rem; font-weight: 800; background: linear-gradient(135deg, #10b981 0%, #3b82f6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">π Version Diffing & Provenance Auditor</h1> | |
| <p style="color: #94a3b8; font-size: 1.1rem; margin-top: 0.5rem;">Audit the evolution of policies, legislative bills, manuscripts, or transcripts with statistical drift analytics.</p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### π Original Version (Draft A)") | |
| text_input_a = gr.Textbox(label="Paste Text A", placeholder="Enter original draft here...", lines=10) | |
| file_input_a = gr.File(label="Or Upload Text File A", file_types=[".txt"]) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### π New Version (Draft B)") | |
| text_input_b = gr.Textbox(label="Paste Text B", placeholder="Enter revised draft here...", lines=10) | |
| file_input_b = gr.File(label="Or Upload Text File B", file_types=[".txt"]) | |
| compare_btn = gr.Button("π Audit Document Drift & Generate Diff Table", variant="primary") | |
| gr.Markdown("### π Analytical Provenance Dashboard & Drift Metrics") | |
| stats_output = gr.HTML(value="<div style='color: #64748b;'>Submit documents above to calculate version changes.</div>") | |
| gr.Markdown("### π Color-Coded Structural Difference View") | |
| diff_output = gr.HTML(value="<div style='color: #64748b;'>Visual diff will render here. Green represents additions, red represents deletions, and yellow represents replacements.</div>") | |
| # Wiring events | |
| compare_btn.click( | |
| fn=handle_compare, | |
| inputs=[text_input_a, file_input_a, text_input_b, file_input_b], | |
| outputs=[stats_output, diff_output] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |