Basementup commited on
Commit
0433e50
·
verified ·
1 Parent(s): 445e6ea

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +63 -236
app.py CHANGED
@@ -1,10 +1,8 @@
1
-
2
  import gradio as gr
3
  import json
4
  import os
5
  import time
6
  import tempfile
7
- import subprocess
8
  import re
9
  import requests
10
  from pathlib import Path
@@ -13,18 +11,8 @@ from typing import List, Dict
13
  from difflib import SequenceMatcher
14
  from huggingface_hub import InferenceClient
15
  from gtts import gTTS
16
-
17
- subprocess.run(["pip", "install", "pypdf", "-q"], capture_output=True)
18
-
19
- try:
20
- from pypdf import PdfReader
21
- except Exception:
22
- PdfReader = None
23
-
24
- try:
25
- import PyPDF2
26
- except Exception:
27
- PyPDF2 = None
28
 
29
 
30
  SAVE_DIR = Path("/data/saved_docs")
@@ -33,8 +21,12 @@ SAVE_DIR.mkdir(parents=True, exist_ok=True)
33
  MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
34
 
35
 
 
 
 
 
36
  def get_client():
37
- token = os.environ.get("HF_TOKEN", "") or os.environ.get("HUGGINGFACE_TOKEN", "")
38
  return InferenceClient(model=MODEL, token=token if token else None)
39
 
40
 
@@ -46,12 +38,9 @@ def call_llama(prompt, max_tokens=1200):
46
  max_tokens=max_tokens,
47
  temperature=0.4,
48
  )
49
-
50
  if hasattr(response, "choices") and response.choices:
51
  return response.choices[0].message.content
52
-
53
  return f"No model output returned. Raw response: {response}"
54
-
55
  except Exception as e:
56
  return f"Model error: {e}"
57
 
@@ -60,47 +49,37 @@ def make_tts(text):
60
  try:
61
  if not text or not str(text).strip():
62
  return None
63
-
64
  clean_text = str(text).strip()[:5000]
65
-
66
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
67
  tmp.close()
68
-
69
  tts = gTTS(text=clean_text, lang="en", slow=False)
70
  tts.save(tmp.name)
71
-
72
  return tmp.name
73
-
74
  except Exception as e:
75
  print(f"TTS error: {e}")
76
  return None
77
 
78
 
 
 
 
 
79
  def extract_pdf_text(path):
80
  if not path:
81
  return ""
82
-
83
  if hasattr(path, "name"):
84
  path = path.name
85
-
86
  try:
87
- if PdfReader is None:
88
- return "[PDF reader unavailable]"
89
-
90
  reader = PdfReader(path)
91
  pages = []
92
-
93
  for i, page in enumerate(reader.pages):
94
  text = page.extract_text() or ""
95
  if text.strip():
96
  pages.append(f"--- Page {i + 1} ---\n{text.strip()}")
97
-
98
  if not pages:
99
  return "[No extractable text — may be a scanned PDF]"
100
-
101
  full = "\n\n".join(pages)
102
  return full[:14000] + "\n\n[...truncated]" if len(full) > 14000 else full
103
-
104
  except Exception as e:
105
  return f"[PDF read error: {e}]"
106
 
@@ -179,28 +158,18 @@ def save_phoenix_analysis(r1, r2, r3, r4):
179
 
180
  try:
181
  index_path = SAVE_DIR / "index.json"
182
- library = json.loads(index_path.read_text()) if index_path.exists() else []
183
-
184
- entry = {
185
- "id": doc_id,
186
- "title": f"Analysis — {ts}",
187
- "content": content,
188
- "saved_at": ts,
189
- }
190
-
191
  library.insert(0, entry)
192
  index_path.write_text(json.dumps(library, indent=2), encoding="utf-8")
193
  (SAVE_DIR / f"{doc_id}.txt").write_text(content, encoding="utf-8")
194
-
195
  return f"Saved: Analysis — {ts}"
196
-
197
  except Exception as e:
198
  return f"Save error: {e}"
199
 
200
 
201
  def load_library():
202
  index_path = SAVE_DIR / "index.json"
203
-
204
  try:
205
  return json.loads(index_path.read_text(encoding="utf-8")) if index_path.exists() else []
206
  except Exception:
@@ -213,7 +182,6 @@ def lib_choices(library):
213
 
214
  def choice_to_entry(library, choice):
215
  choices = lib_choices(library)
216
-
217
  try:
218
  return library[choices.index(choice)]
219
  except Exception:
@@ -227,73 +195,56 @@ def lib_refresh():
227
 
228
  def lib_view(selection):
229
  library = load_library()
230
-
231
  if not selection:
232
  return ""
233
-
234
  entry = choice_to_entry(library, selection)
235
  return entry["content"] if entry else "Not found."
236
 
237
 
238
  def lib_download(selection):
239
  library = load_library()
240
-
241
  if not selection:
242
  return gr.update(visible=False), "Select a document first."
243
-
244
  entry = choice_to_entry(library, selection)
245
-
246
  if not entry:
247
  return gr.update(visible=False), "Not found."
248
-
249
  file_path = SAVE_DIR / f"{entry['id']}.txt"
250
-
251
  if file_path.exists():
252
  return gr.update(value=str(file_path), visible=True), "Ready."
253
-
254
  return gr.update(visible=False), "File missing."
255
 
256
 
257
  def lib_delete(selection):
258
  library = load_library()
259
-
260
  if not selection:
261
  return gr.update(choices=lib_choices(library), value=None), "Select a document."
262
-
263
  entry = choice_to_entry(library, selection)
264
-
265
  if not entry:
266
  return gr.update(choices=lib_choices(library), value=None), "Not found."
267
-
268
  library = [x for x in library if x["id"] != entry["id"]]
269
  (SAVE_DIR / "index.json").write_text(json.dumps(library, indent=2), encoding="utf-8")
270
-
271
  file_path = SAVE_DIR / f"{entry['id']}.txt"
272
-
273
  if file_path.exists():
274
  file_path.unlink()
275
-
276
  return gr.update(choices=lib_choices(library), value=None), "Deleted."
277
 
278
 
 
 
 
 
279
  class DocumentParser:
280
  @staticmethod
281
  def extract_text_from_pdf(file_path: str) -> str:
282
  try:
