Basementup commited on
Commit
4f04e81
·
verified ·
1 Parent(s): 8898d45

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +922 -536
app.py CHANGED
@@ -1,574 +1,960 @@
1
- import gradio as gr
 
 
2
  import os
3
- import json
4
- import tempfile
5
- from datetime import datetime
6
- from typing import List, Dict
7
- import PyPDF2
8
- from difflib import SequenceMatcher
9
  import re
10
- import requests
11
-
12
- # ============================================================================
13
- # OPTIONAL: Hugging Face zero-shot classifier (CPU mode, loads lazily)
14
- # ============================================================================
15
- zero_shot_classifier = None
16
-
17
- def get_classifier():
18
- global zero_shot_classifier
19
- if zero_shot_classifier is None:
20
- try:
21
- from transformers import pipeline
22
- zero_shot_classifier = pipeline(
23
- "zero-shot-classification",
24
- model="facebook/bart-large-mnli",
25
- device=-1
26
- )
27
- except Exception as e:
28
- print(f"Warning: Could not load zero-shot classifier: {e}")
29
- return zero_shot_classifier
30
-
31
-
32
- # ============================================================================
33
- # MODULE 1: Document Parser
34
- # ============================================================================
35
-
36
- class DocumentParser:
37
-
38
- @staticmethod
39
- def extract_text_from_pdf(file_path: str) -> str:
40
- try:
41
- with open(file_path, 'rb') as f:
42
- reader = PyPDF2.PdfReader(f)
43
- text = ""
44
- for page in reader.pages:
45
- extracted = page.extract_text()
46
- if extracted:
47
- text += extracted + "\n"
48
- return text
49
- except Exception as e:
50
- return f"Error reading PDF: {str(e)}"
51
-
52
- @staticmethod
53
- def extract_text_from_file(file_path: str) -> str:
54
- if not file_path:
55
- return "Error: No file path provided"
56
-
57
- try:
58
- lower_path = file_path.lower()
59
-
60
- if lower_path.endswith('.pdf'):
61
- return DocumentParser.extract_text_from_pdf(file_path)
62
-
63
- if lower_path.endswith('.docx'):
64
- from docx import Document as DocxDocument
65
- doc = DocxDocument(file_path)
66
- paragraphs = [p.text.strip() for p in doc.paragraphs if p.text and p.text.strip()]
67
- return "\n\n".join(paragraphs)
68
-
69
- if lower_path.endswith('.txt'):
70
- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
71
- return f.read()
72
-
73
- return "Error reading file: Unsupported file type"
74
-
75
- except Exception as e:
76
- return f"Error reading file: {str(e)}"
77
-
78
- @staticmethod
79
- def parse_document(text: str) -> Dict:
80
- text = text.replace('\r\n', '\n').replace('\r', '\n')
81
- text = re.sub(r'\n[ \t]*\n+', '\n\n', text).strip()
82
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
84
-
85
- sentences = []
86
- for para in paragraphs:
87
- parts = re.split(r'(?<=[.!?])\s+', para)
88
- sentences.extend([s.strip() for s in parts if s.strip()])
89
-
90
- return {
91
- 'full_text': text,
92
- 'paragraphs': paragraphs,
93
- 'sentences': sentences,
94
- 'word_count': len(text.split()),
95
- 'char_count': len(text)
96
- }
97
-
98
-
99
- # ============================================================================
100
- # MODULE 2: Forensic Analyzer
101
- # ============================================================================
102
-
103
- class ForensicAnalyzer:
104
-
105
- RISK_PATTERNS = {
106
- 'denial_trap': r"\b(deny|denies|denied|denial|no such|never|not applicable)\b",
107
- 'admission_by_silence': r"\b(fail|failed|failure|omit|omitted|omission|no response|no reply)\b",
108
- 'procedural_hedging': r"\b(without prejudice|subject to|conditional|provided that|notwithstanding)\b",
109
- 'burden_flip': r"\b(claimant must|burden|onus|responsibility|incumbent upon|required to prove)\b"
110
- }
111
-
112
- @staticmethod
113
- def find_duplicates(docs: List[Dict]) -> List[Dict]:
114
- duplicates = []
115
- for i, doc1 in enumerate(docs):
116
- for j, doc2 in enumerate(docs[i + 1:], start=i + 1):
117
- for para1 in doc1['paragraphs']:
118
- for para2 in doc2['paragraphs']:
119
- if len(para1) < 20 or len(para2) < 20:
120
- continue
121
- ratio = SequenceMatcher(None, para1, para2).ratio()
122
- if ratio > 0.85:
123
- duplicates.append({
124
- 'doc1': i, 'doc2': j,
125
- 'similarity': ratio,
126
- 'text': para1[:50000] + "...",
127
- 'type': 'duplicate' if ratio > 0.95 else 'near_duplicate'
128
- })
129
- return duplicates
130
-
131
- @staticmethod
132
- def find_unique_content(docs: List[Dict]) -> List[Dict]:
133
- unique_items = []
134
- for i, doc in enumerate(docs):
135
- for para in doc['paragraphs']:
136
- if len(para) < 20:
137
- continue
138
- is_unique = True
139
- for j, other_doc in enumerate(docs):
140
- if i == j:
141
- continue
142
- for other_para in other_doc['paragraphs']:
143
- if SequenceMatcher(None, para, other_para).ratio() > 0.8:
144
- is_unique = False
145
- break
146
- if not is_unique:
147
- break
148
- if is_unique:
149
- unique_items.append({
150
- 'doc_index': i,
151
- 'content': para,
152
- 'length': len(para),
153
- 'type': 'unique_paragraph'
154
- })
155
- return unique_items
156
-
157
- @staticmethod
158
- def detect_risk_language(text: str) -> List[Dict]:
159
- findings = []
160
- for risk_type, pattern in ForensicAnalyzer.RISK_PATTERNS.items():
161
- for match in re.finditer(pattern, text, re.IGNORECASE):
162
- start = max(0, match.start() - 50)
163
- end = min(len(text), match.end() + 50)
164
- findings.append({
165
- 'type': risk_type,
166
- 'matched_text': match.group(),
167
- 'context': text[start:end],
168
- 'position': match.start()
169
- })
170
- return findings
171
-
172
- @staticmethod
173
- def analyze_documents(docs: List[Dict]) -> Dict:
174
- return {
175
- 'duplicates': ForensicAnalyzer.find_duplicates(docs),
176
- 'unique_content': ForensicAnalyzer.find_unique_content(docs),
177
- 'risk_language': [
178
- item for doc in docs
179
- for item in ForensicAnalyzer.detect_risk_language(doc['full_text'])
180
- ]
181
- }
182
-
183
-
184
- # ============================================================================
185
- # MODULE 3 & 4: Document Merger
186
- # ============================================================================
187
-
188
- class DocumentMerger:
189
-
190
- @staticmethod
191
- def select_best_content(duplicates, docs):
192
- best_versions = []
193
- processed = set()
194
- for dup in duplicates:
195
- key = tuple(sorted([dup['doc1'], dup['doc2']]))
196
- if key in processed:
197
- continue
198
- processed.add(key)
199
- doc1_text = docs[dup['doc1']]['full_text']
200
- doc2_text = docs[dup['doc2']]['full_text']
201
- best_versions.append(doc1_text if len(doc1_text) > len(doc2_text) else doc2_text)
202
- return best_versions
203
-
204
- @staticmethod
205
- def merge_documents(docs, analysis):
206
- output = []
207
- output.append("MASTER EVIDENCE DOCUMENT")
208
- output.append("=" * 80)
209
- output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
210
- output.append(f"Source Documents: {len(docs)}")
211
- output.append("=" * 80 + "\n")
212
-
213
- output.append("EXECUTIVE SUMMARY")
214
- output.append("-" * 80)
215
- output.append(f"Total Duplicates Found: {len(analysis['duplicates'])}")
216
- output.append(f"Unique Content Items: {len(analysis['unique_content'])}")
217
- output.append(f"Risk Language Instances: {len(analysis['risk_language'])}\n")
218
-
219
- output.append("CONSOLIDATED CONTENT")
220
- output.append("-" * 80)
221
- best_versions = DocumentMerger.select_best_content(analysis['duplicates'], docs)
222
- if best_versions:
223
- for i, content in enumerate(best_versions, 1):
224
- output.append(f"\n[Section {i}]")
225
- output.append(content[:50000] + "..." if len(content) > 500 else content)
226
- else:
227
- output.append("No duplicate content found across documents.")
228
-
229
- output.append("\n\nUNIQUE FINDINGS")
230
- output.append("-" * 80)
231
- for item in analysis['unique_content'][:100]:
232
- output.append(f"\n[From Document {item['doc_index'] + 1}]")
233
- c = item['content']
234
- output.append(c[:50000] + "..." if len(c) > 300 else c)
235
-
236
- output.append("\n\nRISK LANGUAGE ANALYSIS")
237
- output.append("-" * 80)
238
- risk_summary = {}
239
- for risk in analysis['risk_language']:
240
- risk_summary[risk['type']] = risk_summary.get(risk['type'], 0) + 1
241
- if risk_summary:
242
- for risk_type, count in risk_summary.items():
243
- output.append(f"{risk_type.replace('_', ' ').title()}: {count} instances")
244
  else:
