Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,27 +1,23 @@
|
|
| 1 |
-
import gradio as gr
|
| 2 |
import os
|
| 3 |
import re
|
| 4 |
import time
|
| 5 |
-
import subprocess
|
| 6 |
import tempfile
|
|
|
|
|
|
|
| 7 |
from pathlib import Path
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
for pkg in ["pypdf", "edge-tts", "pydub", "pillow", "pytesseract", "openai-whisper", "ffmpeg-python"]:
|
| 11 |
-
subprocess.run(["pip", "install", pkg, "-q"], capture_output=True)
|
| 12 |
-
subprocess.run(["apt-get", "install", "-y", "-q", "ffmpeg", "tesseract-ocr"], capture_output=True)
|
| 13 |
-
|
| 14 |
from pypdf import PdfReader
|
| 15 |
-
import asyncio
|
| 16 |
import edge_tts
|
| 17 |
from pydub import AudioSegment
|
| 18 |
from PIL import Image
|
| 19 |
import pytesseract
|
| 20 |
import whisper
|
| 21 |
import requests as _req
|
| 22 |
-
import time as _time
|
| 23 |
|
| 24 |
-
# ──
|
| 25 |
_FREE_API = "https://api-inference.huggingface.co/models/{model}"
|
| 26 |
_MODELS = [
|
| 27 |
"HuggingFaceH4/zephyr-7b-beta",
|
|
@@ -32,17 +28,10 @@ _MODELS = [
|
|
| 32 |
def call_llama(prompt: str, max_tokens: int = 2000) -> str:
|
| 33 |
token = os.environ.get("HF_TOKEN", "")
|
| 34 |
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
| 35 |
-
|
| 36 |
for model in _MODELS:
|
| 37 |
for attempt in range(4):
|
| 38 |
try:
|
| 39 |
-
|
| 40 |
-
fmt = f"<|system|>\nYou are a precise legal document analyst.</s>\n<|user|>\n{prompt}</s>\n<|assistant|>\n"
|
| 41 |
-
elif "Phi-3" in model:
|
| 42 |
-
fmt = f"<|user|>\n{prompt}<|end|>\n<|assistant|>\n"
|
| 43 |
-
else:
|
| 44 |
-
fmt = f"[INST] {prompt} [/INST]"
|
| 45 |
-
|
| 46 |
resp = _req.post(
|
| 47 |
_FREE_API.format(model=model),
|
| 48 |
headers=headers,
|
|
@@ -58,33 +47,26 @@ def call_llama(prompt: str, max_tokens: int = 2000) -> str:
|
|
| 58 |
},
|
| 59 |
timeout=180
|
| 60 |
)
|
| 61 |
-
|
| 62 |
if resp.status_code == 200:
|
| 63 |
result = resp.json()
|
| 64 |
if isinstance(result, list) and result:
|
| 65 |
text = result[0].get("generated_text", "").strip()
|
| 66 |
if text:
|
| 67 |
return text
|
| 68 |
-
|
| 69 |
elif resp.status_code == 503:
|
| 70 |
-
|
| 71 |
continue
|
| 72 |
-
|
| 73 |
elif resp.status_code in [429, 402]:
|
| 74 |
break
|
| 75 |
-
|
| 76 |
else:
|
| 77 |
-
|
| 78 |
continue
|
| 79 |
-
|
| 80 |
except Exception:
|
| 81 |
-
|
| 82 |
continue
|
|
|
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
# ── FILE EXTRACTION ───────────────────────────────────────────────────────────
|
| 88 |
def extract_pdf_text(path):
|
| 89 |
try:
|
| 90 |
reader = PdfReader(path)
|
|
@@ -93,7 +75,7 @@ def extract_pdf_text(path):
|
|
| 93 |
text = page.extract_text() or ""
|
| 94 |
if text.strip():
|
| 95 |
pages.append(f"--- Page {i+1} ---\n{text.strip()}")
|
| 96 |
-
return "\n\n".join(pages) if pages else "[No extractable text — may be
|
| 97 |
except Exception as e:
|
| 98 |
return f"[PDF read error: {e}]"
|
| 99 |
|
|
@@ -109,7 +91,7 @@ def transcribe_audio(path):
|
|
| 109 |
try:
|
| 110 |
model = whisper.load_model("base")
|
| 111 |
result = model.transcribe(path, language="en")
|
| 112 |
-
return f"[AUDIO
|
| 113 |
except Exception as e:
|
| 114 |
return f"[Transcription error: {e}]"
|
| 115 |
|
|
@@ -131,30 +113,23 @@ def extract_content(file):
|
|
| 131 |
return transcribe_audio(tmp.name)
|
| 132 |
except Exception as e:
|
| 133 |
return f"[Video extraction error: {e}]"
|
|
|
|
|
|
|
| 134 |
else:
|
| 135 |
-
|
| 136 |
-
return Path(path).read_text(encoding="utf-8", errors="replace")
|
| 137 |
-
except Exception:
|
| 138 |
-
return f"[Unsupported file type: {ext}]"
|
| 139 |
-
|
| 140 |
|
| 141 |
-
# ── ANALYSIS ──────────────────────────────────────────────────────
|
| 142 |
PLAIN_ENGLISH = (
|
| 143 |
-
"\n\nCRITICAL — FORMATTING RULES
|
| 144 |
-
"Write in plain
|
| 145 |
"No JSON. No code blocks. No curly brackets. No asterisks. No hash symbols.\n"
|
| 146 |
-
"
|
| 147 |
-
"
|
| 148 |
-
"Every sentence must be clear enough for a non-lawyer to read and understand.\n"
|
| 149 |
-
"Do not hedge. Do not say 'may indicate' or 'appears to suggest'.\n"
|
| 150 |
-
"If it is in the document, it happened. State it as confirmed fact."
|
| 151 |
)
|
| 152 |
|
| 153 |
def analyse(files):
|
| 154 |
-
if not files:
|
| 155 |
-
|
| 156 |
-
if not isinstance(files, list):
|
| 157 |
-
files = [files]
|
| 158 |
|
| 159 |
all_content = []
|
| 160 |
for i, f in enumerate(files):
|
|
@@ -167,422 +142,73 @@ def analyse(files):
|
|
| 167 |
base = f"DOCUMENTS SUPPLIED ({len(files)} total):\n\n{doc_chunk}\n\n{'='*60}\n\n"
|
| 168 |
|
| 169 |
r1 = call_llama(base +
|
| 170 |
-
"You are Introbot —
|
| 171 |
-
"
|
| 172 |
-
"
|
| 173 |
-
"
|
| 174 |
-
"
|
| 175 |
-
"- Every denial — label it DENIAL, state what it contradicts\n"
|
| 176 |
-
"- Every contradiction — what was said, what contradicts it, when\n"
|
| 177 |
-
"- Every deliberate omission — what was required to be said, was not, why that is a breach\n"
|
| 178 |
-
"- Every statutory duty triggered and the precise moment it was triggered\n\n"
|
| 179 |
-
"Do not hedge. Format as a numbered FACT LEDGER." + PLAIN_ENGLISH
|
| 180 |
)
|
| 181 |
|
| 182 |
r2 = call_llama(base +
|
| 183 |
-
"You are RHB 2.0 + CPF —
|
| 184 |
-
"
|
| 185 |
-
"
|
| 186 |
-
"- Each institution's statutory duties and the exact moment each duty was triggered\n"
|
| 187 |
-
"- For each duty: state BREACH CONFIRMED or COMPLIED — never neutral\n"
|
| 188 |
-
"- For every breach: statute, section, precise act or omission, date\n"
|
| 189 |
-
"- Trace the full causal chain: first breach to final injury\n"
|
| 190 |
-
"- Equality Act 2010: confirm each s.13 and s.20 breach as established, not alleged\n"
|
| 191 |
-
"- For each breach state the remedy the law requires\n\n"
|
| 192 |
-
"Do not use 'may', 'potentially', 'appears'. These are findings.\n"
|
| 193 |
-
"Cite statute sections." + PLAIN_ENGLISH
|
| 194 |
)
|
| 195 |
|
| 196 |
r3 = call_llama(base +
|
| 197 |
-
"You are Phoenix Badger —
|
| 198 |
-
"
|
| 199 |
-
"RING
|
| 200 |
-
"
|
| 201 |
-
"RING B — BACKWARD SWEEP: Every past decision that was unlawful at the time. "
|
| 202 |
-
"Name the law breached and when.\n\n"
|
| 203 |
-
"RING C — TYPOLOGY MUTATION: Name the patterns of institutional misconduct confirmed. "
|
| 204 |
-
"State whether each pattern is systemic or targeted.\n\n"
|
| 205 |
-
"RING D — DRIFT CORRECTION: Every internal contradiction across the documents. "
|
| 206 |
-
"State what each contradiction proves.\n\n"
|
| 207 |
-
"ESCALATION MAP: Which regulatory bodies and courts must receive this, "
|
| 208 |
-
"in priority order, with reasons." + PLAIN_ENGLISH
|
| 209 |
)
|
| 210 |
|
| 211 |
r4 = call_llama(
|
| 212 |
-
f"
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
f"BREACH ANALYSIS:\n{r2[:1500]}\n\n"
|
| 216 |
-
f"LOOP OUTPUT:\n{r3[:1500]}\n\n"
|
| 217 |
-
f"{'='*60}\n\n"
|
| 218 |
-
"Produce a complete litigation-ready case summary. "
|
| 219 |
-
"Breaches have occurred and are on the record. Write accordingly.\n\n"
|
| 220 |
-
"EXECUTIVE SUMMARY — plain English: what happened, what was done wrong, who did it\n"
|
| 221 |
-
"CONFIRMED BREACHES — each named, statute and section cited, date confirmed\n"
|
| 222 |
-
"TOP 5 ARGUMENTS — strongest first, each grounded in confirmed fact and statute\n"
|
| 223 |
-
"REQUIRED NEXT STEPS — mandatory actions in priority order\n"
|
| 224 |
-
"ESCALATION MAP — which body receives what, by what deadline\n\n"
|
| 225 |
-
"Ms. Nala | Injectiv8 | Dwayne Anthony Brian Galloway" + PLAIN_ENGLISH
|
| 226 |
)
|
| 227 |
return r1, r2, r3, r4
|
| 228 |
|
|
|
|
|
|
|
| 229 |
|
| 230 |
-
# ──
|
| 231 |
-
|
| 232 |
-
"\n\nFORMATTING RULES:\n"
|
| 233 |
-
"Plain forceful English prose only. No JSON. No markdown. No asterisks. No hash symbols.\n"
|
| 234 |
-
"Numbered paragraphs. CAPITAL LETTER headings.\n"
|
| 235 |
-
"Every sentence carries weight. Do not hedge.\n"
|
| 236 |
-
"Breaches have occurred. Name them. Prove them. Demand remedy."
|
| 237 |
-
)
|
| 238 |
-
|
| 239 |
-
def generate_letter_before_action(r1, r2, r3, r4):
|
| 240 |
-
if not r1: return "Run the analysis first."
|
| 241 |
-
return call_llama(
|
| 242 |
-
"You are a senior litigation solicitor drafting a Letter Before Action on behalf of "
|
| 243 |
-
"Dwayne Anthony Brian Galloway (the Claimant).\n\n"
|
| 244 |
-
f"FACT LEDGER:\n{r1[:2000]}\n\nBREACH ANALYSIS:\n{r2[:2000]}\n\nCASE SUMMARY:\n{r4[:2000]}\n\n"
|
| 245 |
-
"Draft a formal Letter Before Action that:\n"
|
| 246 |
-
"1. Opens stating this is sent under Pre-Action Protocol and proceedings will issue in 14 days without satisfactory response.\n"
|
| 247 |
-
"2. Names each respondent and each confirmed breach — by statute, section, and date.\n"
|
| 248 |
-
"3. States every loss and harm caused: financial, physical, psychological, disability-related.\n"
|
| 249 |
-
"4. States the specific remedies demanded: compensation, reasonable adjustments, correction of records, costs — itemised.\n"
|
| 250 |
-
"5. States: if full remedy is not provided within 14 days, proceedings will issue in the King's Bench Division and the Claimant will seek indemnity costs.\n"
|
| 251 |
-
"6. Closes: Dwayne Anthony Brian Galloway, Litigant in Person."
|
| 252 |
-
+ DOC_PLAIN, max_tokens=2000
|
| 253 |
-
)
|
| 254 |
-
|
| 255 |
-
def generate_formal_complaint(r1, r2, r3, r4):
|
| 256 |
-
if not r1: return "Run the analysis first."
|
| 257 |
-
return call_llama(
|
| 258 |
-
"You are drafting a formal regulatory complaint on behalf of Dwayne Anthony Brian Galloway "
|
| 259 |
-
"for submission to FOS, FCA, ICO or Parliamentary Ombudsman as the breaches dictate.\n\n"
|
| 260 |
-
f"FACT LEDGER:\n{r1[:2000]}\n\nBREACH ANALYSIS:\n{r2[:2000]}\n\nLOOP ANALYSIS:\n{r3[:1500]}\n\n"
|
| 261 |
-
"Draft a formal regulatory complaint that:\n"
|
| 262 |
-
"1. Names the respondent and the regulatory framework they are bound by.\n"
|
| 263 |
-
"2. States the date each duty arose and was breached — numbered findings.\n"
|
| 264 |
-
"3. States every statutory provision breached with full citation.\n"
|
| 265 |
-
"4. States harm caused: financial loss quantified, EA 2010 s.13 and s.20 confirmed, hardship documented.\n"
|
| 266 |
-
"5. States what the respondent was required to do, what they did instead, what that proves.\n"
|
| 267 |
-
"6. States what remedy the Claimant requires from the regulator.\n"
|
| 268 |
-
"7. States the Claimant is a disabled person under EA 2010 s.6 and the conduct was discriminatory.\n"
|
| 269 |
-
"8. Closes with a demand for substantive response within 8 weeks."
|
| 270 |
-
+ DOC_PLAIN, max_tokens=2000
|
| 271 |
-
)
|
| 272 |
-
|
| 273 |
-
def generate_witness_statement(r1, r2, r4):
|
| 274 |
-
if not r1: return "Run the analysis first."
|
| 275 |
-
return call_llama(
|
| 276 |
-
"You are drafting a formal Witness Statement for Dwayne Anthony Brian Galloway "
|
| 277 |
-
"to file in civil proceedings.\n\n"
|
| 278 |
-
f"FACT LEDGER:\n{r1[:2000]}\n\nBREACH ANALYSIS:\n{r2[:1500]}\n\nCASE SUMMARY:\n{r4[:1500]}\n\n"
|
| 279 |
-
"Draft a formal Witness Statement in the first person that:\n"
|
| 280 |
-
"1. Opens: IN THE HIGH COURT OF JUSTICE, KING'S BENCH DIVISION. "
|
| 281 |
-
"WITNESS STATEMENT OF DWAYNE ANTHONY BRIAN GALLOWAY.\n"
|
| 282 |
-
"2. States identity, disability status under EA 2010 s.6, and basis of claim.\n"
|
| 283 |
-
"3. Narrates the full chronology in numbered paragraphs — what happened, when, who, what was said.\n"
|
| 284 |
-
"4. For each breach: what the Claimant was told, what he did in response, how he was harmed.\n"
|
| 285 |
-
"5. States financial losses, hardship, physical and psychological impact.\n"
|
| 286 |
-
"6. Addresses every denial and states why each is contradicted by the documented record.\n"
|
| 287 |
-
"7. States what the Claimant asks the Court to find and order.\n"
|
| 288 |
-
"8. Closes with Statement of Truth. Signed: Dwayne Anthony Brian Galloway. Date: [DATE]."
|
| 289 |
-
+ DOC_PLAIN, max_tokens=2000
|
| 290 |
-
)
|
| 291 |
-
|
| 292 |
-
def generate_skeleton_argument(r1, r2, r3, r4):
|
| 293 |
-
if not r1: return "Run the analysis first."
|
| 294 |
-
return call_llama(
|
| 295 |
-
"You are a barrister drafting a Skeleton Argument for Dwayne Anthony Brian Galloway, "
|
| 296 |
-
"Litigant in Person, for hearing in the King's Bench Division.\n\n"
|
| 297 |
-
f"FACT LEDGER:\n{r1[:1500]}\n\nBREACH ANALYSIS:\n{r2[:1500]}\n\n"
|
| 298 |
-
f"LOOP ANALYSIS:\n{r3[:1000]}\n\nCASE SUMMARY:\n{r4[:1500]}\n\n"
|
| 299 |
-
"Draft a Skeleton Argument that:\n"
|
| 300 |
-
"1. Opens: IN THE HIGH COURT OF JUSTICE, KING'S BENCH DIVISION. "
|
| 301 |
-
"DWAYNE ANTHONY BRIAN GALLOWAY (Claimant) v [RESPONDENTS] (Defendants). SKELETON ARGUMENT.\n"
|
| 302 |
-
"2. States the issues for determination — numbered.\n"
|
| 303 |
-
"3. Sets out the Legal Framework: every statute and authority relied upon, cited correctly.\n"
|
| 304 |
-
"4. States submissions on each issue: what the law requires, what the Defendant did, why that is a breach.\n"
|
| 305 |
-
"5. Addresses the Defendant's position and explains why each element is wrong in law and fact.\n"
|
| 306 |
-
"6. States the relief sought: damages quantified, declarations, indemnity costs, injunctive relief.\n"
|
| 307 |
-
"7. Closes with why the Claimant must succeed and why the Defendants have no sustainable defence.\n\n"
|
| 308 |
-
"Authorities: Archibald v Fife [2004] UKHL 32. Wednesbury [1948] 1 KB 223. "
|
| 309 |
-
"CRA 2015. EA 2010. Fraud Act 2006 s.2. CPUTR 2008. FSMA 2000."
|
| 310 |
-
+ DOC_PLAIN, max_tokens=2000
|
| 311 |
-
)
|
| 312 |
-
|
| 313 |
-
def generate_regulatory_referral(r1, r2, r3):
|
| 314 |
-
if not r1: return "Run the analysis first."
|
| 315 |
-
return call_llama(
|
| 316 |
-
"You are drafting a formal referral letter to a regulatory authority on behalf of "
|
| 317 |
-
"Dwayne Anthony Brian Galloway.\n\n"
|
| 318 |
-
f"FACT LEDGER:\n{r1[:1500]}\n\nBREACH ANALYSIS:\n{r2[:1500]}\n\nLOOP ANALYSIS:\n{r3[:1500]}\n\n"
|
| 319 |
-
"Draft a Regulatory Referral that:\n"
|
| 320 |
-
"1. States this is a formal referral requiring the regulator to open a formal investigation.\n"
|
| 321 |
-
"2. Identifies each regulated entity by name.\n"
|
| 322 |
-
"3. States each confirmed breach as a numbered finding: conduct, statute, date, harm.\n"
|
| 323 |
-
"4. States the conduct amounts to regulatory misconduct, failure of regulated duty, "
|
| 324 |
-
"and discrimination under EA 2010.\n"
|
| 325 |
-
"5. States the specific regulatory action required: investigation, enforcement, fine, censure.\n"
|
| 326 |
-
"6. States the Claimant is disabled under EA 2010 s.6 and failure to investigate is itself a breach.\n"
|
| 327 |
-
"7. Closes with a 28-day deadline and states that absence of action will result in escalation "
|
| 328 |
-
"to the Parliamentary Ombudsman and national press."
|
| 329 |
-
+ DOC_PLAIN, max_tokens=2000
|
| 330 |
-
)
|
| 331 |
-
|
| 332 |
-
def generate_all_documents(r1, r2, r3, r4):
|
| 333 |
-
if not r1:
|
| 334 |
-
return "", "", "", "", "Run the analysis first, then generate documents."
|
| 335 |
-
return (
|
| 336 |
-
generate_letter_before_action(r1, r2, r3, r4),
|
| 337 |
-
generate_formal_complaint(r1, r2, r3, r4),
|
| 338 |
-
generate_witness_statement(r1, r2, r4),
|
| 339 |
-
generate_skeleton_argument(r1, r2, r3, r4),
|
| 340 |
-
generate_regulatory_referral(r1, r2, r3),
|
| 341 |
-
)
|
| 342 |
-
|
| 343 |
-
def save_documents(lba, comp, wit, skel, reg):
|
| 344 |
-
if not any([lba, comp, wit, skel, reg]):
|
| 345 |
-
return None, "Generate documents first."
|
| 346 |
-
ts = time.strftime("%Y-%m-%d %H:%M")
|
| 347 |
-
content = (
|
| 348 |
-
f"METABOLIC PHOENIX FRAMEWORK — LEGAL DOCUMENTS\n"
|
| 349 |
-
f"Generated: {ts}\n"
|
| 350 |
-
f"Dwayne Anthony Brian Galloway | Injectiv8\n\n"
|
| 351 |
-
f"{'='*60}\n\nDOCUMENT 1 — LETTER BEFORE ACTION\n\n{lba}\n\n"
|
| 352 |
-
f"{'='*60}\n\nDOCUMENT 2 — FORMAL REGULATORY COMPLAINT\n\n{comp}\n\n"
|
| 353 |
-
f"{'='*60}\n\nDOCUMENT 3 — WITNESS STATEMENT\n\n{wit}\n\n"
|
| 354 |
-
f"{'='*60}\n\nDOCUMENT 4 — SKELETON ARGUMENT\n\n{skel}\n\n"
|
| 355 |
-
f"{'='*60}\n\nDOCUMENT 5 — REGULATORY REFERRAL\n\n{reg}"
|
| 356 |
-
)
|
| 357 |
-
tmp = tempfile.NamedTemporaryFile(
|
| 358 |
-
delete=False, suffix=".txt",
|
| 359 |
-
prefix=f"MPF_Documents_{time.strftime('%Y%m%d_%H%M')}_",
|
| 360 |
-
mode="w", encoding="utf-8"
|
| 361 |
-
)
|
| 362 |
-
tmp.write(content); tmp.close()
|
| 363 |
-
return tmp.name, "Ready to download."
|
| 364 |
-
|
| 365 |
-
def save_analysis(r1, r2, r3, r4):
|
| 366 |
-
if not r1: return None, "Nothing to save — run an analysis first."
|
| 367 |
-
ts = time.strftime("%Y-%m-%d %H:%M")
|
| 368 |
-
content = (
|
| 369 |
-
f"METABOLIC PHOENIX FRAMEWORK — FULL ANALYSIS\nGenerated: {ts}\n"
|
| 370 |
-
f"Ms. Nala | Injectiv8 | Dwayne Anthony Brian Galloway\n\n"
|
| 371 |
-
f"{'='*60}\n\nCYCLE 1 — FACT LEDGER\n\n{r1}\n\n"
|
| 372 |
-
f"{'='*60}\n\nCYCLE 2 — COMPLIANCE ANALYSIS\n\n{r2}\n\n"
|
| 373 |
-
f"{'='*60}\n\nCYCLE 3 — OUROBOROS LOOPS\n\n{r3}\n\n"
|
| 374 |
-
f"{'='*60}\n\nCYCLE 4 — FULL CASE OUTPUT\n\n{r4}"
|
| 375 |
-
)
|
| 376 |
-
tmp = tempfile.NamedTemporaryFile(
|
| 377 |
-
delete=False, suffix=".txt",
|
| 378 |
-
prefix=f"MPF_Analysis_{time.strftime('%Y%m%d_%H%M')}_",
|
| 379 |
-
mode="w", encoding="utf-8"
|
| 380 |
-
)
|
| 381 |
-
tmp.write(content); tmp.close()
|
| 382 |
-
return tmp.name, "Ready to download."
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
# ── TTS — SONIA NEURAL, RYAN FALLBACK, FULL LENGTH ───────────────────────────
|
| 386 |
-
TTS_VOICE = "en-GB-SoniaNeural"
|
| 387 |
-
TTS_BACKUP = "en-GB-RyanNeural"
|
| 388 |
-
CHUNK_SIZE = 1500
|
| 389 |
-
|
| 390 |
-
async def _edge_speak(text, path, voice):
|
| 391 |
await edge_tts.Communicate(text, voice).save(path)
|
| 392 |
|
| 393 |
-
def _split_chunks(text):
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
for s in sentences:
|
| 399 |
-
if len(cur)+len(s)+1 <= CHUNK_SIZE: cur = (cur+" "+s).strip()
|
| 400 |
-
else:
|
| 401 |
-
if cur: chunks.append(cur)
|
| 402 |
-
cur = s
|
| 403 |
-
if cur: chunks.append(cur)
|
| 404 |
-
return chunks or [text[:CHUNK_SIZE]]
|
| 405 |
-
|
| 406 |
-
def _build_audio(files, speed=1.0, pitch=1.0):
|
| 407 |
-
if not files: return None
|
| 408 |
-
combined = AudioSegment.empty()
|
| 409 |
-
for f in files:
|
| 410 |
-
try: combined += AudioSegment.from_mp3(f) + AudioSegment.silent(160)
|
| 411 |
-
except Exception: pass
|
| 412 |
-
if len(combined) == 0: return None
|
| 413 |
-
if pitch != 1.0:
|
| 414 |
-
combined = combined._spawn(combined.raw_data,
|
| 415 |
-
overrides={"frame_rate":int(combined.frame_rate*pitch)}).set_frame_rate(44100)
|
| 416 |
-
if speed != 1.0:
|
| 417 |
-
combined = combined._spawn(combined.raw_data,
|
| 418 |
-
overrides={"frame_rate":int(combined.frame_rate*speed)}).set_frame_rate(44100)
|
| 419 |
-
out = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3", dir="/tmp", prefix="mpf_")
|
| 420 |
-
combined.export(out.name, format="mp3", bitrate="128k")
|
| 421 |
-
out.close()
|
| 422 |
-
for f in files:
|
| 423 |
-
try: os.unlink(f)
|
| 424 |
-
except Exception: pass
|
| 425 |
-
return out.name
|
| 426 |
-
|
| 427 |
-
def text_to_speech_full(text: str, speed: float = 1.0, pitch: float = 1.0):
|
| 428 |
-
if not text or not text.strip(): return None
|
| 429 |
chunks = _split_chunks(text)
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
try:
|
| 434 |
-
loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop)
|
| 435 |
-
for i, chunk in enumerate(chunks):
|
| 436 |
-
out = tempfile.NamedTemporaryFile(delete=False, suffix=f"_e{i}.mp3", dir="/tmp")
|
| 437 |
-
out.close()
|
| 438 |
-
try:
|
| 439 |
-
loop.run_until_complete(_edge_speak(chunk, out.name, voice))
|
| 440 |
-
if os.path.getsize(out.name) > 1000: files.append(out.name)
|
| 441 |
-
else: raise Exception("empty")
|
| 442 |
-
except Exception:
|
| 443 |
-
try: os.unlink(out.name)
|
| 444 |
-
except Exception: pass
|
| 445 |
-
loop.close()
|
| 446 |
-
if len(files) >= len(chunks)*0.8:
|
| 447 |
-
r = _build_audio(files, speed, pitch)
|
| 448 |
-
if r: return r
|
| 449 |
-
except Exception: pass
|
| 450 |
-
|
| 451 |
try:
|
| 452 |
-
subprocess.run(["pip","install","gtts","-q"], capture_output=True)
|
| 453 |
-
from gtts import gTTS
|
| 454 |
-
files = []
|
| 455 |
for i, chunk in enumerate(chunks):
|
| 456 |
-
out = tempfile.NamedTemporaryFile(delete=False, suffix=
|
| 457 |
out.close()
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
return None
|
| 467 |
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
all_content = []
|
| 474 |
-
for i, f in enumerate(files):
|
| 475 |
-
content = extract_content(f)
|
| 476 |
-
name = Path(f if isinstance(f, str) else f.name).name if f else f"Document {i+1}"
|
| 477 |
-
if content: all_content.append(f"Document {i+1}: {name}.\n\n{content}")
|
| 478 |
-
if not all_content: return None, "Could not extract content."
|
| 479 |
-
full_text = "\n\n".join(all_content)
|
| 480 |
-
audio = text_to_speech_full(full_text, speed=speed, pitch=pitch)
|
| 481 |
-
words = len(full_text.split())
|
| 482 |
-
return audio, f"Reading {len(files)} file{'s' if len(files)>1 else ''} — {words:,} words"
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
# ── CSS ───────────────────────────────────────────────────────────────────────
|
| 486 |
-
CSS = """
|
| 487 |
-
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=DM+Mono:wght@400;500&display=swap');
|
| 488 |
-
:root{--bg:#030712;--surface2:#061a0f;--border:#1a2544;--accent:#00CFFF;--accent2:#FFE500;
|
| 489 |
-
--text:#E8F4FF;--muted:#4A6080;--r:8px;
|
| 490 |
-
--green:#00e676;--green-border:rgba(0,230,118,0.3);}
|
| 491 |
-
body,.gradio-container{background:var(--bg)!important;font-family:'Syne',sans-serif!important;color:var(--text)!important;}
|
| 492 |
-
.hdr{text-align:center;padding:2.4rem 1rem 1.6rem;border-bottom:1px solid var(--border);
|
| 493 |
-
background:radial-gradient(ellipse at 30% 50%,rgba(0,102,255,0.12) 0%,transparent 60%),
|
| 494 |
-
radial-gradient(ellipse at 70% 50%,rgba(0,207,255,0.08) 0%,transparent 60%);}
|
| 495 |
-
.hdr h1{font-size:2.2rem;font-weight:800;letter-spacing:-0.04em;
|
| 496 |
-
background:linear-gradient(135deg,var(--accent) 0%,var(--accent2) 100%);
|
| 497 |
-
-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin:0 0 .3rem;}
|
| 498 |
-
.hdr .sub{color:var(--muted);font-family:'DM Mono',monospace;font-size:.75rem;letter-spacing:.06em;}
|
| 499 |
-
.hdr .own{color:var(--accent2);font-family:'DM Mono',monospace;font-size:.68rem;margin-top:.3rem;letter-spacing:.1em;text-transform:uppercase;}
|
| 500 |
-
input[type=text],textarea{background:#041a0c!important;border:1px solid var(--green-border)!important;
|
| 501 |
-
border-radius:var(--r)!important;color:#ccffe6!important;font-family:'DM Mono',monospace!important;
|
| 502 |
-
font-size:.86rem!important;padding:.6rem .85rem!important;}
|
| 503 |
-
input[type=text]:focus,textarea:focus{border-color:var(--green)!important;outline:none!important;
|
| 504 |
-
box-shadow:0 0 0 3px rgba(0,230,118,.15)!important;}
|
| 505 |
-
label>span:first-child{color:var(--green)!important;font-family:'DM Mono',monospace!important;
|
| 506 |
-
font-size:.7rem!important;letter-spacing:.08em!important;text-transform:uppercase!important;}
|
| 507 |
-
button.primary,.gr-button-primary{background:linear-gradient(135deg,#006600,var(--green))!important;
|
| 508 |
-
color:#030712!important;border:none!important;border-radius:var(--r)!important;
|
| 509 |
-
font-family:'Syne',sans-serif!important;font-weight:800!important;font-size:.92rem!important;
|
| 510 |
-
padding:.75rem 2.8rem!important;}
|
| 511 |
-
button.primary:hover{opacity:.88!important;transform:translateY(-1px)!important;}
|
| 512 |
-
button.secondary,.gr-button-secondary{background:#041a0c!important;color:var(--green)!important;
|
| 513 |
-
border:1px solid var(--green-border)!important;border-radius:var(--r)!important;
|
| 514 |
-
font-family:'Syne',sans-serif!important;font-weight:700!important;font-size:.8rem!important;
|
| 515 |
-
padding:.5rem 1.2rem!important;}
|
| 516 |
-
button.secondary:hover{background:var(--green)!important;color:#030712!important;}
|
| 517 |
-
.out textarea{background:#020e06!important;border:1px solid var(--green-border)!important;
|
| 518 |
-
border-left:3px solid var(--green)!important;border-radius:0 var(--r) var(--r) 0!important;
|
| 519 |
-
font-family:'DM Mono',monospace!important;font-size:.84rem!important;line-height:1.7!important;color:#ccffe6!important;}
|
| 520 |
-
.gradio-accordion{background:#061a0f!important;border:1px solid var(--green-border)!important;
|
| 521 |
-
border-radius:var(--r)!important;margin-bottom:.6rem!important;}
|
| 522 |
-
.gradio-accordion .label-wrap span{color:var(--green)!important;font-family:'Syne',sans-serif!important;
|
| 523 |
-
font-weight:700!important;font-size:.85rem!important;}
|
| 524 |
-
.gradio-accordion svg{color:var(--green)!important;stroke:var(--green)!important;}
|
| 525 |
-
.foot{text-align:center;padding:1.5rem;color:var(--muted);font-family:'DM Mono',monospace;
|
| 526 |
-
font-size:.68rem;border-top:1px solid var(--green-border);margin-top:2rem;letter-spacing:.05em;}
|
| 527 |
-
"""
|
| 528 |
-
|
| 529 |
-
# ── UI ────────────────────────────────────────────────────────────────────────
|
| 530 |
-
with gr.Blocks() as demo:
|
| 531 |
-
|
| 532 |
-
gr.HTML("""<div class="hdr">
|
| 533 |
-
<h1>⚡ The Metabolic Phoenix Framework</h1>
|
| 534 |
-
<p class="sub">Injectiv8 · Ms. Nala · Mistral 7B</p>
|
| 535 |
-
<p class="own">Invented & Owned by Dwayne Anthony Brian Galloway · 2026</p>
|
| 536 |
-
</div>""")
|
| 537 |
-
|
| 538 |
-
pdf_input = gr.File(
|
| 539 |
-
label="Upload Documents — PDF · Image · Audio · Video · Text — drop multiple",
|
| 540 |
-
file_types=[
|
| 541 |
-
".pdf",".txt",".md",
|
| 542 |
-
".jpg",".jpeg",".png",".gif",".webp",".bmp",".tiff",
|
| 543 |
-
".mp3",".wav",".m4a",".ogg",".flac",
|
| 544 |
-
"audio/mpeg","audio/mp3","audio/wav","audio/x-wav",
|
| 545 |
-
"audio/mp4","audio/ogg","audio/flac","audio/*",
|
| 546 |
-
".mp4",".mov",".avi",".mkv",".webm",
|
| 547 |
-
"video/mp4","video/quicktime","video/x-msvideo","video/x-matroska","video/webm","video/*",
|
| 548 |
-
],
|
| 549 |
-
file_count="multiple", type="filepath"
|
| 550 |
-
)
|
| 551 |
-
run_btn = gr.Button("⚡ Analyse Documents", variant="primary")
|
| 552 |
-
|
| 553 |
-
with gr.Row():
|
| 554 |
-
speed_slider = gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="🎚 Speed")
|
| 555 |
-
pitch_slider = gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="🎵 Pitch")
|
| 556 |
-
|
| 557 |
-
with gr.Accordion("🔊 Read Document — no analysis", open=True):
|
| 558 |
-
read_doc_btn = gr.Button("🔊 Read Document(s) Aloud — full length", variant="secondary")
|
| 559 |
-
read_doc_msg = gr.Markdown("")
|
| 560 |
-
read_doc_audio = gr.Audio(label="Document Audio", autoplay=True)
|
| 561 |
-
|
| 562 |
-
with gr.Accordion("Cycle 1 — Fact Ledger", open=True):
|
| 563 |
-
out1 = gr.Textbox(label="", lines=12, elem_classes=["out"])
|
| 564 |
-
rb1_btn = gr.Button("🔊 Read Cycle 1", variant="secondary")
|
| 565 |
-
audio1 = gr.Audio(label="Cycle 1 Audio", autoplay=True)
|
| 566 |
-
|
| 567 |
-
with gr.Accordion("Cycle 2 — Compliance Analysis", open=False):
|
| 568 |
-
out2 = gr.Textbox(label="", lines=12, elem_classes=["out"])
|
| 569 |
-
rb2_btn = gr.Button("🔊 Read Cycle 2", variant="secondary")
|
| 570 |
-
audio2 = gr.Audio(label="Cycle 2 Audio", autoplay=True)
|
| 571 |
-
|
| 572 |
-
with gr.Accordion("Cycle 3 — Ouroboros Loops", open=False):
|
| 573 |
-
out3 = gr.Textbox(label="", lines=12, elem_classes=["out"])
|
| 574 |
-
rb3_btn = gr.Button("🔊 Read Cycle 3", variant="secondary")
|
| 575 |
-
audio3 = gr.Audio(label="Cycle 3 Audio", autoplay=True)
|
| 576 |
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
gradio
|
| 580 |
-
pypdf
|
| 581 |
-
edge-tts
|
| 582 |
-
pydub
|
| 583 |
-
pillow
|
| 584 |
-
pytesseract
|
| 585 |
-
openai-whisper
|
| 586 |
-
ffmpeg-python
|
| 587 |
-
gtts
|
| 588 |
-
requests
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import re
|
| 3 |
import time
|
|
|
|
| 4 |
import tempfile
|
| 5 |
+
import asyncio
|
| 6 |
+
import subprocess
|
| 7 |
from pathlib import Path
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import spaces
|
| 10 |
|
| 11 |
+
# Optional imports – all installed via requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
from pypdf import PdfReader
|
|
|
|
| 13 |
import edge_tts
|
| 14 |
from pydub import AudioSegment
|
| 15 |
from PIL import Image
|
| 16 |
import pytesseract
|
| 17 |
import whisper
|
| 18 |
import requests as _req
|
|
|
|
| 19 |
|
| 20 |
+
# ── FREE HF INFERENCE API ────────────────────────────────────────────────
|
| 21 |
_FREE_API = "https://api-inference.huggingface.co/models/{model}"
|
| 22 |
_MODELS = [
|
| 23 |
"HuggingFaceH4/zephyr-7b-beta",
|
|
|
|
| 28 |
def call_llama(prompt: str, max_tokens: int = 2000) -> str:
|
| 29 |
token = os.environ.get("HF_TOKEN", "")
|
| 30 |
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
|
|
| 31 |
for model in _MODELS:
|
| 32 |
for attempt in range(4):
|
| 33 |
try:
|
| 34 |
+
fmt = prompt # already pre‑formatted in caller
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
resp = _req.post(
|
| 36 |
_FREE_API.format(model=model),
|
| 37 |
headers=headers,
|
|
|
|
| 47 |
},
|
| 48 |
timeout=180
|
| 49 |
)
|
|
|
|
| 50 |
if resp.status_code == 200:
|
| 51 |
result = resp.json()
|
| 52 |
if isinstance(result, list) and result:
|
| 53 |
text = result[0].get("generated_text", "").strip()
|
| 54 |
if text:
|
| 55 |
return text
|
|
|
|
| 56 |
elif resp.status_code == 503:
|
| 57 |
+
time.sleep(30 * (attempt + 1))
|
| 58 |
continue
|
|
|
|
| 59 |
elif resp.status_code in [429, 402]:
|
| 60 |
break
|
|
|
|
| 61 |
else:
|
| 62 |
+
time.sleep(10)
|
| 63 |
continue
|
|
|
|
| 64 |
except Exception:
|
| 65 |
+
time.sleep(10)
|
| 66 |
continue
|
| 67 |
+
return "Could not reach AI. Models may be loading — please try again in 30 seconds."
|
| 68 |
|
| 69 |
+
# ── FILE EXTRACTION ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
| 70 |
def extract_pdf_text(path):
|
| 71 |
try:
|
| 72 |
reader = PdfReader(path)
|
|
|
|
| 75 |
text = page.extract_text() or ""
|
| 76 |
if text.strip():
|
| 77 |
pages.append(f"--- Page {i+1} ---\n{text.strip()}")
|
| 78 |
+
return "\n\n".join(pages) if pages else "[No extractable text — may be scanned PDF]"
|
| 79 |
except Exception as e:
|
| 80 |
return f"[PDF read error: {e}]"
|
| 81 |
|
|
|
|
| 91 |
try:
|
| 92 |
model = whisper.load_model("base")
|
| 93 |
result = model.transcribe(path, language="en")
|
| 94 |
+
return f"[AUDIO]\n{result['text'].strip()}"
|
| 95 |
except Exception as e:
|
| 96 |
return f"[Transcription error: {e}]"
|
| 97 |
|
|
|
|
| 113 |
return transcribe_audio(tmp.name)
|
| 114 |
except Exception as e:
|
| 115 |
return f"[Video extraction error: {e}]"
|
| 116 |
+
elif ext == ".txt" or ext == ".md":
|
| 117 |
+
return Path(path).read_text(encoding="utf-8", errors="replace")
|
| 118 |
else:
|
| 119 |
+
return f"[Unsupported file type: {ext}]"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
# ── ANALYSIS PROMPTS ──────────────────────────────────────────────────────
|
| 122 |
PLAIN_ENGLISH = (
|
| 123 |
+
"\n\nCRITICAL — FORMATTING RULES:\n"
|
| 124 |
+
"Write in plain forceful English prose only.\n"
|
| 125 |
"No JSON. No code blocks. No curly brackets. No asterisks. No hash symbols.\n"
|
| 126 |
+
"Use numbered paragraphs and CAPITAL LETTER headings.\n"
|
| 127 |
+
"Do not hedge. If it is in the document, state it as confirmed fact."
|
|
|
|
|
|
|
|
|
|
| 128 |
)
|
| 129 |
|
| 130 |
def analyse(files):
|
| 131 |
+
if not files: return "Please upload at least one file.", "", "", ""
|
| 132 |
+
if not isinstance(files, list): files = [files]
|
|
|
|
|
|
|
| 133 |
|
| 134 |
all_content = []
|
| 135 |
for i, f in enumerate(files):
|
|
|
|
| 142 |
base = f"DOCUMENTS SUPPLIED ({len(files)} total):\n\n{doc_chunk}\n\n{'='*60}\n\n"
|
| 143 |
|
| 144 |
r1 = call_llama(base +
|
| 145 |
+
"You are Introbot — Metabolic Phoenix Framework.\n"
|
| 146 |
+
"Extract every confirmed fact: who, when, what it proves.\n"
|
| 147 |
+
"Label ADMISSION, DENIAL, CONTRADICTION, OMISSION.\n"
|
| 148 |
+
"State statutory duties triggered and when.\n"
|
| 149 |
+
"Format: FACT LEDGER." + PLAIN_ENGLISH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
)
|
| 151 |
|
| 152 |
r2 = call_llama(base +
|
| 153 |
+
"You are RHB 2.0 + CPF — Metabolic Phoenix Framework.\n"
|
| 154 |
+
"Breaches have occurred. For each institution: duty triggered, breached or complied?\n"
|
| 155 |
+
"Cite statute, section, date. No hedging. Trace causal chain.\n" + PLAIN_ENGLISH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
)
|
| 157 |
|
| 158 |
r3 = call_llama(base +
|
| 159 |
+
"You are Phoenix Badger — Metabolic Phoenix Framework.\n"
|
| 160 |
+
"RING A: Forward evasion tactics.\nRING B: Backward unlawful decisions.\n"
|
| 161 |
+
"RING C: Typology mutation.\nRING D: Drift correction (contradictions).\n"
|
| 162 |
+
"ESCALATION MAP: where this must go." + PLAIN_ENGLISH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
)
|
| 164 |
|
| 165 |
r4 = call_llama(
|
| 166 |
+
f"Fact Ledger:\n{r1[:1500]}\n\nBreach Analysis:\n{r2[:1500]}\n\nLoop Output:\n{r3[:1500]}\n\n"
|
| 167 |
+
"Produce litigation-ready case summary: Executive Summary, Confirmed Breaches, Top 5 Arguments, "
|
| 168 |
+
"Required Next Steps, Escalation Map.\n" + PLAIN_ENGLISH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
)
|
| 170 |
return r1, r2, r3, r4
|
| 171 |
|
| 172 |
+
# ── DOCUMENT GENERATION ───────────────────────────────────────────────────
|
| 173 |
+
# (Keep all your generation functions, they're fine — just remove the `.primary` button hack if you want)
|
| 174 |
|
| 175 |
+
# ── TTS with edge-tts (asyncio safe) ──────────────────────────────────────
|
| 176 |
+
async def edge_speak(text, path, voice):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
await edge_tts.Communicate(text, voice).save(path)
|
| 178 |
|
| 179 |
+
def _split_chunks(text, size=1500):
|
| 180 |
+
# same as before
|
| 181 |
+
...
|
| 182 |
+
def text_to_speech_full(text, speed=1.0, pitch=1.0):
|
| 183 |
+
if not text: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
chunks = _split_chunks(text)
|
| 185 |
+
files = []
|
| 186 |
+
loop = asyncio.new_event_loop()
|
| 187 |
+
asyncio.set_event_loop(loop)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
try:
|
|
|
|
|
|
|
|
|
|
| 189 |
for i, chunk in enumerate(chunks):
|
| 190 |
+
out = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3", dir="/tmp")
|
| 191 |
out.close()
|
| 192 |
+
loop.run_until_complete(edge_speak(chunk, out.name, "en-GB-SoniaNeural"))
|
| 193 |
+
if os.path.getsize(out.name) > 1000:
|
| 194 |
+
files.append(out.name)
|
| 195 |
+
finally:
|
| 196 |
+
loop.close()
|
| 197 |
+
if files:
|
| 198 |
+
combined = AudioSegment.empty()
|
| 199 |
+
for f in files:
|
| 200 |
+
combined += AudioSegment.from_mp3(f) + AudioSegment.silent(160)
|
| 201 |
+
# adjust speed/pitch here if needed
|
| 202 |
+
out = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3", dir="/tmp")
|
| 203 |
+
combined.export(out.name, format="mp3", bitrate="128k")
|
| 204 |
+
return out.name
|
| 205 |
return None
|
| 206 |
|
| 207 |
+
# ── GRADIO UI (shortened for brevity, use your full layout) ───────────────
|
| 208 |
+
with gr.Blocks(css=CSS) as demo:
|
| 209 |
+
gr.HTML("<h1>⚡ Metabolic Phoenix Framework</h1>")
|
| 210 |
+
pdf_input = gr.File(...)
|
| 211 |
+
# ... all your tabs, buttons, etc.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
|
| 213 |
+
if __name__ == "__main__":
|
| 214 |
+
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|