283
- if PyPDF2 is None:
284
- return "Error reading PDF: PyPDF2 unavailable"
285
-
286
  with open(file_path, "rb") as f:
287
  reader = PyPDF2.PdfReader(f)
288
  text = ""
289
-
290
  for page in reader.pages:
291
  extracted = page.extract_text()
292
  if extracted:
293
  text += extracted + "\n"
294
-
295
  return text
296
-
297
  except Exception as e:
298
  return f"Error reading PDF: {str(e)}"
299
 
@@ -301,10 +252,8 @@ class DocumentParser:
301
  def extract_text_from_docx(file_path: str) -> str:
302
  try:
303
  from docx import Document
304
-
305
  doc = Document(file_path)
306
  return "\n\n".join([p.text for p in doc.paragraphs if p.text.strip()])
307
-
308
  except Exception as e:
309
  return f"Error reading DOCX: {str(e)}"
310
 
@@ -312,19 +261,14 @@ class DocumentParser:
312
  def extract_text_from_file(file_path: str) -> str:
313
  if not file_path:
314
  return "Error: No file path provided"
315
-
316
  try:
317
  lower = file_path.lower()
318
-
319
  if lower.endswith(".pdf"):
320
  return DocumentParser.extract_text_from_pdf(file_path)
321
-
322
  if lower.endswith(".docx"):
323
  return DocumentParser.extract_text_from_docx(file_path)
324
-
325
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
326
  return f.read()
327
-
328
  except Exception as e:
329
  return f"Error reading file: {str(e)}"
330
 
@@ -332,10 +276,8 @@ class DocumentParser:
332
  def parse_document(text: str) -> Dict:
333
  paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
334
  sentences = []
335
-
336
  for paragraph in paragraphs:
337
  sentences.extend([s.strip() + "." for s in paragraph.split(".") if s.strip()])
