Jomaric's picture
feat: initial release of curated portfolio app
a71f292
Raw
History Blame Contribute Delete
9.33 kB
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()