Spaces:
Running
Running
| """Conversation export helpers kept separate from Flask request handling.""" | |
| import os | |
| import re | |
| from typing import Iterable, Mapping | |
| def _unicode_font(base_dir: str) -> str: | |
| candidates = [ | |
| os.path.join(base_dir, "fonts", "NotoSans-Regular.ttf"), | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", | |
| r"C:\Windows\Fonts\arial.ttf", | |
| ] | |
| return next((candidate for candidate in candidates if os.path.exists(candidate)), "") | |
| def _core_font_text(value) -> str: | |
| text = str(value or "") | |
| replacements = { | |
| "→": " -> ", "⇒": " => ", "—": " - ", "–": " - ", | |
| "‘": "'", "’": "'", "“": '"', "”": '"', "…": "...", | |
| "•": "-", "✓": "[done]", "✗": "[failed]", | |
| } | |
| for source, replacement in replacements.items(): | |
| text = text.replace(source, replacement) | |
| return text.encode("latin-1", "replace").decode("latin-1") | |
| def _readable_export_text(value) -> str: | |
| text = str(value or "") | |
| text = re.sub(r"^#{1,6}\s+", "", text, flags=re.MULTILINE) | |
| text = text.replace("**", "").replace("__", "") | |
| text = re.sub(r"^- \[ \]\s+", "[ ] ", text, flags=re.MULTILINE) | |
| text = re.sub(r"^- \[[xX]\]\s+", "[done] ", text, flags=re.MULTILINE) | |
| return text | |
| def create_conversation_pdf(messages: Iterable[Mapping[str, str]], output_path: str, base_dir: str) -> str: | |
| """Create a paginated export without silently truncating normal conversation turns.""" | |
| from fpdf import FPDF | |
| class ConversationPDF(FPDF): | |
| def header(self): | |
| if self.page_no() <= 1: | |
| return | |
| self.set_font("Helvetica", "", 8) | |
| self.set_text_color(90, 100, 115) | |
| self.cell(0, 6, "Invicta AI Conversation Export", align="R") | |
| self.ln(10) | |
| self.set_text_color(0, 0, 0) | |
| def footer(self): | |
| self.set_y(-12) | |
| self.set_font("Helvetica", "", 8) | |
| self.set_text_color(110, 120, 132) | |
| self.cell(0, 6, f"Page {self.page_no()}/{{nb}}", align="C") | |
| pdf = ConversationPDF() | |
| pdf.alias_nb_pages() | |
| pdf.set_title("Invicta AI Conversation Export") | |
| pdf.set_author("InvictaTill AI") | |
| pdf.set_auto_page_break(auto=True, margin=15) | |
| pdf.add_page() | |
| font_path = _unicode_font(base_dir) | |
| if font_path: | |
| pdf.add_font("InvictaUnicode", "", font_path) | |
| pdf.set_font("InvictaUnicode", "", 16) | |
| else: | |
| pdf.set_font("Helvetica", "B", 16) | |
| pdf.cell(0, 10, "Invicta AI Conversation Export", new_x="LMARGIN", new_y="NEXT", align="C") | |
| pdf.ln(5) | |
| for message in messages: | |
| role = str(message.get("role", "user")).upper() | |
| content = _readable_export_text(str(message.get("content", ""))[:20_000]) | |
| if not font_path: | |
| role = _core_font_text(role) | |
| content = _core_font_text(content) | |
| pdf.set_font("InvictaUnicode" if font_path else "Helvetica", "" if font_path else "B", 11) | |
| pdf.multi_cell(0, 6, f"[{role}]", new_x="LMARGIN", new_y="NEXT") | |
| pdf.set_font("InvictaUnicode" if font_path else "Helvetica", "", 10) | |
| pdf.multi_cell(0, 5.5, content, new_x="LMARGIN", new_y="NEXT") | |
| pdf.ln(2) | |
| pdf.output(output_path) | |
| return output_path | |