lrkkumar/portfolio-analysis
0
1import gradio as gr2import requests3import os4import json5 6import time, re, math7 8def call_n8n(input_type, uploaded_file, json_data):9 N8N_WEBHOOK_URL = "https://tsademo.app.n8n.cloud/webhook-test/68f9ae31-c226-4a14-b398-671819b8433b"10 if input_type in ("pdf", "csv"):11 file_path = uploaded_file.name if hasattr(uploaded_file, "name") else str(uploaded_file)12 with open(file_path, "rb") as f:13 files = {14 "file": (os.path.basename(file_path), f, "application/pdf" if input_type == "pdf" else "text/csv")15 }16 data = {"type": input_type}17 resp = requests.post(N8N_WEBHOOK_URL, data=data, files=files, timeout=120)18 else:19 payload = {"type": "json", "data":json.loads(json_data)}20 resp = requests.post(N8N_WEBHOOK_URL, json=payload, timeout=120)21 ct = resp.headers.get("content-type", "").lower()22 if "application/json" in ct:23 data = resp.json()24 # Expecting {"text": "..."}25 if isinstance(data, dict) and "text" in data:26 return data["text"]27 # Fallback if structure is unexpected28 return json.dumps(data, indent=2)29 else:30 return resp.text31 32def text_to_pdf(text: str) -> str:33 text = text or ""34 ts = time.strftime("%Y%m%d-%H%M%S")35 out_path = os.path.join("/tmp", f"portfolio_analysis_{ts}.pdf")36 37 # --- Sanitize text to avoid PDF string/encoding issues ---38 # Keep it stable: replace non-Latin-1 chars so PDF always renders.39 # (If you need Telugu/Unicode in the PDF, you must embed a Unicode font—requires a library.)40 safe = text.encode("latin-1", errors="replace").decode("latin-1")41 42 # Normalize line endings and remove problematic control chars (except \n)43 safe = safe.replace("\r\n", "\n").replace("\r", "\n")44 safe = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F]", " ", safe)45 46 def esc_pdf_string(s: str) -> str:47 return s.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")48 49 # --- Page geometry (A4 points) ---50 page_w, page_h = 595, 84251 left, right, top, bottom = 50, 50, 60, 6052 font_size = 1153 leading = 1454 max_chars = 90 # rough wrap; works well enough without font metrics55 56 # Wrap lines57 wrapped_lines = []58 for raw in safe.split("\n"):59 raw = raw.rstrip()60 if not raw:61 wrapped_lines.append("")62 continue63 # simple wrap by character count64 while len(raw) > max_chars:65 wrapped_lines.append(raw[:max_chars])66 raw = raw[max_chars:]67 wrapped_lines.append(raw)68 69 # Paginate70 lines_per_page = max(1, int((page_h - top - bottom) / leading))71 pages = [72 wrapped_lines[i:i + lines_per_page]73 for i in range(0, len(wrapped_lines), lines_per_page)74 ] or [[""]]75 76 # --- Build PDF objects dynamically with correct xref offsets ---77 objects = []78 79 def add_obj(obj_str: str) -> int:80 objects.append(obj_str)81 return len(objects) # object number (1-based, 0 reserved)82 83 # 1) Catalog (added later after Pages obj number known)84 # 2) Pages85 # 3..n) Page objects86 # Contents objects per page87 # Font object88 89 # Font object90 font_obj = add_obj("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")91 92 # Placeholder for Pages and Catalog; we’ll fill after we know Kids93 pages_placeholder_index = len(objects) # 0-based index94 pages_obj_num = add_obj("<<>>") # placeholder95 catalog_obj_num = add_obj(f"<< /Type /Catalog /Pages {pages_obj_num} 0 R >>")96 97 page_obj_nums = []98 contents_obj_nums = []99 100 # Create each page + its content stream101 for page_lines in pages:102 # Build content stream103 y = page_h - top104 stream_cmds = [f"BT /F1 {font_size} Tf"]105 for line in page_lines:106 stream_cmds.append(f"1 0 0 1 {left} {y} Tm ({esc_pdf_string(line)}) Tj")107 y -= leading108 stream_cmds.append("ET")109 stream_data = "\n".join(stream_cmds).encode("latin-1", errors="replace")110 contents_obj_num = add_obj(111 f"<< /Length {len(stream_data)} >>\nstream\n{stream_data.decode('latin-1')}\nendstream"112 )113 contents_obj_nums.append(contents_obj_num)114 115 # Page object (refers to Pages parent, Font, and its Contents)116 page_obj_num = add_obj(117 f"<< /Type /Page /Parent {pages_obj_num} 0 R "118 f"/MediaBox [0 0 {page_w} {page_h}] "119 f"/Resources << /Font << /F1 {font_obj} 0 R >> >> "120 f"/Contents {contents_obj_num} 0 R >>"121 )122 page_obj_nums.append(page_obj_num)123 124 # Now replace Pages placeholder with correct Kids list125 kids = " ".join([f"{n} 0 R" for n in page_obj_nums])126 pages_obj = f"<< /Type /Pages /Kids [ {kids} ] /Count {len(page_obj_nums)} >>"127 objects[pages_placeholder_index] = pages_obj # overwrite placeholder string128 129 # --- Assemble PDF with xref table ---130 pdf_parts = [b"%PDF-1.4\n"]131 xref_offsets = [0] # object 0 offset is 0 by spec132 133 # write each obj and record offset134 for i, obj in enumerate(objects, start=1):135 xref_offsets.append(sum(len(p) for p in pdf_parts))136 pdf_parts.append(f"{i} 0 obj\n{obj}\nendobj\n".encode("latin-1", errors="replace"))137 138 xref_start = sum(len(p) for p in pdf_parts)139 140 # xref table141 xref = [b"xref\n"]142 xref.append(f"0 {len(objects)+1}\n".encode("latin-1"))143 xref.append(b"0000000000 65535 f \n")144 for off in xref_offsets[1:]:145 xref.append(f"{off:010d} 00000 n \n".encode("latin-1"))146 147 trailer = (148 f"trailer\n<< /Size {len(objects)+1} /Root {catalog_obj_num} 0 R >>\n"149 f"startxref\n{xref_start}\n%%EOF\n"150 ).encode("latin-1")151 152 with open(out_path, "wb") as f:153 for part in pdf_parts:154 f.write(part)155 for part in xref:156 f.write(part)157 f.write(trailer)158 159 return out_path160with gr.Blocks() as demo:161 type = gr.Radio(choices=['json', 'pdf', 'csv'], label="Data type")162 file = gr.File()163 json_text = gr.Textbox(lines=5, label="Paste JSON portfolio")164 process_btn = gr.Button(value="Process")165 analysis_text = gr.Textbox(label="LLM Analysis", lines=20, interactive=True)166 process_btn.click(167 fn=call_n8n,168 inputs=[type, file, json_text],169 outputs=analysis_text170 )171 gr.Markdown("### Download")172 pdf_btn = gr.Button(value="Download as PDF")173 pdf_file = gr.File(label="Generated PDF")174 175 pdf_btn.click(176 fn=text_to_pdf,177 inputs=analysis_text,178 outputs=pdf_file179 )180 181demo.launch()