Spaces:
Runtime error
Runtime error
Commit Β·
a71f292
0
Parent(s):
feat: initial release of curated portfolio app
Browse files- README.md +20 -0
- app.py +185 -0
- requirements.txt +3 -0
README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Version Diffing & Provenance Auditor
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: cyan
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.19.2
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Version Diffing & Provenance Auditor
|
| 13 |
+
|
| 14 |
+
A premium computational social science application designed to analyze text version differences, track document provenance, and measure lexical drift across legislative bills, treaties, oral history transcripts, or manuscript revisions.
|
| 15 |
+
|
| 16 |
+
### Features
|
| 17 |
+
- **Side-by-Side Visual Diffing**: Color-coded HTML grid highlighting insertions (green), modifications (yellow), and deletions (red).
|
| 18 |
+
- **Lexical Drift Analytics**: Computes sequence alignment ratios, Jaccard vocabulary similarity indices, and precise word mutations.
|
| 19 |
+
- **File Upload Support**: Drag-and-drop plain text files or paste text blocks directly.
|
| 20 |
+
- **Headless CPU Optimized**: Pure mathematical comparison algorithms that start in milliseconds.
|
app.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import difflib
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
def clean_and_tokenize(text):
|
| 6 |
+
# Normalize whitespaces
|
| 7 |
+
return re.findall(r'\b\w+\b|[^\w\s]', text)
|
| 8 |
+
|
| 9 |
+
def calculate_drift_metrics(text_a, text_b):
|
| 10 |
+
words_a = text_a.split()
|
| 11 |
+
words_b = text_b.split()
|
| 12 |
+
|
| 13 |
+
set_a = set(words_a)
|
| 14 |
+
set_b = set(words_b)
|
| 15 |
+
|
| 16 |
+
# Jaccard similarity
|
| 17 |
+
union = len(set_a.union(set_b))
|
| 18 |
+
intersection = len(set_a.intersection(set_b))
|
| 19 |
+
jaccard_sim = (intersection / union) * 100 if union > 0 else 100.0
|
| 20 |
+
|
| 21 |
+
# SequenceMatcher
|
| 22 |
+
matcher = difflib.SequenceMatcher(None, words_a, words_b)
|
| 23 |
+
ratio = matcher.ratio() * 100
|
| 24 |
+
|
| 25 |
+
# Edits breakdown
|
| 26 |
+
opcodes = matcher.get_opcodes()
|
| 27 |
+
insertions = 0
|
| 28 |
+
deletions = 0
|
| 29 |
+
replacements = 0
|
| 30 |
+
|
| 31 |
+
for tag, i1, i2, j1, j2 in opcodes:
|
| 32 |
+
if tag == 'insert':
|
| 33 |
+
insertions += (j2 - j1)
|
| 34 |
+
elif tag == 'delete':
|
| 35 |
+
deletions += (i2 - i1)
|
| 36 |
+
elif tag == 'replace':
|
| 37 |
+
replacements += max(i2 - i1, j2 - j1)
|
| 38 |
+
|
| 39 |
+
drift_percent = 100 - ratio
|
| 40 |
+
|
| 41 |
+
return {
|
| 42 |
+
"jaccard": round(jaccard_sim, 2),
|
| 43 |
+
"similarity": round(ratio, 2),
|
| 44 |
+
"drift": round(drift_percent, 2),
|
| 45 |
+
"insertions": insertions,
|
| 46 |
+
"deletions": deletions,
|
| 47 |
+
"replacements": replacements,
|
| 48 |
+
"words_a": len(words_a),
|
| 49 |
+
"words_b": len(words_b)
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
def generate_word_diff(text_a, text_b):
|
| 53 |
+
# Clean texts
|
| 54 |
+
words_a = text_a.splitlines()
|
| 55 |
+
words_b = text_b.splitlines()
|
| 56 |
+
|
| 57 |
+
# We will generate a beautiful side-by-side or inline HTML diff
|
| 58 |
+
# difflib.HtmlDiff is nice, but we want a highly premium custom HTML view
|
| 59 |
+
diff = difflib.HtmlDiff(wrapcolumn=60)
|
| 60 |
+
html_table = diff.make_table(words_a, words_b, context=True, numlines=3)
|
| 61 |
+
|
| 62 |
+
# Style the table to fit our glowing dark theme
|
| 63 |
+
styled_html = f"""
|
| 64 |
+
<style>
|
| 65 |
+
.diff_table {{ font-family: 'Inter', monospace; font-size: 13px; border-collapse: collapse; width: 100%; color: #e2e8f0; background: #0f172a; border-radius: 8px; overflow: hidden; }}
|
| 66 |
+
.diff_header {{ background: #1e293b; color: #94a3b8; font-weight: 600; padding: 8px; border-bottom: 2px solid #334155; }}
|
| 67 |
+
td.diff_header {{ text-align: right; background: #1e293b; color: #64748b; padding: 4px 8px; user-select: none; width: 40px; border-right: 1px solid #334155; }}
|
| 68 |
+
.diff_next {{ display: none; }}
|
| 69 |
+
.diff_add {{ background-color: rgba(16, 185, 129, 0.25) !important; color: #34d399 !important; text-decoration: none; }}
|
| 70 |
+
.diff_chg {{ background-color: rgba(245, 158, 11, 0.25) !important; color: #fbbf24 !important; text-decoration: none; }}
|
| 71 |
+
.diff_sub {{ background-color: rgba(239, 68, 68, 0.25) !important; color: #f87171 !important; text-decoration: line-through; }}
|
| 72 |
+
td {{ padding: 4px 12px; word-break: break-all; white-space: pre-wrap; }}
|
| 73 |
+
tr:hover td {{ background: #1e293b; }}
|
| 74 |
+
</style>
|
| 75 |
+
<div style="overflow-x: auto; border-radius: 8px; border: 1px solid #334155;">
|
| 76 |
+
{html_table}
|
| 77 |
+
</div>
|
| 78 |
+
"""
|
| 79 |
+
return styled_html
|
| 80 |
+
|
| 81 |
+
def handle_compare(text_a, file_a, text_b, file_b):
|
| 82 |
+
# Retrieve values
|
| 83 |
+
final_a = text_a
|
| 84 |
+
if file_a is not None:
|
| 85 |
+
try:
|
| 86 |
+
with open(file_a.name, 'r', encoding='utf-8', errors='ignore') as f:
|
| 87 |
+
final_a = f.read()
|
| 88 |
+
except Exception as e:
|
| 89 |
+
final_a = f"Error reading File A: {str(e)}"
|
| 90 |
+
|
| 91 |
+
final_b = text_b
|
| 92 |
+
if file_b is not None:
|
| 93 |
+
try:
|
| 94 |
+
with open(file_b.name, 'r', encoding='utf-8', errors='ignore') as f:
|
| 95 |
+
final_b = f.read()
|
| 96 |
+
except Exception as e:
|
| 97 |
+
final_b = f"Error reading File B: {str(e)}"
|
| 98 |
+
|
| 99 |
+
if not final_a.strip() and not final_b.strip():
|
| 100 |
+
return "<div style='color: #ef4444; padding: 1rem;'>Please enter or upload text for both versions to compare.</div>", ""
|
| 101 |
+
|
| 102 |
+
# Calculate stats
|
| 103 |
+
stats = calculate_drift_metrics(final_a, final_b)
|
| 104 |
+
|
| 105 |
+
# HTML stats card
|
| 106 |
+
stats_html = f"""
|
| 107 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem;">
|
| 108 |
+
<div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center;">
|
| 109 |
+
<div style="font-size: 0.8rem; color: #94a3b8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Document Similarity</div>
|
| 110 |
+
<div style="font-size: 2rem; font-weight: 700; color: #10b981; margin-top: 0.25rem;">{stats['similarity']}%</div>
|
| 111 |
+
</div>
|
| 112 |
+
<div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center;">
|
| 113 |
+
<div style="font-size: 0.8rem; color: #94a3b8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Lexical Drift Spread</div>
|
| 114 |
+
<div style="font-size: 2rem; font-weight: 700; color: #ef4444; margin-top: 0.25rem;">{stats['drift']}%</div>
|
| 115 |
+
</div>
|
| 116 |
+
<div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center;">
|
| 117 |
+
<div style="font-size: 0.8rem; color: #94a3b8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Word Additions</div>
|
| 118 |
+
<div style="font-size: 2rem; font-weight: 700; color: #3b82f6; margin-top: 0.25rem;">+{stats['insertions']}</div>
|
| 119 |
+
</div>
|
| 120 |
+
<div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; text-align: center;">
|
| 121 |
+
<div style="font-size: 0.8rem; color: #94a3b8; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em;">Word Deletions</div>
|
| 122 |
+
<div style="font-size: 2rem; font-weight: 700; color: #f43f5e; margin-top: 0.25rem;">-{stats['deletions']}</div>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
<div style="background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; margin-bottom: 1.5rem; color: #cbd5e1;">
|
| 126 |
+
<h4 style="margin: 0 0 0.5rem 0; color: #e2e8f0;">Provenance Analysis Summary</h4>
|
| 127 |
+
<p style="margin: 0; font-size: 0.95rem; line-height: 1.5;">
|
| 128 |
+
Version A contains <strong>{stats['words_a']}</strong> words, and Version B contains <strong>{stats['words_b']}</strong> words.
|
| 129 |
+
There are <strong>{stats['replacements']}</strong> words modified in place.
|
| 130 |
+
The vocabulary overlap index (Jaccard similarity) is <strong>{stats['jaccard']}%</strong>.
|
| 131 |
+
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.
|
| 132 |
+
</p>
|
| 133 |
+
</div>
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
# Generate interactive table
|
| 137 |
+
diff_table = generate_word_diff(final_a, final_b)
|
| 138 |
+
|
| 139 |
+
return stats_html, diff_table
|
| 140 |
+
|
| 141 |
+
# Setup CSS styling for premium look
|
| 142 |
+
custom_css = """
|
| 143 |
+
body, .gradio-container { background-color: #0b0f19 !important; font-family: 'Inter', sans-serif !important; }
|
| 144 |
+
h1, h2, h3 { color: #f8fafc !important; }
|
| 145 |
+
.gr-button-primary { background: linear-gradient(135deg, #059669 0%, #10b981 100%) !important; border: none !important; }
|
| 146 |
+
.gr-button-primary:hover { opacity: 0.9 !important; }
|
| 147 |
+
"""
|
| 148 |
+
|
| 149 |
+
# App Interface
|
| 150 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="emerald", secondary_hue="slate"), css=custom_css) as demo:
|
| 151 |
+
gr.HTML("""
|
| 152 |
+
<div style="text-align: center; margin-bottom: 2rem; border-bottom: 1px solid #1e293b; padding-bottom: 1.5rem;">
|
| 153 |
+
<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>
|
| 154 |
+
<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>
|
| 155 |
+
</div>
|
| 156 |
+
""")
|
| 157 |
+
|
| 158 |
+
with gr.Row():
|
| 159 |
+
with gr.Column(scale=1):
|
| 160 |
+
gr.Markdown("### π Original Version (Draft A)")
|
| 161 |
+
text_input_a = gr.Textbox(label="Paste Text A", placeholder="Enter original draft here...", lines=10)
|
| 162 |
+
file_input_a = gr.File(label="Or Upload Text File A", file_types=[".txt"])
|
| 163 |
+
|
| 164 |
+
with gr.Column(scale=1):
|
| 165 |
+
gr.Markdown("### π New Version (Draft B)")
|
| 166 |
+
text_input_b = gr.Textbox(label="Paste Text B", placeholder="Enter revised draft here...", lines=10)
|
| 167 |
+
file_input_b = gr.File(label="Or Upload Text File B", file_types=[".txt"])
|
| 168 |
+
|
| 169 |
+
compare_btn = gr.Button("π Audit Document Drift & Generate Diff Table", variant="primary")
|
| 170 |
+
|
| 171 |
+
gr.Markdown("### π Analytical Provenance Dashboard & Drift Metrics")
|
| 172 |
+
stats_output = gr.HTML(value="<div style='color: #64748b;'>Submit documents above to calculate version changes.</div>")
|
| 173 |
+
|
| 174 |
+
gr.Markdown("### π Color-Coded Structural Difference View")
|
| 175 |
+
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>")
|
| 176 |
+
|
| 177 |
+
# Wiring events
|
| 178 |
+
compare_btn.click(
|
| 179 |
+
fn=handle_compare,
|
| 180 |
+
inputs=[text_input_a, file_input_a, text_input_b, file_input_b],
|
| 181 |
+
outputs=[stats_output, diff_output]
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
if __name__ == "__main__":
|
| 185 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
pandas
|
| 3 |
+
jinja2
|