338
-
339
  return {
340
  "full_text": text,
341
  "paragraphs": paragraphs,
@@ -356,82 +298,62 @@ class ForensicAnalyzer:
356
  @staticmethod
357
  def find_duplicates(docs: List[Dict]) -> List[Dict]:
358
  duplicates = []
359
-
360
  for i, doc1 in enumerate(docs):
361
  for j, doc2 in enumerate(docs[i + 1:], start=i + 1):
362
  for para1 in doc1["paragraphs"]:
363
  for para2 in doc2["paragraphs"]:
364
  if len(para1) < 20 or len(para2) < 20:
365
  continue
366
-
367
  ratio = SequenceMatcher(None, para1, para2).ratio()
368
-
369
  if ratio > 0.85:
370
- duplicates.append(
371
- {
372
- "doc1": i,
373
- "doc2": j,
374
- "similarity": ratio,
375
- "text": para1[:5000] + "...",
376
- "type": "duplicate" if ratio > 0.95 else "near_duplicate",
377
- }
378
- )
379
-
380
  return duplicates
381
 
382
  @staticmethod
383
  def find_unique_content(docs: List[Dict]) -> List[Dict]:
384
  unique_items = []
385
-
386
  for i, doc in enumerate(docs):
387
  for para in doc["paragraphs"]:
388
  if len(para) < 20:
389
  continue
390
-
391
  is_unique = True
392
-
393
  for j, other_doc in enumerate(docs):
394
  if i == j:
395
  continue
396
-
397
  for other_para in other_doc["paragraphs"]:
398
  if SequenceMatcher(None, para, other_para).ratio() > 0.8:
399
  is_unique = False
400
  break
401
-
402
  if not is_unique:
403
  break
404
-
405
  if is_unique:
406
- unique_items.append(
407
- {
408
- "doc_index": i,
409
- "content": para,
410
- "length": len(para),
411
- "type": "unique_paragraph",
412
- }
413
- )
414
-
415
  return unique_items
416
 
417
  @staticmethod
418
  def detect_risk_language(text: str) -> List[Dict]:
419
  findings = []
420
-
421
  for risk_type, pattern in ForensicAnalyzer.RISK_PATTERNS.items():
422
  for match in re.finditer(pattern, text, re.IGNORECASE):
423
  start = max(0, match.start() - 50)
424
  end = min(len(text), match.end() + 50)
425
-
426
- findings.append(
427
- {
428
- "type": risk_type,
429
- "matched_text": match.group(),
430
- "context": text[start:end],
431
- "position": match.start(),
432
- }
433
- )
434
-
435
  return findings
436
 
437
  @staticmethod
@@ -452,84 +374,63 @@ class DocumentMerger:
452
  def select_best_content(duplicates, docs):
453
  best_versions = []
454
  processed = set()
455
-
456
  for duplicate in duplicates:
457
  key = tuple(sorted([duplicate["doc1"], duplicate["doc2"]]))
458
-
459
  if key in processed:
460
  continue
461
-
462
  processed.add(key)
463
-
464
  doc1_text = docs[duplicate["doc1"]]["full_text"]
465
  doc2_text = docs[duplicate["doc2"]]["full_text"]
466
-
467
  best_versions.append(doc1_text if len(doc1_text) > len(doc2_text) else doc2_text)
468
-
469
  return best_versions
470
 
471
  @staticmethod
472
  def merge_documents(docs, analysis):
473
  output = []
474
-
475
  output.append("MASTER EVIDENCE DOCUMENT")
476
  output.append("=" * 80)
477
  output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
478
  output.append(f"Source Documents: {len(docs)}")
479
  output.append("=" * 80 + "\n")
480
-
481
  output.append("EXECUTIVE SUMMARY")
482
  output.append("-" * 80)
483
  output.append(f"Total Duplicates Found: {len(analysis['duplicates'])}")
484
  output.append(f"Unique Content Items: {len(analysis['unique_content'])}")
485
  output.append(f"Risk Language Instances: {len(analysis['risk_language'])}\n")
486
-
487
  output.append("CONSOLIDATED CONTENT")
488
  output.append("-" * 80)
489
-
490
  best_versions = DocumentMerger.select_best_content(analysis["duplicates"], docs)
491
-
492
  if best_versions:
493
  for i, content in enumerate(best_versions, 1):
494
  output.append(f"\n[Section {i}]")
495
  output.append(content[:50000] + "..." if len(content) > 50000 else content)
496
  else:
497
  output.append("No duplicate content found across documents.")
498
-
499
  output.append("\n\nUNIQUE FINDINGS")
500
  output.append("-" * 80)
501
-
502
  for item in analysis["unique_content"][:100]:
503
  output.append(f"\n[From Document {item['doc_index'] + 1}]")
504
  content = item["content"]
505
  output.append(content[:50000] + "..." if len(content) > 50000 else content)
506
-
507
  output.append("\n\nRISK LANGUAGE ANALYSIS")
508
  output.append("-" * 80)
509
-
510
  risk_summary = {}
511
-
512
  for risk in analysis["risk_language"]:
513
  risk_summary[risk["type"]] = risk_summary.get(risk["type"], 0) + 1
514
-
515
  if risk_summary:
516
  for risk_type, count in risk_summary.items():
517
  output.append(f"{risk_type.replace('_', ' ').title()}: {count} instances")
518
  else:
519
  output.append("No risk language patterns detected.")
520
-
521
  output.append("\n" + "=" * 80)
522
  output.append("END OF MASTER EVIDENCE DOCUMENT")
523
-
524
  return "\n".join(output)
525
 
526
 
527
  def ai_summarise(merged_doc: str, analysis: Dict) -> str:
528
  try:
529
- hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
530
-
531
  risk_summary = {}
532
-
533
  for risk in analysis.get("risk_language", []):
534
  risk_summary[risk["type"]] = risk_summary.get(risk["type"], 0) + 1
535
 
@@ -544,7 +445,6 @@ Document excerpt:
544
  """.strip()
545
 
546
  headers = {"Content-Type": "application/json"}
547
-
548
  if hf_token:
549
  headers["Authorization"] = f"Bearer {hf_token}"
550
 
@@ -564,18 +464,12 @@ Document excerpt:
564
 
565
  if response.status_code == 200:
566
  data = response.json()
567
-
568
  if isinstance(data, list) and data:
569
  summary = data[0].get("summary_text", "")
570
  risk_lines = "\n".join(
571
- [
572
- f"- {k.replace('_', ' ').title()}: {v} instances"
573
- for k, v in risk_summary.items()
574
- ]
575
  )
576
-
577
  return f"AI SUMMARY\n{'=' * 60}\n\n{summary}\n\nRisk Language Breakdown:\n{risk_lines}"
578
-
579
  return "Summary could not be extracted from model response."
580
 
581
  if response.status_code == 503:
@@ -590,7 +484,6 @@ Document excerpt:
590
  def export_txt(merged_doc: str, analysis_report: str) -> str:
591
  try:
592
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8")
593
-
594
  tmp.write("SEAMSTRESSES FRAMEWORK — EXPORT\n")
595
  tmp.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
596
  tmp.write("=" * 80 + "\n\n")
@@ -598,9 +491,7 @@ def export_txt(merged_doc: str, analysis_report: str) -> str:
598
  tmp.write("\n\n" + "=" * 80 + "\n\n")
599
  tmp.write(analysis_report)
600
  tmp.close()
601
-
602
  return tmp.name
603
-
604
  except Exception:
605
  return None
606
 
@@ -627,15 +518,7 @@ def export_pdf(merged_doc: str, analysis_report: str) -> str:
627
  )
628
 
629
  styles = getSampleStyleSheet()
630
-
631
- title_style = ParagraphStyle(
632
- "Title",
633
- parent=styles["Heading1"],
634
- fontSize=16,
635
- alignment=TA_CENTER,
636
- spaceAfter=6,
637
- )
638
-
639
  heading_style = ParagraphStyle(
640
  "Heading",
641
  parent=styles["Heading2"],
@@ -643,17 +526,9 @@ def export_pdf(merged_doc: str, analysis_report: str) -> str:
643
  textColor=colors.HexColor("#1a1a2e"),
644
  spaceAfter=4,
645
  )
646
-
647
- body_style = ParagraphStyle(
648
- "Body",
649
- parent=styles["Normal"],
650
- fontSize=9,
651
- leading=14,
652
- spaceAfter=3,
653
- )
654
 
655
  story = []
656
-
657
  story.append(Paragraph("Seamstresses Framework", title_style))
658
  story.append(Paragraph("Forensic Document Analysis Report", heading_style))
659
  story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", body_style))
@@ -664,7 +539,6 @@ def export_pdf(merged_doc: str, analysis_report: str) -> str:
664
  for line in merged_doc.split("\n"):
665
  if line.strip():
666
  safe = line.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
667
-
668
  if line.startswith("===") or line.startswith("---"):
669
  story.append(HRFlowable(width="100%", thickness=0.5, color=colors.lightgrey))
670
  elif line.isupper() and len(line) > 3 and not line.startswith("["):
@@ -679,17 +553,15 @@ def export_pdf(merged_doc: str, analysis_report: str) -> str:
679
  for line in analysis_report.split("\n"):
680
  if line.strip():
681
  safe = line.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
682
-
683
  if line.isupper() and len(line) > 3:
684
  story.append(Paragraph(safe, heading_style))
685
  else:
686
  story.append(Paragraph(safe, body_style))
687
 
688
  doc.build(story)
689
-
690
  return tmp.name
691
-
692
- except Exception:
693
  return None
694
 
695
 
@@ -702,7 +574,6 @@ def export_docx(merged_doc: str, analysis_report: str, summary: str) -> str:
702
  tmp.close()
703
 
704
  doc = DocxDocument()
705
-
706
  title = doc.add_heading("Seamstresses Framework", 0)
707
  title.alignment = WD_ALIGN_PARAGRAPH.CENTER
708
 
@@ -739,10 +610,9 @@ def export_docx(merged_doc: str, analysis_report: str, summary: str) -> str:
739
  doc.add_paragraph(line)
740
 
741
  doc.save(tmp.name)
742
-
743
  return tmp.name
744
-
745
- except Exception:
746
  return None
747
 
748
 
@@ -754,7 +624,6 @@ def process_documents(file1, file2, file3):
754
  for uploaded in raw_inputs:
755
  if uploaded is None:
756
  continue
757
-
758
  if isinstance(uploaded, dict):
759
  path = uploaded.get("path") or uploaded.get("name")
760
  elif hasattr(uploaded, "name"):
@@ -763,7 +632,6 @@ def process_documents(file1, file2, file3):
763
  path = uploaded
764
  else:
765
  path = str(uploaded)
766
-
767
  if path:
768
  files.append(path)
769
 
@@ -775,10 +643,8 @@ def process_documents(file1, file2, file3):
775
 
776
  for file_path in files:
777
  text = DocumentParser.extract_text_from_file(file_path)
778
-
779
  if not text or text.startswith("Error"):
780
  return f"Error: {text}", "", "", None, None, None
781
-
782
  docs.append(DocumentParser.parse_document(text))
783
  file_names.append(os.path.basename(file_path))
784
 
@@ -787,7 +653,6 @@ def process_documents(file1, file2, file3):
787
 
788
  analysis_report = f"FORENSIC ANALYSIS REPORT\n{'=' * 80}\n\n"
789
  analysis_report += f"FILES ANALYZED: {', '.join(file_names)}\n\n"
790
-
791
  analysis_report += f"DUPLICATE ANALYSIS\n{'-' * 80}\n"
792
  analysis_report += f"Total Duplicates Found: {len(analysis['duplicates'])}\n\n"
793
 
@@ -804,7 +669,6 @@ def process_documents(file1, file2, file3):
804
  analysis_report += f"- From Document {item['doc_index'] + 1}: {item['content'][:5000]}...\n"
805
 
806
  analysis_report += f"\nRISK LANGUAGE DETECTED\n{'-' * 80}\n"
807
-
808
  risk_types = {}
809
 
810
  for risk in analysis["risk_language"]:
@@ -826,13 +690,14 @@ def process_documents(file1, file2, file3):
826
 
827
  except Exception as e:
828
  import traceback
829
-
830
  return f"Error: {str(e)}\n\n{traceback.format_exc()}", "", "", None, None, None
831
 
832
 
833
- CSS = """
834
- @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=DM+Mono:wght@400;500&display=swap');
 
835
 
 
836
  :root {
837
  --bg: #030712;
838
  --surface: #0a0f1e;
@@ -848,7 +713,6 @@ CSS = """
848
 
849
  body, .gradio-container {
850
  background: var(--bg) !important;
851
- font-family: 'Syne', sans-serif !important;
852
  color: var(--text) !important;
853
  }
854
 
@@ -856,31 +720,23 @@ body, .gradio-container {
856
  text-align: center;
857
  padding: 2.4rem 1rem 1.6rem;
858
  border-bottom: 1px solid var(--border);
859
- background:
860
- radial-gradient(ellipse at 30% 50%, rgba(0,102,255,0.12) 0%, transparent 60%),
861
- radial-gradient(ellipse at 70% 50%, rgba(0,207,255,0.08) 0%, transparent 60%);
862
  }
863
 
864
  .hdr h1 {
865
  font-size: 2.2rem;
866
  font-weight: 800;
867
- letter-spacing: -0.04em;
868
- background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%);
869
- -webkit-background-clip: text;
870
- -webkit-text-fill-color: transparent;
871
  margin: 0 0 0.3rem;
872
  }
873
 
874
  .hdr .sub {
875
  color: var(--muted);
876
- font-family: 'DM Mono', monospace;
877
  font-size: .75rem;
878
  letter-spacing: .06em;
879
  }
880
 
881
  .hdr .own {
882
  color: var(--accent2);
883
- font-family: 'DM Mono', monospace;
884
  font-size: .68rem;
885
  margin-top: .3rem;
886
  letter-spacing: .1em;
@@ -892,7 +748,6 @@ textarea, input[type=text] {
892
  border: 1px solid var(--border) !important;
893
  border-radius: var(--r) !important;
894
  color: var(--text) !important;
895
- font-family: 'DM Mono', monospace !important;
896
  }
897
 
898
  button.primary {
@@ -900,7 +755,6 @@ button.primary {
900
  color: #030712 !important;
901
  border: none !important;
902
  border-radius: var(--r) !important;
903
- font-family: 'Syne', sans-serif !important;
904
  font-weight: 800 !important;
905
  }
906
 
@@ -909,14 +763,12 @@ button.secondary {
909
  color: var(--accent2) !important;
910
  border: 1px solid var(--accent3) !important;
911
  border-radius: var(--r) !important;
912
- font-family: 'Syne', sans-serif !important;
913
  font-weight: 700 !important;
914
  }
915
 
916
  .out textarea {
917
  background: #020510 !important;
918
  border-left: 3px solid var(--accent) !important;
919
- font-family: 'DM Mono', monospace !important;
920
  font-size: .84rem !important;
921
  line-height: 1.7 !important;
922
  }
@@ -925,7 +777,6 @@ button.secondary {
925
  text-align: center;
926
  padding: 1.5rem;
927
  color: var(--muted);
928
- font-family: 'DM Mono', monospace;
929
  font-size: .68rem;
930
  border-top: 1px solid var(--border);
931
  margin-top: 2rem;
@@ -983,12 +834,7 @@ with gr.Blocks(css=CSS, title="The Metamorphic Phoenix") as demo:
983
  value=None,
984
  interactive=True,
985
  )
986
- lib_viewer = gr.Textbox(
987
- label="Content",
988
- lines=16,
989
- elem_classes=["out"],
990
- interactive=False,
991
- )
992
 
993
  with gr.Row():
994
  lib_dl_btn = gr.Button("Download", variant="secondary")
@@ -997,11 +843,7 @@ with gr.Blocks(css=CSS, title="The Metamorphic Phoenix") as demo:
997
  lib_dl_file = gr.File(label="Your file", visible=False)
998
  lib_msg = gr.Markdown("")
999
 
1000
- run_btn.click(
1001
- phoenix_analyse,
1002
- inputs=[pdf_input],
1003
- outputs=[out1, out2, out3, out4],
1004
- )
1005
 
1006
  top_tts_btn.click(
1007
  lambda a, b, c, d: make_tts(
@@ -1019,32 +861,20 @@ with gr.Blocks(css=CSS, title="The Metamorphic Phoenix") as demo:
1019
  tts3_btn.click(make_tts, inputs=[out3], outputs=[tts3_audio])
1020
  tts4_btn.click(make_tts, inputs=[out4], outputs=[tts4_audio])
1021
 
1022
- save_btn.click(
1023
- save_phoenix_analysis,
1024
- inputs=[out1, out2, out3, out4],
1025
- outputs=[save_msg],
1026
- )
1027
-
1028
  lib_ref_btn.click(lib_refresh, inputs=[], outputs=[lib_dd, lib_msg])
1029
  lib_dd.change(lib_view, inputs=[lib_dd], outputs=lib_viewer)
1030
  lib_dl_btn.click(lib_download, inputs=[lib_dd], outputs=[lib_dl_file, lib_msg])
1031
  lib_del_btn.click(lib_delete, inputs=[lib_dd], outputs=[lib_dd, lib_msg])
1032
 
1033
  with gr.TabItem("Seamstresses Framework"):
1034
- gr.Markdown(
1035
- """
1036
- # Seamstresses Framework
1037
 
1038
- Forensic document analysis, intelligent merging, risk-language detection, AI summary, and export.
1039
- """
1040
- )
1041
 
1042
- with gr.Row():
1043
- with gr.Column():
1044
- file1 = gr.File(label="Document 1", file_types=[".pdf", ".txt", ".docx"], type="filepath")
1045
- file2 = gr.File(label="Document 2", file_types=[".pdf", ".txt", ".docx"], type="filepath")
1046
- file3 = gr.File(label="Document 3 Optional", file_types=[".pdf", ".txt", ".docx"], type="filepath")
1047
- analyze_btn = gr.Button("Analyze, Merge & Summarise", variant="primary", size="lg")
1048
 
1049
  with gr.Tabs():
1050
  with gr.TabItem("Master Document"):
@@ -1057,16 +887,13 @@ with gr.Blocks(css=CSS, title="The Metamorphic Phoenix") as demo:
1057
  summary_output = gr.Textbox(label="AI Summary", lines=30, max_lines=200)
1058
 
1059
  with gr.TabItem("Export"):
1060
- with gr.Row():
1061
- txt_file = gr.File(label="Plain Text", interactive=False)
1062
- pdf_file = gr.File(label="PDF", interactive=False)
1063
- docx_file = gr.File(label="Word Document", interactive=False)
1064
-
1065
- with gr.Row():
1066
- tts_merged_btn = gr.Button("Voice: Master Document", variant="secondary")
1067
- tts_analysis_btn = gr.Button("Voice: Analysis Report", variant="secondary")
1068
- tts_summary_btn = gr.Button("Voice: AI Summary", variant="secondary")
1069
 
 
 
 
1070
  tts_seam_audio = gr.Audio(label="TTS Playback", type="filepath")
1071
 
1072
  analyze_btn.click(
 
 
1
  import gradio as gr
2
  import json
3
  import os
4
  import time
5
  import tempfile
 
6
  import re
7
  import requests
8
  from pathlib import Path
 
11
  from difflib import SequenceMatcher
12
  from huggingface_hub import InferenceClient
13
  from gtts import gTTS
14
+ from pypdf import PdfReader
15
+ import PyPDF2
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
  SAVE_DIR = Path("/data/saved_docs")
 
21
  MODEL = "mistralai/Mistral-7B-Instruct-v0.3"
22
 
23
 
24
+ # =========================
25
+ # SHARED: TOKEN / MODEL / TTS
26
+ # =========================
27
+
28
  def get_client():
29
+ token = os.environ.get("HF_TOKEN", "")
30
  return InferenceClient(model=MODEL, token=token if token else None)
31
 
32
 
 
38
  max_tokens=max_tokens,
39
  temperature=0.4,
40
  )
 
41
  if hasattr(response, "choices") and response.choices:
42
  return response.choices[0].message.content
 
43
  return f"No model output returned. Raw response: {response}"
 
44
  except Exception as e:
45
  return f"Model error: {e}"
46
 
 
49
  try:
50
  if not text or not str(text).strip():
51
  return None
 
52
  clean_text = str(text).strip()[:5000]
 
53
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
54
  tmp.close()
 
55
  tts = gTTS(text=clean_text, lang="en", slow=False)
56
  tts.save(tmp.name)
 
57
  return tmp.name
 
58
  except Exception as e:
59
  print(f"TTS error: {e}")
60
  return None
61
 
62
 
63
+ # =========================
64
+ # METABOLIC PHOENIX
65
+ # =========================
66
+
67
  def extract_pdf_text(path):
68
  if not path:
69
  return ""
 
70
  if hasattr(path, "name"):
71
  path = path.name
 
72
  try:
 
 
 
73
  reader = PdfReader(path)
74
  pages = []
 
75
  for i, page in enumerate(reader.pages):
76
  text = page.extract_text() or ""
77
  if text.strip():
78
  pages.append(f"--- Page {i + 1} ---\n{text.strip()}")
 
79
  if not pages:
80
  return "[No extractable text — may be a scanned PDF]"
 
81
  full = "\n\n".join(pages)
82
  return full[:14000] + "\n\n[...truncated]" if len(full) > 14000 else full
 
83
  except Exception as e:
84
  return f"[PDF read error: {e}]"
85
 
 
158
 
159
  try:
160
  index_path = SAVE_DIR / "index.json"
161
+ library = json.loads(index_path.read_text(encoding="utf-8")) if index_path.exists() else []
162
+ entry = {"id": doc_id, "title": f"Analysis — {ts}", "content": content, "saved_at": ts}
 
 
 
 
 
 
 
163
  library.insert(0, entry)
164
  index_path.write_text(json.dumps(library, indent=2), encoding="utf-8")
165
  (SAVE_DIR / f"{doc_id}.txt").write_text(content, encoding="utf-8")
 
166
  return f"Saved: Analysis — {ts}"
 
167
  except Exception as e:
168
  return f"Save error: {e}"
169
 
170
 
171
  def load_library():
172
  index_path = SAVE_DIR / "index.json"
 
173
  try:
174
  return json.loads(index_path.read_text(encoding="utf-8")) if index_path.exists() else []
175
  except Exception:
 
182
 
183
  def choice_to_entry(library, choice):
184
  choices = lib_choices(library)
 
185
  try:
186
  return library[choices.index(choice)]
187
  except Exception:
 
195
 
196
  def lib_view(selection):
197
  library = load_library()
 
198
  if not selection:
199
  return ""
 
200
  entry = choice_to_entry(library, selection)
201
  return entry["content"] if entry else "Not found."
202
 
203
 
204
  def lib_download(selection):
205
  library = load_library()
 
206
  if not selection:
207
  return gr.update(visible=False), "Select a document first."
 
208
  entry = choice_to_entry(library, selection)
 
209
  if not entry:
210
  return gr.update(visible=False), "Not found."
 
211
  file_path = SAVE_DIR / f"{entry['id']}.txt"
 
212
  if file_path.exists():
213
  return gr.update(value=str(file_path), visible=True), "Ready."
 
214
  return gr.update(visible=False), "File missing."
215
 
216
 
217
  def lib_delete(selection):
218
  library = load_library()
 
219
  if not selection:
220
  return gr.update(choices=lib_choices(library), value=None), "Select a document."
 
221
  entry = choice_to_entry(library, selection)
 
222
  if not entry:
223
  return gr.update(choices=lib_choices(library), value=None), "Not found."
 
224
  library = [x for x in library if x["id"] != entry["id"]]
225
  (SAVE_DIR / "index.json").write_text(json.dumps(library, indent=2), encoding="utf-8")
 
226
  file_path = SAVE_DIR / f"{entry['id']}.txt"
 
227
  if file_path.exists():
228
  file_path.unlink()
 
229
  return gr.update(choices=lib_choices(library), value=None), "Deleted."
230
 
231
 
232
+ # =========================
233
+ # SEAMSTRESSES DOCUMENT PROCESSING
234
+ # =========================
235
+
236
  class DocumentParser:
237
  @staticmethod
238
  def extract_text_from_pdf(file_path: str) -> str:
239
  try:
 
 
 
240
  with open(file_path, "rb") as f:
241
  reader = PyPDF2.PdfReader(f)
242
  text = ""
 
243
  for page in reader.pages:
244
  extracted = page.extract_text()
245
  if extracted:
246
  text += extracted + "\n"
 
247
  return text
 
248
  except Exception as e:
249
  return f"Error reading PDF: {str(e)}"
250
 
 
252
  def extract_text_from_docx(file_path: str) -> str:
253
  try:
254
  from docx import Document
 
255
  doc = Document(file_path)
256
  return "\n\n".join([p.text for p in doc.paragraphs if p.text.strip()])
 
257
  except Exception as e:
258
  return f"Error reading DOCX: {str(e)}"
259
 
 
261
  def extract_text_from_file(file_path: str) -> str:
262
  if not file_path:
263
  return "Error: No file path provided"
 
264
  try:
265
  lower = file_path.lower()
 
266
  if lower.endswith(".pdf"):
267
  return DocumentParser.extract_text_from_pdf(file_path)
 
268
  if lower.endswith(".docx"):
269
  return DocumentParser.extract_text_from_docx(file_path)
 
270
  with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
271
  return f.read()
 
272
  except Exception as e:
273
  return f"Error reading file: {str(e)}"
274
 
 
276
  def parse_document(text: str) -> Dict:
277
  paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
278
  sentences = []
 
279
  for paragraph in paragraphs:
280
  sentences.extend([s.strip() + "." for s in paragraph.split(".") if s.strip()])
 
281
  return {
282
  "full_text": text,
283
  "paragraphs": paragraphs,
 
298
  @staticmethod
299
  def find_duplicates(docs: List[Dict]) -> List[Dict]:
300
  duplicates = []
 
301
  for i, doc1 in enumerate(docs):
302
  for j, doc2 in enumerate(docs[i + 1:], start=i + 1):
303
  for para1 in doc1["paragraphs"]:
304
  for para2 in doc2["paragraphs"]:
305
  if len(para1) < 20 or len(para2) < 20:
306
  continue
 
307
  ratio = SequenceMatcher(None, para1, para2).ratio()
 
308
  if ratio > 0.85:
309
+ duplicates.append({
310
+ "doc1": i,
311
+ "doc2": j,
312
+ "similarity": ratio,
313
+ "text": para1[:5000] + "...",
314
+ "type": "duplicate" if ratio > 0.95 else "near_duplicate",
315
+ })
 
 
 
316
  return duplicates
317
 
318
  @staticmethod
319
  def find_unique_content(docs: List[Dict]) -> List[Dict]:
320
  unique_items = []
 
321
  for i, doc in enumerate(docs):
322
  for para in doc["paragraphs"]:
323
  if len(para) < 20:
324
  continue
 
325
  is_unique = True
 
326
  for j, other_doc in enumerate(docs):
327
  if i == j:
328
  continue
 
329
  for other_para in other_doc["paragraphs"]:
330
  if SequenceMatcher(None, para, other_para).ratio() > 0.8:
331
  is_unique = False
332
  break
 
333
  if not is_unique:
334
  break
 
335
  if is_unique:
336
+ unique_items.append({
337
+ "doc_index": i,
338
+ "content": para,
339
+ "length": len(para),
340
+ "type": "unique_paragraph",
341
+ })
 
 
 
342
  return unique_items
343
 
344
  @staticmethod
345
  def detect_risk_language(text: str) -> List[Dict]:
346
  findings = []
 
347
  for risk_type, pattern in ForensicAnalyzer.RISK_PATTERNS.items():
348
  for match in re.finditer(pattern, text, re.IGNORECASE):
349
  start = max(0, match.start() - 50)
350
  end = min(len(text), match.end() + 50)
351
+ findings.append({
352
+ "type": risk_type,
353
+ "matched_text": match.group(),
354
+ "context": text[start:end],
355
+ "position": match.start(),
356
+ })
 
 
 
 
357
  return findings
358
 
359
  @staticmethod
 
374
  def select_best_content(duplicates, docs):
375
  best_versions = []
376
  processed = set()
 
377
  for duplicate in duplicates:
378
  key = tuple(sorted([duplicate["doc1"], duplicate["doc2"]]))
 
379
  if key in processed:
380
  continue
 
381
  processed.add(key)
 
382
  doc1_text = docs[duplicate["doc1"]]["full_text"]
383
  doc2_text = docs[duplicate["doc2"]]["full_text"]
 
384
  best_versions.append(doc1_text if len(doc1_text) > len(doc2_text) else doc2_text)
 
385
  return best_versions
386
 
387
  @staticmethod
388
  def merge_documents(docs, analysis):
389
  output = []
 
390
  output.append("MASTER EVIDENCE DOCUMENT")
391
  output.append("=" * 80)
392
  output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
393
  output.append(f"Source Documents: {len(docs)}")
394
  output.append("=" * 80 + "\n")
 
395
  output.append("EXECUTIVE SUMMARY")
396
  output.append("-" * 80)
397
  output.append(f"Total Duplicates Found: {len(analysis['duplicates'])}")
398
  output.append(f"Unique Content Items: {len(analysis['unique_content'])}")
399
  output.append(f"Risk Language Instances: {len(analysis['risk_language'])}\n")
 
400
  output.append("CONSOLIDATED CONTENT")
401
  output.append("-" * 80)
 
402
  best_versions = DocumentMerger.select_best_content(analysis["duplicates"], docs)
 
403
  if best_versions:
404
  for i, content in enumerate(best_versions, 1):
405
  output.append(f"\n[Section {i}]")
406
  output.append(content[:50000] + "..." if len(content) > 50000 else content)
407
  else:
408
  output.append("No duplicate content found across documents.")
 
409
  output.append("\n\nUNIQUE FINDINGS")
410
  output.append("-" * 80)
 
411
  for item in analysis["unique_content"][:100]:
412
  output.append(f"\n[From Document {item['doc_index'] + 1}]")
413
  content = item["content"]
414
  output.append(content[:50000] + "..." if len(content) > 50000 else content)
 
415
  output.append("\n\nRISK LANGUAGE ANALYSIS")
416
  output.append("-" * 80)
 
417
  risk_summary = {}
 
418
  for risk in analysis["risk_language"]:
419
  risk_summary[risk["type"]] = risk_summary.get(risk["type"], 0) + 1
 
420
  if risk_summary:
421
  for risk_type, count in risk_summary.items():
422
  output.append(f"{risk_type.replace('_', ' ').title()}: {count} instances")
423
  else:
424
  output.append("No risk language patterns detected.")
 
425
  output.append("\n" + "=" * 80)
426
  output.append("END OF MASTER EVIDENCE DOCUMENT")
 
427
  return "\n".join(output)
428
 
429
 
430
  def ai_summarise(merged_doc: str, analysis: Dict) -> str:
431
  try:
432
+ hf_token = os.environ.get("HF_TOKEN")
 
433
  risk_summary = {}
 
434
  for risk in analysis.get("risk_language", []):
435
  risk_summary[risk["type"]] = risk_summary.get(risk["type"], 0) + 1
436
 
 
445
  """.strip()
446
 
447
  headers = {"Content-Type": "application/json"}
 
448
  if hf_token:
449
  headers["Authorization"] = f"Bearer {hf_token}"
450
 
 
464
 
465
  if response.status_code == 200:
466
  data = response.json()
 
467
  if isinstance(data, list) and data:
468
  summary = data[0].get("summary_text", "")
469
  risk_lines = "\n".join(
470
+ [f"- {k.replace('_', ' ').title()}: {v} instances" for k, v in risk_summary.items()]
 
 
 
471
  )
 
472
  return f"AI SUMMARY\n{'=' * 60}\n\n{summary}\n\nRisk Language Breakdown:\n{risk_lines}"
 
473
  return "Summary could not be extracted from model response."
474
 
475
  if response.status_code == 503:
 
484
  def export_txt(merged_doc: str, analysis_report: str) -> str:
485
  try:
486
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8")
 
487
  tmp.write("SEAMSTRESSES FRAMEWORK — EXPORT\n")
488
  tmp.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
489
  tmp.write("=" * 80 + "\n\n")
 
491
  tmp.write("\n\n" + "=" * 80 + "\n\n")
492
  tmp.write(analysis_report)
493
  tmp.close()
 
494
  return tmp.name
 
495
  except Exception:
496
  return None
497
 
 
518
  )
519
 
520
  styles = getSampleStyleSheet()
521
+ title_style = ParagraphStyle("Title", parent=styles["Heading1"], fontSize=16, alignment=TA_CENTER, spaceAfter=6)
 
 
 
 
 
 
 
 
522
  heading_style = ParagraphStyle(
523
  "Heading",
524
  parent=styles["Heading2"],
 
526
  textColor=colors.HexColor("#1a1a2e"),
527
  spaceAfter=4,
528
  )
529
+ body_style = ParagraphStyle("Body", parent=styles["Normal"], fontSize=9, leading=14, spaceAfter=3)
 
 
 
 
 
 
 
530
 
531
  story = []
 
532
  story.append(Paragraph("Seamstresses Framework", title_style))
533
  story.append(Paragraph("Forensic Document Analysis Report", heading_style))
534
  story.append(Paragraph(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", body_style))
 
539
  for line in merged_doc.split("\n"):
540
  if line.strip():
541
  safe = line.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
 
542
  if line.startswith("===") or line.startswith("---"):
543
  story.append(HRFlowable(width="100%", thickness=0.5, color=colors.lightgrey))
544
  elif line.isupper() and len(line) > 3 and not line.startswith("["):
 
553
  for line in analysis_report.split("\n"):
554
  if line.strip():
555
  safe = line.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
 
556
  if line.isupper() and len(line) > 3:
557
  story.append(Paragraph(safe, heading_style))
558
  else:
559
  story.append(Paragraph(safe, body_style))
560
 
561
  doc.build(story)
 
562
  return tmp.name
563
+ except Exception as e:
564
+ print(f"PDF export error: {e}")
565
  return None
566
 
567
 
 
574
  tmp.close()
575
 
576
  doc = DocxDocument()
 
577
  title = doc.add_heading("Seamstresses Framework", 0)
578
  title.alignment = WD_ALIGN_PARAGRAPH.CENTER
579
 
 
610
  doc.add_paragraph(line)
611
 
612
  doc.save(tmp.name)
 
613
  return tmp.name
614
+ except Exception as e:
615
+ print(f"DOCX export error: {e}")
616
  return None
617
 
618
 
 
624
  for uploaded in raw_inputs:
625
  if uploaded is None:
626
  continue
 
627
  if isinstance(uploaded, dict):
628
  path = uploaded.get("path") or uploaded.get("name")
629
  elif hasattr(uploaded, "name"):
 
632
  path = uploaded
633
  else:
634
  path = str(uploaded)
 
635
  if path:
636
  files.append(path)
637
 
 
643
 
644
  for file_path in files:
645
  text = DocumentParser.extract_text_from_file(file_path)
 
646
  if not text or text.startswith("Error"):
647
  return f"Error: {text}", "", "", None, None, None
 
648
  docs.append(DocumentParser.parse_document(text))
649
  file_names.append(os.path.basename(file_path))
650
 
 
653
 
654
  analysis_report = f"FORENSIC ANALYSIS REPORT\n{'=' * 80}\n\n"
655
  analysis_report += f"FILES ANALYZED: {', '.join(file_names)}\n\n"
 
656
  analysis_report += f"DUPLICATE ANALYSIS\n{'-' * 80}\n"
657
  analysis_report += f"Total Duplicates Found: {len(analysis['duplicates'])}\n\n"
658
 
 
669
  analysis_report += f"- From Document {item['doc_index'] + 1}: {item['content'][:5000]}...\n"
670
 
671
  analysis_report += f"\nRISK LANGUAGE DETECTED\n{'-' * 80}\n"
 
672
  risk_types = {}
673
 
674
  for risk in analysis["risk_language"]:
 
690
 
691
  except Exception as e:
692
  import traceback
 
693
  return f"Error: {str(e)}\n\n{traceback.format_exc()}", "", "", None, None, None
694
 
695
 
696
+ # =========================
697
+ # UI
698
+ # =========================
699
 
700
+ CSS = """
701
  :root {
702
  --bg: #030712;
703
  --surface: #0a0f1e;
 
713
 
714
  body, .gradio-container {
715
  background: var(--bg) !important;
 
716
  color: var(--text) !important;
717
  }
718
 
 
720
  text-align: center;
721
  padding: 2.4rem 1rem 1.6rem;
722
  border-bottom: 1px solid var(--border);
 
 
 
723
  }
724
 
725
  .hdr h1 {
726
  font-size: 2.2rem;
727
  font-weight: 800;
728
+ color: var(--accent);
 
 
 
729
  margin: 0 0 0.3rem;
730
  }
731
 
732
  .hdr .sub {
733
  color: var(--muted);
 
734
  font-size: .75rem;
735
  letter-spacing: .06em;
736
  }
737
 
738
  .hdr .own {
739
  color: var(--accent2);
 
740
  font-size: .68rem;
741
  margin-top: .3rem;
742
  letter-spacing: .1em;
 
748
  border: 1px solid var(--border) !important;
749
  border-radius: var(--r) !important;
750
  color: var(--text) !important;
 
751
  }
752
 
753
  button.primary {
 
755
  color: #030712 !important;
756
  border: none !important;
757
  border-radius: var(--r) !important;
 
758
  font-weight: 800 !important;
759
  }
760
 
 
763
  color: var(--accent2) !important;
764
  border: 1px solid var(--accent3) !important;
765
  border-radius: var(--r) !important;
 
766
  font-weight: 700 !important;
767
  }
768
 
769
  .out textarea {
770
  background: #020510 !important;
771
  border-left: 3px solid var(--accent) !important;
 
772
  font-size: .84rem !important;
773
  line-height: 1.7 !important;
774
  }
 
777
  text-align: center;
778
  padding: 1.5rem;
779
  color: var(--muted);
 
780
  font-size: .68rem;
781
  border-top: 1px solid var(--border);
782
  margin-top: 2rem;
 
834
  value=None,
835
  interactive=True,
836
  )
837
+ lib_viewer = gr.Textbox(label="Content", lines=16, elem_classes=["out"], interactive=False)
 
 
 
 
 
838
 
839
  with gr.Row():
840
  lib_dl_btn = gr.Button("Download", variant="secondary")
 
843
  lib_dl_file = gr.File(label="Your file", visible=False)
844
  lib_msg = gr.Markdown("")
845
 
846
+ run_btn.click(phoenix_analyse, inputs=[pdf_input], outputs=[out1, out2, out3, out4])
 
 
 
 
847
 
848
  top_tts_btn.click(
849
  lambda a, b, c, d: make_tts(
 
861
  tts3_btn.click(make_tts, inputs=[out3], outputs=[tts3_audio])
862
  tts4_btn.click(make_tts, inputs=[out4], outputs=[tts4_audio])
863
 
864
+ save_btn.click(save_phoenix_analysis, inputs=[out1, out2, out3, out4], outputs=[save_msg])
 
 
 
 
 
865
  lib_ref_btn.click(lib_refresh, inputs=[], outputs=[lib_dd, lib_msg])
866
  lib_dd.change(lib_view, inputs=[lib_dd], outputs=lib_viewer)
867
  lib_dl_btn.click(lib_download, inputs=[lib_dd], outputs=[lib_dl_file, lib_msg])
868
  lib_del_btn.click(lib_delete, inputs=[lib_dd], outputs=[lib_dd, lib_msg])
869
 
870
  with gr.TabItem("Seamstresses Framework"):
871
+ gr.Markdown("# Seamstresses Framework\n\nForensic document analysis, intelligent merging, risk-language detection, AI summary, and export.")
 
 
872
 
873
+ file1 = gr.File(label="Document 1", file_types=[".pdf", ".txt", ".docx"], type="filepath")
874
+ file2 = gr.File(label="Document 2", file_types=[".pdf", ".txt", ".docx"], type="filepath")
875
+ file3 = gr.File(label="Document 3 Optional", file_types=[".pdf", ".txt", ".docx"], type="filepath")
876
 
877
+ analyze_btn = gr.Button("Analyze, Merge & Summarise", variant="primary", size="lg")
 
 
 
 
 
878
 
879
  with gr.Tabs():
880
  with gr.TabItem("Master Document"):
 
887
  summary_output = gr.Textbox(label="AI Summary", lines=30, max_lines=200)
888
 
889
  with gr.TabItem("Export"):
890
+ txt_file = gr.File(label="Plain Text", interactive=False)
891
+ pdf_file = gr.File(label="PDF", interactive=False)
892
+ docx_file = gr.File(label="Word Document", interactive=False)
 
 
 
 
 
 
893
 
894
+ tts_merged_btn = gr.Button("Voice: Master Document", variant="secondary")
895
+ tts_analysis_btn = gr.Button("Voice: Analysis Report", variant="secondary")
896
+ tts_summary_btn = gr.Button("Voice: AI Summary", variant="secondary")
897
  tts_seam_audio = gr.Audio(label="TTS Playback", type="filepath")
898
 
899
  analyze_btn.click(