245
- output.append("No risk language patterns detected.")
246
-
247
- output.append("\n" + "=" * 80)
248
- output.append("END OF MASTER EVIDENCE DOCUMENT")
249
- return "\n".join(output)
250
-
251
-
252
- # ============================================================================
253
- # AI SUMMARISATION
254
- # ============================================================================
255
-
256
- def ai_summarise(merged_doc: str, analysis: Dict) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  try:
258
- hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
259
-
260
- risk_summary = {}
261
- for risk in analysis.get('risk_language', []):
262
- risk_summary[risk['type']] = risk_summary.get(risk['type'], 0) + 1
263
-
264
- # Condensed input for BART summarisation model
265
- input_text = f"""
266
- Forensic document analysis results:
267
- - Duplicates found: {len(analysis.get('duplicates', []))}
268
- - Unique content items: {len(analysis.get('unique_content', []))}
269
- - Risk language: {json.dumps(risk_summary)}
270
-
271
- Document excerpt:
272
- {merged_doc[:50000]}
273
- """.strip()
274
-
275
- headers = {"Content-Type": "application/json"}
276
- if hf_token:
277
- headers["Authorization"] = f"Bearer {hf_token}"
278
-
279
- response = requests.post(
280
- "https://router.huggingface.co/hf-inference/models/facebook/bart-large-cnn",
281
- headers=headers,
282
- json={
283
- "inputs": input_text,
284
- "parameters": {
285
- "max_length": 400,
286
- "min_length": 100,
287
- "do_sample": False
288
- }
289
- }
290
- )
291
 
292
- if response.status_code == 200:
293
- data = response.json()
294
- if isinstance(data, list) and data:
295
- summary = data[0].get('summary_text', '')
296
- risk_lines = "\n".join([
297
- f"- {k.replace('_', ' ').title()}: {v} instances"
298
- for k, v in risk_summary.items()
299
- ])
300
- return f"AI SUMMARY\n{'=' * 60}\n\n{summary}\n\nRisk Language Breakdown:\n{risk_lines}"
301
- return "Summary could not be extracted from model response."
302
- elif response.status_code == 503:
303
- return "Model is loading — please wait 20 seconds and try again."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  else:
305
- return f"Summarisation unavailable (status {response.status_code}): {response.text}"
306
-
 
 
 
 
 
 
 
 
 
307
  except Exception as e:
308
- return f"Summarisation error: {str(e)}"
309
-
310
 
311
- # ============================================================================
312
- # EXPORT FUNCTIONS
313
- # ============================================================================
314
-
315
- def export_txt(merged_doc: str, analysis_report: str) -> str:
316
  try:
317
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.txt', mode='w', encoding='utf-8')
318
- tmp.write("SEAMSTRESSES FRAMEWORK — EXPORT\n")
319
- tmp.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
320
- tmp.write("=" * 80 + "\n\n")
321
- tmp.write(merged_doc)
322
- tmp.write("\n\n" + "=" * 80 + "\n\n")
323
- tmp.write(analysis_report)
324
- tmp.close()
325
- return tmp.name
326
  except Exception as e:
327
- print(f"TXT export error: {e}")
328
- return None
 
 
329
 
 
330
 
331
- def export_pdf(merged_doc: str, analysis_report: str) -> str:
332
- try:
333
- from reportlab.lib.pagesizes import A4
334
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
335
- from reportlab.lib.units import mm
336
- from reportlab.lib import colors
337
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
338
- from reportlab.lib.enums import TA_CENTER
339
-
340
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
341
- tmp.close()
342
-
343
- doc = SimpleDocTemplate(
344
- tmp.name, pagesize=A4,
345
- rightMargin=20*mm, leftMargin=20*mm,
346
- topMargin=20*mm, bottomMargin=20*mm
347
- )
 
 
 
 
 
 
 
 
 
 
348
 
349
- styles = getSampleStyleSheet()
350
- title_style = ParagraphStyle('Title', parent=styles['Heading1'], fontSize=16, alignment=TA_CENTER, spaceAfter=6)
351
- heading_style = ParagraphStyle('Heading', parent=styles['Heading2'], fontSize=11, textColor=colors.HexColor('#1a1a2e'), spaceAfter=4)
352
- body_style = ParagraphStyle('Body', parent=styles['Normal'], fontSize=9, leading=14, spaceAfter=3)
353
-
354
- story = []
355
- story.append(Paragraph("Seamstresses Framework", title_style))
356
- story.append(Paragraph("Forensic Document Analysis Report", heading_style))
357
- story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", body_style))
358
- story.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
359
- story.append(Spacer(1, 10))
360
-
361
- story.append(Paragraph("Master Document", heading_style))
362
- for block in re.split(r'\n\s*\n', merged_doc):
363
- block = block.strip()
364
- if not block:
365
- continue
366
- safe = block.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br/>')
367
- if set(block) <= {'=', '-'}:
368
- story.append(HRFlowable(width="100%", thickness=0.5, color=colors.lightgrey))
369
- elif block.isupper() and len(block) > 3 and '\n' not in block and not block.startswith('['):
370
- story.append(Paragraph(safe, heading_style))
371
- else:
372
- story.append(Paragraph(safe, body_style))
373
-
374
- story.append(Spacer(1, 12))
375
- story.append(Paragraph("Analysis Report", heading_style))
376
- story.append(HRFlowable(width="100%", thickness=0.5, color=colors.lightgrey))
377
- for block in re.split(r'\n\s*\n', analysis_report):
378
- block = block.strip()
379
- if not block:
380
- continue
381
- safe = block.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br/>')
382
- if block.isupper() and len(block) > 3 and '\n' not in block:
383
- story.append(Paragraph(safe, heading_style))
384
- else:
385
- story.append(Paragraph(safe, body_style))
386
-
387
- doc.build(story)
388
- return tmp.name
389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  except Exception as e:
391
- print(f"PDF export error: {e}")
392
  return None
393
 
 
394
 
395
- def export_docx(merged_doc: str, analysis_report: str, summary: str) -> str:
 
 
 
 
 
396
  try:
397
- from docx import Document as DocxDocument
398
- from docx.enum.text import WD_ALIGN_PARAGRAPH
399
-
400
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.docx')
401
- tmp.close()
402
-
403
- doc = DocxDocument()
404
-
405
- title = doc.add_heading('Seamstresses Framework', 0)
406
- title.alignment = WD_ALIGN_PARAGRAPH.CENTER
407
- sub = doc.add_heading('Forensic Document Analysis Report', level=2)
408
- sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
409
- doc.add_paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
410
- doc.add_paragraph()
 
 
411
 
