File size: 9,332 Bytes
a71f292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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()