412
- # AI Summary
413
- if summary and not summary.startswith("AI summarisation"):
414
- doc.add_heading('AI Summary', level=1)
415
- doc.add_paragraph(summary)
416
- doc.add_paragraph()
417
-
418
- # Master document
419
- doc.add_heading('Master Document', level=1)
420
- for line in merged_doc.split('\n'):
421
- if line.strip():
422
- if line.startswith('===') or line.startswith('---'):
423
- doc.add_paragraph('─' * 60)
424
- elif line.isupper() and len(line) > 3 and not line.startswith('['):
425
- doc.add_heading(line, level=2)
426
- else:
427
- doc.add_paragraph(line)
428
 
429
- doc.add_page_break()
 
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
- # Analysis report
432
- doc.add_heading('Analysis Report', level=1)
433
- for line in analysis_report.split('\n'):
434
- if line.strip():
435
- if line.isupper() and len(line) > 3:
436
- doc.add_heading(line, level=2)
437
- else:
438
- doc.add_paragraph(line)
439
 
440
- doc.save(tmp.name)
441
- return tmp.name
 
442
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  except Exception as e:
444
- print(f"DOCX export error: {e}")
445
  return None
446
 
 
447
 
448
- # ============================================================================
449
- # MAIN PROCESSING FUNCTION
450
- # ============================================================================
451
-
452
- def process_documents(file1, file2, file3):
 
453
  try:
454
- raw_inputs = [file1, file2, file3]
455
- files = []
456
- for f in raw_inputs:
457
- if f is None:
458
- continue
459
- if isinstance(f, dict):
460
- path = f.get('path') or f.get('name')
461
- elif hasattr(f, 'name'):
462
- path = f.name
463
- elif isinstance(f, str):
464
- path = f
465
- else:
466
- path = str(f)
467
- if path:
468
- files.append(path)
469
-
470
- if not files:
471
- return "Please upload at least one document.", "", "", None, None, None
472
-
473
- docs = []
474
- file_names = []
475
- for file_path in files:
476
- text = DocumentParser.extract_text_from_file(file_path)
477
- if not text or text.startswith("Error"):
478
- return f"Error: {text}", "", "", None, None, None
479
- docs.append(DocumentParser.parse_document(text))
480
- file_names.append(os.path.basename(file_path))
481
-
482
- analysis = ForensicAnalyzer.analyze_documents(docs)
483
- merged_doc = DocumentMerger.merge_documents(docs, analysis)
484
-
485
- # Analysis report
486
- analysis_report = f"FORENSIC ANALYSIS REPORT\n{'=' * 80}\n\n"
487
- analysis_report += f"FILES ANALYZED: {', '.join(file_names)}\n\n"
488
- analysis_report += f"DUPLICATE ANALYSIS\n{'-' * 80}\n"
489
- analysis_report += f"Total Duplicates Found: {len(analysis['duplicates'])}\n\n"
490
- for dup in analysis['duplicates'][:50]:
491
- analysis_report += f"- Documents {dup['doc1']+1} & {dup['doc2']+1}: {dup['similarity']:.1%} similarity\n"
492
- analysis_report += f"\nUNIQUE CONTENT\n{'-' * 80}\n"
493
- analysis_report += f"Total Unique Items: {len(analysis['unique_content'])}\n\n"
494
- for item in analysis['unique_content'][:50]:
495
- analysis_report += f"- From Document {item['doc_index']+1}: {item['content'][:50000]}...\n"
496
- analysis_report += f"\nRISK LANGUAGE DETECTED\n{'-' * 80}\n"
497
- risk_types = {}
498
- for risk in analysis['risk_language']:
499
- risk_types[risk['type']] = risk_types.get(risk['type'], 0) + 1
500
- for risk_type, count in risk_types.items():
501
- analysis_report += f"- {risk_type.replace('_', ' ').title()}: {count} instances\n"
502
-
503
- # AI Summary
504
- summary = ai_summarise(merged_doc, analysis)
505
-
506
- # Exports
507
- txt_path = export_txt(merged_doc, analysis_report)
508
- pdf_path = export_pdf(merged_doc, analysis_report)
509
- docx_path = export_docx(merged_doc, analysis_report, summary)
510
-
511
- return merged_doc, analysis_report, summary, txt_path, pdf_path, docx_path
512
-
513
  except Exception as e:
514
- import traceback
515
- return f"Error: {str(e)}\n\n{traceback.format_exc()}", "", "", None, None, None
516
-
517
-
518
- # ============================================================================
519
- # GRADIO INTERFACE
520
- # ============================================================================
 
 
 
 
 
 
 
521
 
522
- with gr.Blocks(title="Seamstresses Framework", theme=gr.themes.Soft(primary_hue="blue", secondary_hue="cyan")) as demo:
523
- gr.Markdown("""
524
- # 🧵 Seamstresses Framework
525
- ## Forensic Document Analysis, Intelligent Merging & AI Summarisation
526
- Upload up to 3 legal documents to deduplicate, analyse risk language, and generate an AI-powered summary.
527
- """)
528
 
 
 
 
 
529
  with gr.Row():
530
- with gr.Column():
531
- gr.Markdown("### Upload Documents")
532
- file1 = gr.File(label="Document 1", file_types=[".pdf", ".txt", ".docx"], type="filepath")
533
- file2 = gr.File(label="Document 2", file_types=[".pdf", ".txt", ".docx"], type="filepath")
534
- file3 = gr.File(label="Document 3 (Optional)", file_types=[".pdf", ".txt", ".docx"], type="filepath")
535
- analyze_btn = gr.Button("🔍 Analyze, Merge & Summarise", variant="primary", size="lg")
536
-
537
- with gr.Tabs():
538
- with gr.TabItem("📄 Master Document"):
539
- merged_output = gr.Textbox(label="Merged Document", lines=30, max_lines=200)
540
-
541
- with gr.TabItem("🔬 Analysis Report"):
542
- analysis_output = gr.Textbox(label="Forensic Analysis", lines=30, max_lines=200)
543
-
544
- with gr.TabItem("🤖 AI Summary"):
545
- summary_output = gr.Textbox(label="AI-Powered Summary", lines=30, max_lines=200)
546
-
547
- with gr.TabItem("📥 Export"):
548
- gr.Markdown("### Download Your Reports")
549
  with gr.Row():
550
- txt_file = gr.File(label="📄 Plain Text (.txt)", interactive=False)
551
- pdf_file = gr.File(label="📕 PDF (.pdf)", interactive=False)
552
- docx_file = gr.File(label="📘 Word Document (.docx)", interactive=False)
553
- gr.Markdown("""
554
- > **Word document** includes AI summary, merged document, and full analysis.
555
- > **PDF & TXT** contain the merged document and analysis report.
556
- """)
557
-
558
- analyze_btn.click(
559
- fn=process_documents,
560
- inputs=[file1, file2, file3],
561
- outputs=[merged_output, analysis_output, summary_output, txt_file, pdf_file, docx_file]
562
- )
563
 
564
- gr.Markdown("""
565
- ---
566
- ### Risk Language Detected
567
- - **Denial Trap** — deny, denied, never, not applicable
568
- - **Admission by Silence** — failure, omission, no response
569
- - **Procedural Hedging** — without prejudice, notwithstanding, subject to
570
- - **Burden Flip** — burden, onus, required to prove, claimant must
571
- """)
572
 
 
573
  if __name__ == "__main__":
574
- demo.launch()
 
1
+ https://huggingface.co/spaces/Basementup/Seamstressesedout/edit/main/app.py
2
+
3
+
4
  import os
 
 
 
 
 
 
5
  import re
6
+ import asyncio
7
+ import pandas as pd
8
+ import numpy as np
9
+ import gradio as gr
10
+ import pdfplumber
11
+ import dateparser
12
+ import edge_tts
13
+ from datetime import datetime
14
+ from sentence_transformers import SentenceTransformer, util
15
+ from docx import Document
16
+ from docx.shared import Pt, Inches, RGBColor
17
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
18
+ from docx.oxml.ns import qn
19
+ from docx.oxml import OxmlElement
20
+ from itertools import groupby
21
+ from openai import OpenAI # Import OpenAI library
22
+
23
+ # Force model to cache properly on Hugging Face
24
+ os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
25
+ os.environ["SENTENCE_TRANSFORMERS_HOME"] = "/tmp/model_cache"
26
+
27
+ # Initialize the Sentence Transformer model for deduplication
28
+ model_deduplication = SentenceTransformer('all-MiniLM-L6-v2')
29
+
30
+ # Initialize OpenAI client (will use environment variable OPENAI_API_KEY if set)
31
+ # For Hugging Face Spaces, you might need to set this as a Space Secret
32
+ client = OpenAI()
33
+
34
+ # --- Utility Functions ---
35
+
36
+ def extract_text_from_file(file_path):
37
+ """Extracts text from PDF, DOCX, or TXT files."""
38
+ ext = os.path.splitext(file_path)[1].lower()
39
+ text = ""
40
+ try:
41
+ if ext == ".pdf":
42
+ with pdfplumber.open(file_path) as pdf:
43
+ for page in pdf.pages:
44
+ page_text = page.extract_text()
45
+ if page_text:
46
+ text += page_text + "\n"
47
+ elif ext == ".docx":
48
+ doc = Document(file_path)
49
+ text = "\n".join([para.text for para in doc.paragraphs])
50
+ elif ext == ".txt":
51
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
52
+ text = f.read()
53
+ except Exception as e:
54
+ print(f"Error extracting from {file_path}: {e}")
55
+ return text
56
+
57
+ def extract_date(text):
58
+ """Attempts to extract a date from a text snippet."""
59
+ date_patterns = [
60
+ r'\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b',
61
+ r'\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]* \d{1,2},? \d{4}\b',
62
+ r'\b\d{4}-\d{2}-\d{2}\b'
63
+ ]
64
+ for pattern in date_patterns:
65
+ match = re.search(pattern, text, re.IGNORECASE)
66
+ if match:
67
+ parsed_date = dateparser.parse(match.group())
68
+ if parsed_date:
69
+ return parsed_date
70
+ return None
71
+
72
+ def detect_breach(text):
73
+ """Detects if a text snippet describes a breach or violation."""
74
+ breach_keywords = [
75
+ "breach", "violation", "failure", "non-compliance", "infringement",
76
+ "unauthorized", "leak", "compromise", "illegal", "incident", "misconduct"
77
+ ]
78
+ for keyword in breach_keywords:
79
+ if keyword.lower() in text.lower():
80
+ return True
81
+ return False
82
+
83
+ def create_docx(sections, output_path="Master_Evidence_Document.docx"):
84
+ """Creates a professionally formatted DOCX file with cover, TOC, and monthly grouping."""
85
+ doc = Document()
86
+
87
+ # Style settings
88
+ style = doc.styles['Normal']
89
+ font = style.font
90
+ font.name = 'Calibri'
91
+ font.size = Pt(11)
92
+ style.paragraph_format.space_after = Pt(6)
93
+
94
+ # Cover page
95
+ title = doc.add_heading('Master Evidence Document', 0)
96
+ title.alignment = WD_ALIGN_PARAGRAPH.CENTER
97
+ subtitle = doc.add_heading('Consolidated Chronology and Identified Breaches', level=1)
98
+ subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
99
+ date_para = doc.add_paragraph()
100
+ date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
101
+ date_para.add_run(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
102
+ doc.add_page_break()
103
+
104
+ # Table of Contents
105
+ doc.add_heading('Table of Contents', level=1)
106
+ para = doc.add_paragraph()
107
+ run = para.add_run()
108
+ fldChar1 = OxmlElement('w:fldChar')
109
+ fldChar1.set(qn('w:fldCharType'), 'begin')
110
+ run._r.append(fldChar1)
111
+ instrText = OxmlElement('w:instrText')
112
+ instrText.set(qn('xml:space'), 'preserve')
113
+ instrText.text = ' TOC \\o "1-3" \\h \\z \\u '
114
+ run._r.append(instrText)
115
+ fldChar2 = OxmlElement('w:fldChar')
116
+ fldChar2.set(qn('w:fldCharType'), 'separate')
117
+ run._r.append(fldChar2)
118
+ run2 = para.add_run('Right-click and choose "Update Field" to generate the Table of Contents')
119
+ fldChar3 = OxmlElement('w:fldChar')
120
+ fldChar3.set(qn('w:fldCharType'), 'end')
121
+ run2._r.append(fldChar3)
122
+ doc.add_page_break()
123
+
124
+ # Group dated sections by month
125
+ dated_sections = [s for s in sections if s['date']]
126
+ undated_sections = [s for s in sections if not s['date']]
127
+
128
+ def get_month_key(section):
129
+ return section['date'].strftime('%B %Y')
130
+
131
+ grouped = {}
132
+ for month, group in groupby(dated_sections, key=get_month_key):
133
+ grouped[month] = list(group)
134
+
135
+ for month, items in grouped.items():
136
+ month_heading = doc.add_heading(month, level=2)
137
+ for item in items:
138
+ p = doc.add_paragraph()
139
+ date_run = p.add_run(f"🔹 {item['date'].strftime('%Y-%m-%d')} ")
140
+ date_run.bold = True
141
+ date_run.font.size = Pt(10)
142
+ if item['is_breach']:
143
+ breach_run = p.add_run("⚠️ BREACH IDENTIFIED ")
144
+ breach_run.bold = True
145
+ breach_run.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
146
+ p.add_run(item['content'])
147
+ doc.add_paragraph("_" * 40)
148
+ doc.add_paragraph()
149
+
150
+ # Undated sections
151
+ if undated_sections:
152
+ doc.add_heading('Undated Entries', level=2)
153
+ for item in undated_sections:
154
+ p = doc.add_paragraph()
155
+ if item['is_breach']:
156
+ breach_run = p.add_run("⚠️ BREACH IDENTIFIED ")
157
+ breach_run.bold = True
158
+ breach_run.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
159
+ p.add_run(item['content'])
160
+
161
+ doc.save(output_path)
162
+ return output_path
163
+
164
+ # --- LLM Integration for PhD-level English and Book Generation ---
165
+
166
+ def generate_phd_level_text(prompt, max_tokens=2000):
167
+ """Generates PhD-level English text using an LLM."""
168
+ try:
169
+ response = client.chat.completions.create(
170
+ model="gpt-4o-mini", # Using gpt-4o-mini as a powerful and cost-effective option
171
+ messages=[
172
+ {"role": "system", "content": "You are an expert academic writer, specializing in producing PhD-level English. Your writing is characterized by precise vocabulary, complex sentence structures, rigorous argumentation, and adherence to scholarly conventions. You are writing a book based on provided evidence. Maintain a formal, objective, and authoritative tone. Ensure logical flow and coherence across paragraphs and sections. Avoid colloquialisms, contractions, and overly simplistic language. Focus on clarity, conciseness, and intellectual depth. Structure your output as a well-organized academic text, suitable for a scholarly publication."},
173
+ {"role": "user", "content": prompt}
174
+ ],
175
+ max_tokens=max_tokens,
176
+ temperature=0.7, # Adjust for creativity vs. factual accuracy
177
+ top_p=1,
178
+ frequency_penalty=0,
179
+ presence_penalty=0
180
+ )
181
+ return response.choices[0].message.content
182
+ except Exception as e:
183
+ print(f"LLM generation error: {e}")
184
+ return f"Error generating text: {e}"
185
+
186
+ def generate_book_chapter(title, content_sections):
187
+ """Generates a single book chapter from processed content sections."""
188
+ chapter_prompt = f"""Generate a comprehensive and rigorously argued chapter titled '{title}'.
189
+
190
+ Integrate the following key evidence and arguments, presented as individual sections. Synthesize these points into a cohesive narrative, ensuring smooth transitions and a logical progression of ideas. Your writing should reflect PhD-level academic standards, with precise language, complex sentence structures, and a formal tone. Expand on the evidence where necessary to provide context, analysis, and critical discussion. Ensure that the chapter contributes meaningfully to a larger scholarly work.
191
+
192
+ Evidence sections:
193
+ """
194
+ for i, section in enumerate(content_sections):
195
+ chapter_prompt += f"\nSection {i+1}:\n{section['content']}\n"
196
+ if section['is_breach']:
197
+ chapter_prompt += "(Note: This section highlights a potential breach or violation.)\n"
198
+
199
+ chapter_prompt += """
200
+ Structure the chapter with an introduction, several analytical paragraphs, and a concluding summary that ties back to the chapter's main argument. Ensure proper academic phrasing and avoid repetition. The goal is to transform raw evidence into a polished, scholarly narrative.
201
+ """
202
+ return generate_phd_level_text(chapter_prompt)
203
+
204
+ # --- Core Logic with Enhanced Deduplication ---
205
+
206
+ def process_files(files, deduplication_threshold=0.85):
207
+ if not files:
208
+ return "No files uploaded.", []
209
+
210
+ all_sections = []
211
+ for file in files:
212
+ file_path = file.name
213
+ file_name = os.path.basename(file_path)
214
+ text = extract_text_from_file(file_path)
215
+ # Split by paragraphs and also by sentences for finer granularity in deduplication
216
  paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
217
+ for p in paragraphs:
218
+ all_sections.append({
219
+ "content": p,
220
+ "source": file_name,
221
+ "date": extract_date(p),
222
+ "is_breach": detect_breach(p)
223
+ })
224
+
225
+ if not all_sections:
226
+ return "No content extracted.", []
227
+
228
+ # Aggressive Deduplication using SentenceTransformer with strict thresholding
229
+ contents = [s["content"] for s in all_sections]
230
+ embeddings = model_deduplication.encode(contents, convert_to_tensor=True)
231
+ unique_sections = []
232
+ processed_indices = set()
233
+
234
+ for i in range(len(all_sections)):
235
+ if i in processed_indices:
236
+ continue
237
+
238
+ # Find all similar sections using a very strict threshold for "crazy" deduplication
239
+ cosine_scores = util.cos_sim(embeddings[i], embeddings)[0]
240
+ # We use a slightly lower threshold for grouping but will use LLM to pick the absolute best
241
+ similar_indices = [j for j, score in enumerate(cosine_scores) if score > 0.75]
242
+
243
+ if len(similar_indices) > 1:
244
+ # If multiple similar sections, use LLM to pick the single best phrasing
245
+ candidates = [all_sections[idx]["content"] for idx in similar_indices]
246
+ selection_prompt = "From the following similar excerpts, select the single best, most comprehensive, and academically rigorous version. Return ONLY the selected text, nothing else.\n\n" + "\n---\n".join(candidates)
247
+ best_content = generate_phd_level_text(selection_prompt, max_tokens=1000)
248
+
249
+ # Use the metadata from the first occurrence but updated content
250
+ best_section = all_sections[i].copy()
251
+ best_section["content"] = best_content
252
+ unique_sections.append(best_section)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  else:
254
+ unique_sections.append(all_sections[i])
255
+
256
+ for idx in similar_indices:
257
+ processed_indices.add(idx)
258
+
259
+ # Sort by date
260
+ dated = [s for s in unique_sections if s["date"]]
261
+ undated = [s for s in unique_sections if not s["date"]]
262
+ dated.sort(key=lambda x: x["date"])
263
+ final_sections = dated + undated
264
+
265
+ # Text output for preview
266
+ output_text = "# Master Evidence Document\n\n"
267
+ for s in final_sections:
268
+ prefix = ""
269
+ if s["date"]:
270
+ prefix += f"**[{s['date'].strftime('%Y-%m-%d')}]** "
271
+ if s["is_breach"]:
272
+ prefix += "⚠️ **[BREACH IDENTIFIED]** "
273
+ output_text += f"{prefix}{s['content']}\n\n---\n\n"
274
+
275
+ return output_text, final_sections
276
+
277
+ # --- TTS ---
278
+
279
+ async def generate_tts_audio(text):
280
  try:
281
+ short_text = text[:5000] # Limit text for TTS
282
+ communicate = edge_tts.Communicate(short_text, "en-US-GuyNeural")
283
+ output_file = "output_audio.mp3"
284
+ await communicate.save(output_file)
285
+ return output_file
286
+ except Exception as e:
287
+ print(f"TTS error: {e}")
288
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
+ # --- Main Runner ---
291
+
292
+ def create_book_docx(title, content, output_path="PhD_Level_Book.docx"):
293
+ """Creates a professionally formatted DOCX for the generated book."""
294
+ doc = Document()
295
+
296
+ # Cover page
297
+ title_para = doc.add_heading(title, 0)
298
+ title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
299
+ doc.add_page_break()
300
+
301
+ # Split content by markdown headers for chapters
302
+ chapters = re.split(r'\n# ', content)
303
+ for chapter in chapters:
304
+ if not chapter.strip(): continue
305
+ lines = chapter.split('\n', 1)
306
+ chap_title = lines[0].strip()
307
+ chap_body = lines[1].strip() if len(lines) > 1 else ""
308
+
309
+ doc.add_heading(chap_title, level=1)
310
+ doc.add_paragraph(chap_body)
311
+ doc.add_page_break()
312
+
313
+ doc.save(output_path)
314
+ return output_path
315
+
316
+ def run_app(files, book_title="A Scholarly Examination", chapter_titles_str="Introduction, Analysis, Conclusion"):
317
+ output_text, final_sections = process_files(files)
318
+ if not final_sections:
319
+ return output_text, None, None, "No book generated.", None
320
+
321
+ # Generate Evidence DOCX
322
+ try:
323
+ evidence_docx_path = create_docx(final_sections, "Master_Evidence_Document.docx")
324
+ except Exception as e:
325
+ print(f"Evidence DOCX error: {e}")
326
+ evidence_docx_path = None
327
+
328
+ # Generate Book Content
329
+ book_content = ""
330
+ chapter_titles = [t.strip() for t in chapter_titles_str.split(',') if t.strip()]
331
+ sections_per_chapter = len(final_sections) // len(chapter_titles) if len(chapter_titles) > 0 else 1
332
+
333
+ start_idx = 0
334
+ for i, title in enumerate(chapter_titles):
335
+ end_idx = start_idx + sections_per_chapter
336
+ if i == len(chapter_titles) - 1:
337
+ chapter_sections = final_sections[start_idx:]
338
  else:
339
+ chapter_sections = final_sections[start_idx:end_idx]
340
+
341
+ if chapter_sections:
342
+ book_content += f"\n# {title}\n"
343
+ generated_chapter = generate_book_chapter(title, chapter_sections)
344
+ book_content += generated_chapter + "\n"
345
+ start_idx = end_idx
346
+
347
+ # Generate Book DOCX
348
+ try:
349
+ book_docx_path = create_book_docx(book_title, book_content)
350
  except Exception as e:
351
+ print(f"Book DOCX error: {e}")
352
+ book_docx_path = None
353
 
354
+ # Generate Audio
 
 
 
 
355
  try:
356
+ loop = asyncio.new_event_loop()
357
+ asyncio.set_event_loop(loop)
358
+ audio_path = loop.run_until_complete(generate_tts_audio(book_content))
359
+ loop.close()
 
 
 
 
 
360
  except Exception as e:
361
+ print(f"Audio error: {e}")
362
+ audio_path = None
363
+
364
+ return output_text, audio_path, evidence_docx_path, book_content, book_docx_path
365
 
366
+ # --- Gradio UI ---
367
 
368
+ with gr.Blocks(title="File Seamstresses Framework - PhD Edition") as demo:
369
+ gr.Markdown("# 🧵 File Seamstresses Framework - PhD Edition")
370
+ gr.Markdown("Intelligent merging, aggressive deduplication, chronology tracking, breach placement, and **PhD-level book generation** with TTS.")
371
+
372
+ with gr.Row():
373
+ with gr.Column(scale=1):
374
+ file_input = gr.File(label="Upload Files", file_count="multiple")
375
+ book_title_input = gr.Textbox(label="Book Title", value="A Scholarly Examination of Evidence")
376
+ chapter_titles_input = gr.Textbox(label="Chapter Titles (comma-separated)", value="Introduction, Background, Analysis of Breaches, Case Studies, Conclusion")
377
+ process_btn = gr.Button("🧵 Process, Sew, and Write Book", variant="primary")
378
+
379
+ with gr.Column(scale=2):
380
+ gr.Markdown("## Processed Evidence Preview")
381
+ text_output = gr.Markdown(label="Consolidated Evidence")
382
+ with gr.Row():
383
+ audio_output = gr.Audio(label="Audio Version (TTS)", type="filepath")
384
+ file_output = gr.File(label="Download Master Evidence Document (.docx)")
385
+
386
+ gr.Markdown("## Generated Book Content")
387
+ book_output = gr.Markdown(label="PhD-Level Book Draft")
388
+ book_file_output = gr.File(label="Download PhD-Level Book (.docx)")
389
+
390
+ process_btn.click(
391
+ fn=run_app,
392
+ inputs=[file_input, book_title_input, chapter_titles_input],
393
+ outputs=[text_output, audio_output, file_output, book_output, book_file_output]
394
+ )
395
 
396
+ # Launch
397
+ if __name__ == "__main__":
398
+ demo.launch(server_name="0.0.0.0")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
+ import os
401
+ import re
402
+ import asyncio
403
+ import pandas as pd
404
+ import numpy as np
405
+ import gradio as gr
406
+ import pdfplumber
407
+ import dateparser
408
+ import edge_tts
409
+ from datetime import datetime
410
+ from sentence_transformers import SentenceTransformer, util
411
+ from docx import Document
412
+ from docx.shared import Pt, Inches, RGBColor
413
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
414
+ from docx.oxml.ns import qn
415
+ from docx.oxml import OxmlElement
416
+ from itertools import groupby
417
+
418
+ # Force model to cache properly on Hugging Face
419
+ os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
420
+ os.environ["SENTENCE_TRANSFORMERS_HOME"] = "/tmp/model_cache"
421
+
422
+ # Initialize the Sentence Transformer model
423
+ model = SentenceTransformer('all-MiniLM-L6-v2')
424
+
425
+ # --- Utility Functions ---
426
+
427
+ def extract_text_from_file(file_path):
428
+ """Extracts text from PDF, DOCX, or TXT files."""
429
+ ext = os.path.splitext(file_path)[1].lower()
430
+ text = ""
431
+ try:
432
+ if ext == ".pdf":
433
+ with pdfplumber.open(file_path) as pdf:
434
+ for page in pdf.pages:
435
+ page_text = page.extract_text()
436
+ if page_text:
437
+ text += page_text + "\n"
438
+ elif ext == ".docx":
439
+ doc = Document(file_path)
440
+ text = "\n".join([para.text for para in doc.paragraphs])
441
+ elif ext == ".txt":
442
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
443
+ text = f.read()
444
+ except Exception as e:
445
+ print(f"Error extracting from {file_path}: {e}")
446
+ return text
447
+
448
+ def extract_date(text):
449
+ """Attempts to extract a date from a text snippet."""
450
+ date_patterns = [
451
+ r'\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b',
452
+ r'\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]* \d{1,2},? \d{4}\b',
453
+ r'\b\d{4}-\d{2}-\d{2}\b'
454
+ ]
455
+ for pattern in date_patterns:
456
+ match = re.search(pattern, text, re.IGNORECASE)
457
+ if match:
458
+ parsed_date = dateparser.parse(match.group())
459
+ if parsed_date:
460
+ return parsed_date
461
+ return None
462
+
463
+ def detect_breach(text):
464
+ """Detects if a text snippet describes a breach or violation."""
465
+ breach_keywords = [
466
+ "breach", "violation", "failure", "non-compliance", "infringement",
467
+ "unauthorized", "leak", "compromise", "illegal", "incident", "misconduct"
468
+ ]
469
+ for keyword in breach_keywords:
470
+ if keyword.lower() in text.lower():
471
+ return True
472
+ return False
473
+
474
+ def create_docx(sections, output_path="Master_Evidence_Document.docx"):
475
+ """Creates a professionally formatted DOCX file with cover, TOC, and monthly grouping."""
476
+ doc = Document()
477
+
478
+ # Style settings
479
+ style = doc.styles['Normal']
480
+ font = style.font
481
+ font.name = 'Calibri'
482
+ font.size = Pt(11)
483
+ style.paragraph_format.space_after = Pt(6)
484
+
485
+ # Cover page
486
+ title = doc.add_heading('Master Evidence Document', 0)
487
+ title.alignment = WD_ALIGN_PARAGRAPH.CENTER
488
+ subtitle = doc.add_heading('Consolidated Chronology and Identified Breaches', level=1)
489
+ subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
490
+ date_para = doc.add_paragraph()
491
+ date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
492
+ date_para.add_run(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
493
+ doc.add_page_break()
494
+
495
+ # Table of Contents
496
+ doc.add_heading('Table of Contents', level=1)
497
+ para = doc.add_paragraph()
498
+ run = para.add_run()
499
+ fldChar1 = OxmlElement('w:fldChar')
500
+ fldChar1.set(qn('w:fldCharType'), 'begin')
501
+ run._r.append(fldChar1)
502
+ instrText = OxmlElement('w:instrText')
503
+ instrText.set(qn('xml:space'), 'preserve')
504
+ instrText.text = ' TOC \\o "1-3" \\h \\z \\u '
505
+ run._r.append(instrText)
506
+ fldChar2 = OxmlElement('w:fldChar')
507
+ fldChar2.set(qn('w:fldCharType'), 'separate')
508
+ run._r.append(fldChar2)
509
+ run2 = para.add_run('Right-click and choose "Update Field" to generate the Table of Contents')
510
+ fldChar3 = OxmlElement('w:fldChar')
511
+ fldChar3.set(qn('w:fldCharType'), 'end')
512
+ run2._r.append(fldChar3)
513
+ doc.add_page_break()
514
+
515
+ # Group dated sections by month
516
+ dated_sections = [s for s in sections if s['date']]
517
+ undated_sections = [s for s in sections if not s['date']]
518
+
519
+ def get_month_key(section):
520
+ return section['date'].strftime('%B %Y')
521
+
522
+ grouped = {}
523
+ for month, group in groupby(dated_sections, key=get_month_key):
524
+ grouped[month] = list(group)
525
+
526
+ for month, items in grouped.items():
527
+ month_heading = doc.add_heading(month, level=2)
528
+ for item in items:
529
+ p = doc.add_paragraph()
530
+ date_run = p.add_run(f"🔹 {item['date'].strftime('%Y-%m-%d')} ")
531
+ date_run.bold = True
532
+ date_run.font.size = Pt(10)
533
+ if item['is_breach']:
534
+ breach_run = p.add_run("⚠️ BREACH IDENTIFIED ")
535
+ breach_run.bold = True
536
+ breach_run.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
537
+ p.add_run(item['content'])
538
+ doc.add_paragraph("_" * 40)
539
+ doc.add_paragraph()
540
+
541
+ # Undated sections
542
+ if undated_sections:
543
+ doc.add_heading('Undated Entries', level=2)
544
+ for item in undated_sections:
545
+ p = doc.add_paragraph()
546
+ if item['is_breach']:
547
+ breach_run = p.add_run("⚠️ BREACH IDENTIFIED ")
548
+ breach_run.bold = True
549
+ breach_run.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
550
+ p.add_run(item['content'])
551
+
552
+ doc.save(output_path)
553
+ return output_path
554
+
555
+ # --- Core Logic ---
556
+
557
+ def process_files(files):
558
+ if not files:
559
+ return "No files uploaded.", []
560
+
561
+ all_sections = []
562
+ for file in files:
563
+ file_path = file.name
564
+ file_name = os.path.basename(file_path)
565
+ text = extract_text_from_file(file_path)
566
+ paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
567
+ for p in paragraphs:
568
+ all_sections.append({
569
+ "content": p,
570
+ "source": file_name,
571
+ "date": extract_date(p),
572
+ "is_breach": detect_breach(p)
573
+ })
574
+
575
+ if not all_sections:
576
+ return "No content extracted.", []
577
+
578
+ # Deduplication
579
+ contents = [s["content"] for s in all_sections]
580
+ embeddings = model.encode(contents, convert_to_tensor=True)
581
+ unique_sections = []
582
+ processed_indices = set()
583
+ for i in range(len(all_sections)):
584
+ if i in processed_indices:
585
+ continue
586
+ cosine_scores = util.cos_sim(embeddings[i], embeddings)[0]
587
+ similar_indices = [j for j, score in enumerate(cosine_scores) if score > 0.85]
588
+ best_idx = i
589
+ max_len = len(all_sections[i]["content"])
590
+ for idx in similar_indices:
591
+ if len(all_sections[idx]["content"]) > max_len:
592
+ max_len = len(all_sections[idx]["content"])
593
+ best_idx = idx
594
+ processed_indices.add(idx)
595
+ unique_sections.append(all_sections[best_idx])
596
+
597
+ # Sort by date
598
+ dated = [s for s in unique_sections if s["date"]]
599
+ undated = [s for s in unique_sections if not s["date"]]
600
+ dated.sort(key=lambda x: x["date"])
601
+ final_sections = dated + undated
602
+
603
+ # Text output for preview
604
+ output_text = "# Master Evidence Document\n\n"
605
+ for s in final_sections:
606
+ prefix = ""
607
+ if s["date"]:
608
+ prefix += f"**[{s['date'].strftime('%Y-%m-%d')}]** "
609
+ if s["is_breach"]:
610
+ prefix += "⚠️ **[BREACH IDENTIFIED]** "
611
+ output_text += f"{prefix}{s['content']}\n\n---\n\n"
612
+
613
+ return output_text, final_sections
614
+
615
+ # --- TTS ---
616
+
617
+ async def generate_tts_audio(text):
618
+ try:
619
+ short_text = text[:5000]
620
+ communicate = edge_tts.Communicate(short_text, "en-US-GuyNeural")
621
+ output_file = "output_audio.mp3"
622
+ await communicate.save(output_file)
623
+ return output_file
624
  except Exception as e:
625
+ print(f"TTS error: {e}")
626
  return None
627
 
628
+ # --- Main Runner ---
629
 
630
+ def run_app(files):
631
+ output_text, final_sections = process_files(files)
632
+ if not final_sections:
633
+ return output_text, None, None
634
+
635
+ # Generate DOCX
636
  try:
637
+ docx_path = create_docx(final_sections)
638
+ except Exception as e:
639
+ print(f"DOCX error: {e}")
640
+ docx_path = None
641
+
642
+ # Generate Audio
643
+ try:
644
+ loop = asyncio.new_event_loop()
645
+ asyncio.set_event_loop(loop)
646
+ audio_path = loop.run_until_complete(generate_tts_audio(output_text))
647
+ loop.close()
648
+ except Exception as e:
649
+ print(f"Audio error: {e}")
650
+ audio_path = None
651
+
652
+ return output_text, audio_path, docx_path
653
 
654
+ # --- Gradio UI ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
655
 
656
+ with gr.Blocks(title="File Seamstresses Framework") as demo:
657
+ gr.Markdown("# 🧵 File Seamstresses Framework")
658
+ gr.Markdown("Intelligent merging, deduplication, chronology tracking, breach placement, and TTS.")
659
+
660
+ with gr.Row():
661
+ with gr.Column(scale=1):
662
+ file_input = gr.File(label="Upload Files", file_count="multiple")
663
+ process_btn = gr.Button("🧵 Process and Sew Files", variant="primary")
664
+
665
+ with gr.Column(scale=2):
666
+ text_output = gr.Markdown(label="Preview")
667
+ with gr.Row():
668
+ audio_output = gr.Audio(label="Audio Version (TTS)", type="filepath")
669
+ file_output = gr.File(label="Download Master Document (.docx)")
670
 
671
+ process_btn.click(
672
+ fn=run_app,
673
+ inputs=[file_input],
674
+ outputs=[text_output, audio_output, file_output]
675
+ )
 
 
 
676
 
677
+ # Launch
678
+ if __name__ == "__main__":
679
+ demo.launch(server_name="0.0.0.0")
680
 
681
+ import os
682
+ import re
683
+ import asyncio
684
+ import pandas as pd
685
+ import numpy as np
686
+ import gradio as gr
687
+ import pdfplumber
688
+ import dateparser
689
+ import edge_tts
690
+ from datetime import datetime
691
+ from sentence_transformers import SentenceTransformer, util
692
+ from docx import Document
693
+ from docx.shared import Pt, Inches, RGBColor
694
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
695
+ from docx.oxml.ns import qn
696
+ from docx.oxml import OxmlElement
697
+ from itertools import groupby
698
+
699
+ # Force model to cache properly on Hugging Face
700
+ os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
701
+ os.environ["SENTENCE_TRANSFORMERS_HOME"] = "/tmp/model_cache"
702
+
703
+ # Initialize the Sentence Transformer model
704
+ model = SentenceTransformer('all-MiniLM-L6-v2')
705
+
706
+ # --- Utility Functions ---
707
+
708
+ def extract_text_from_file(file_path):
709
+ """Extracts text from PDF, DOCX, or TXT files."""
710
+ ext = os.path.splitext(file_path)[1].lower()
711
+ text = ""
712
+ try:
713
+ if ext == ".pdf":
714
+ with pdfplumber.open(file_path) as pdf:
715
+ for page in pdf.pages:
716
+ page_text = page.extract_text()
717
+ if page_text:
718
+ text += page_text + "\n"
719
+ elif ext == ".docx":
720
+ doc = Document(file_path)
721
+ text = "\n".join([para.text for para in doc.paragraphs])
722
+ elif ext == ".txt":
723
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
724
+ text = f.read()
725
+ except Exception as e:
726
+ print(f"Error extracting from {file_path}: {e}")
727
+ return text
728
+
729
+ def extract_date(text):
730
+ """Attempts to extract a date from a text snippet."""
731
+ date_patterns = [
732
+ r'\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b',
733
+ r'\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]* \d{1,2},? \d{4}\b',
734
+ r'\b\d{4}-\d{2}-\d{2}\b'
735
+ ]
736
+ for pattern in date_patterns:
737
+ match = re.search(pattern, text, re.IGNORECASE)
738
+ if match:
739
+ parsed_date = dateparser.parse(match.group())
740
+ if parsed_date:
741
+ return parsed_date
742
+ return None
743
+
744
+ def detect_breach(text):
745
+ """Detects if a text snippet describes a breach or violation."""
746
+ breach_keywords = [
747
+ "breach", "violation", "failure", "non-compliance", "infringement",
748
+ "unauthorized", "leak", "compromise", "illegal", "incident", "misconduct"
749
+ ]
750
+ for keyword in breach_keywords:
751
+ if keyword.lower() in text.lower():
752
+ return True
753
+ return False
754
+
755
+ def create_docx(sections, output_path="Master_Evidence_Document.docx"):
756
+ """Creates a professionally formatted DOCX file with cover, TOC, and monthly grouping."""
757
+ doc = Document()
758
+
759
+ # Style settings
760
+ style = doc.styles['Normal']
761
+ font = style.font
762
+ font.name = 'Calibri'
763
+ font.size = Pt(11)
764
+ style.paragraph_format.space_after = Pt(6)
765
+
766
+ # Cover page
767
+ title = doc.add_heading('Master Evidence Document', 0)
768
+ title.alignment = WD_ALIGN_PARAGRAPH.CENTER
769
+ subtitle = doc.add_heading('Consolidated Chronology and Identified Breaches', level=1)
770
+ subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
771
+ date_para = doc.add_paragraph()
772
+ date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
773
+ date_para.add_run(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
774
+ doc.add_page_break()
775
+
776
+ # Table of Contents
777
+ doc.add_heading('Table of Contents', level=1)
778
+ para = doc.add_paragraph()
779
+ run = para.add_run()
780
+ fldChar1 = OxmlElement('w:fldChar')
781
+ fldChar1.set(qn('w:fldCharType'), 'begin')
782
+ run._r.append(fldChar1)
783
+ instrText = OxmlElement('w:instrText')
784
+ instrText.set(qn('xml:space'), 'preserve')
785
+ instrText.text = ' TOC \\o "1-3" \\h \\z \\u '
786
+ run._r.append(instrText)
787
+ fldChar2 = OxmlElement('w:fldChar')
788
+ fldChar2.set(qn('w:fldCharType'), 'separate')
789
+ run._r.append(fldChar2)
790
+ run2 = para.add_run('Right-click and choose "Update Field" to generate the Table of Contents')
791
+ fldChar3 = OxmlElement('w:fldChar')
792
+ fldChar3.set(qn('w:fldCharType'), 'end')
793
+ run2._r.append(fldChar3)
794
+ doc.add_page_break()
795
+
796
+ # Group dated sections by month
797
+ dated_sections = [s for s in sections if s['date']]
798
+ undated_sections = [s for s in sections if not s['date']]
799
+
800
+ def get_month_key(section):
801
+ return section['date'].strftime('%B %Y')
802
+
803
+ grouped = {}
804
+ for month, group in groupby(dated_sections, key=get_month_key):
805
+ grouped[month] = list(group)
806
+
807
+ for month, items in grouped.items():
808
+ month_heading = doc.add_heading(month, level=2)
809
+ for item in items:
810
+ p = doc.add_paragraph()
811
+ date_run = p.add_run(f"🔹 {item['date'].strftime('%Y-%m-%d')} ")
812
+ date_run.bold = True
813
+ date_run.font.size = Pt(10)
814
+ if item['is_breach']:
815
+ breach_run = p.add_run("⚠️ BREACH IDENTIFIED ")
816
+ breach_run.bold = True
817
+ breach_run.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
818
+ p.add_run(item['content'])
819
+ doc.add_paragraph("_" * 40)
820
+ doc.add_paragraph()
821
+
822
+ # Undated sections
823
+ if undated_sections:
824
+ doc.add_heading('Undated Entries', level=2)
825
+ for item in undated_sections:
826
+ p = doc.add_paragraph()
827
+ if item['is_breach']:
828
+ breach_run = p.add_run("⚠️ BREACH IDENTIFIED ")
829
+ breach_run.bold = True
830
+ breach_run.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
831
+ p.add_run(item['content'])
832
+
833
+ doc.save(output_path)
834
+ return output_path
835
+
836
+ # --- Core Logic ---
837
+
838
+ def process_files(files):
839
+ if not files:
840
+ return "No files uploaded.", []
841
+
842
+ all_sections = []
843
+ for file in files:
844
+ file_path = file.name
845
+ file_name = os.path.basename(file_path)
846
+ text = extract_text_from_file(file_path)
847
+ paragraphs = [p.strip() for p in text.split('\n\n') if p.strip()]
848
+ for p in paragraphs:
849
+ all_sections.append({
850
+ "content": p,
851
+ "source": file_name,
852
+ "date": extract_date(p),
853
+ "is_breach": detect_breach(p)
854
+ })
855
+
856
+ if not all_sections:
857
+ return "No content extracted.", []
858
+
859
+ # Deduplication
860
+ contents = [s["content"] for s in all_sections]
861
+ embeddings = model.encode(contents, convert_to_tensor=True)
862
+ unique_sections = []
863
+ processed_indices = set()
864
+ for i in range(len(all_sections)):
865
+ if i in processed_indices:
866
+ continue
867
+ cosine_scores = util.cos_sim(embeddings[i], embeddings)[0]
868
+ similar_indices = [j for j, score in enumerate(cosine_scores) if score > 0.85]
869
+ best_idx = i
870
+ max_len = len(all_sections[i]["content"])
871
+ for idx in similar_indices:
872
+ if len(all_sections[idx]["content"]) > max_len:
873
+ max_len = len(all_sections[idx]["content"])
874
+ best_idx = idx
875
+ processed_indices.add(idx)
876
+ unique_sections.append(all_sections[best_idx])
877
+
878
+ # Sort by date
879
+ dated = [s for s in unique_sections if s["date"]]
880
+ undated = [s for s in unique_sections if not s["date"]]
881
+ dated.sort(key=lambda x: x["date"])
882
+ final_sections = dated + undated
883
+
884
+ # Text output for preview
885
+ output_text = "# Master Evidence Document\n\n"
886
+ for s in final_sections:
887
+ prefix = ""
888
+ if s["date"]:
889
+ prefix += f"**[{s['date'].strftime('%Y-%m-%d')}]** "
890
+ if s["is_breach"]:
891
+ prefix += "⚠️ **[BREACH IDENTIFIED]** "
892
+ output_text += f"{prefix}{s['content']}\n\n---\n\n"
893
+
894
+ return output_text, final_sections
895
+
896
+ # --- TTS ---
897
+
898
+ async def generate_tts_audio(text):
899
+ try:
900
+ short_text = text[:5000]
901
+ communicate = edge_tts.Communicate(short_text, "en-US-GuyNeural")
902
+ output_file = "output_audio.mp3"
903
+ await communicate.save(output_file)
904
+ return output_file
905
  except Exception as e:
906
+ print(f"TTS error: {e}")
907
  return None
908
 
909
+ # --- Main Runner ---
910
 
911
+ def run_app(files):
912
+ output_text, final_sections = process_files(files)
913
+ if not final_sections:
914
+ return output_text, None, None
915
+
916
+ # Generate DOCX
917
  try:
918
+ docx_path = create_docx(final_sections)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
919
  except Exception as e:
920
+ print(f"DOCX error: {e}")
921
+ docx_path = None
922
+
923
+ # Generate Audio
924
+ try:
925
+ loop = asyncio.new_event_loop()
926
+ asyncio.set_event_loop(loop)
927
+ audio_path = loop.run_until_complete(generate_tts_audio(output_text))
928
+ loop.close()
929
+ except Exception as e:
930
+ print(f"Audio error: {e}")
931
+ audio_path = None
932
+
933
+ return output_text, audio_path, docx_path
934
 
935
+ # --- Gradio UI ---
 
 
 
 
 
936
 
937
+ with gr.Blocks(title="File Seamstresses Framework") as demo:
938
+ gr.Markdown("# 🧵 File Seamstresses Framework")
939
+ gr.Markdown("Intelligent merging, deduplication, chronology tracking, breach placement, and TTS.")
940
+
941
  with gr.Row():
942
+ with gr.Column(scale=1):
943
+ file_input = gr.File(label="Upload Files", file_count="multiple")
944
+ process_btn = gr.Button("🧵 Process and Sew Files", variant="primary")
945
+
946
+ with gr.Column(scale=2):
947
+ text_output = gr.Markdown(label="Preview")
 
 
 
 
 
 
 
 
 
 
 
 
 
948
  with gr.Row():
949
+ audio_output = gr.Audio(label="Audio Version (TTS)", type="filepath")
950
+ file_output = gr.File(label="Download Master Document (.docx)")
 
 
 
 
 
 
 
 
 
 
 
951
 
952
+ process_btn.click(
953
+ fn=run_app,
954
+ inputs=[file_input],
955
+ outputs=[text_output, audio_output, file_output]
956
+ )
 
 
 
957
 
958
+ # Launch
959
  if __name__ == "__main__":
960
+ demo.launch(server_name="0.0.0.0")