Spaces:
Running
Running
File size: 7,882 Bytes
7ee2a84 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | """High-level API: build the target spec from all sources and format a manuscript."""
from __future__ import annotations
import io
import os
import re
import shutil
import subprocess
import tempfile
import zipfile
from dataclasses import dataclass, field
from typing import Optional
from .pipeline import Report, prepare
from .read_any import read_any
from .render_docx import render_docx
from .render_latex import render_latex
from .spec import DEFAULT_SPEC, deep_merge, get_preset, lookup_journal, parse_guidelines, parse_guidelines_llm
from .template import analyze_docx_template, analyze_pdf_sample
def dotx_to_docx(data: bytes) -> bytes:
"""Word templates (.dotx/.dotm) differ from .docx only in the main part's content type."""
src = zipfile.ZipFile(io.BytesIO(data))
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as out:
for item in src.infolist():
content = src.read(item.filename)
if item.filename == "[Content_Types].xml":
content = re.sub(rb"application/vnd\.ms-word\.template\.macroEnabledTemplate\.main\+xml|"
rb"application/vnd\.openxmlformats-officedocument\.wordprocessingml\.template\.main\+xml",
b"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
content)
out.writestr(item, content)
return buf.getvalue()
def guidelines_text_from_file(name: str, data: bytes) -> str:
ext = name.rsplit(".", 1)[-1].lower()
if ext == "pdf":
import pymupdf
return "\n".join(p.get_text() for p in pymupdf.open(stream=data, filetype="pdf"))
if ext in ("docx", "dotx"):
import docx
d = docx.Document(io.BytesIO(dotx_to_docx(data) if ext == "dotx" else data))
return "\n".join(p.text for p in d.paragraphs)
if ext in ("html", "htm"):
return html_to_text(data.decode("utf-8", "ignore"))
return data.decode("utf-8", "ignore")
def html_to_text(html: str) -> str:
html = re.sub(r"(?is)<(script|style|nav|footer|header)[^>]*>.*?</\1>", " ", html)
html = re.sub(r"(?i)<br\s*/?>|</p>|</li>|</h\d>|</div>", "\n", html)
text = re.sub(r"<[^>]+>", " ", html)
import html as h
text = h.unescape(text)
return re.sub(r"[ \t]+", " ", re.sub(r"\n\s*\n+", "\n\n", text)).strip()
def fetch_guidelines_url(url: str, timeout=20) -> str:
import requests
r = requests.get(url, timeout=timeout, headers={"User-Agent": "Mozilla/5.0 (FormatMatch guideline reader)"})
r.raise_for_status()
ctype = r.headers.get("content-type", "")
if "pdf" in ctype or url.lower().endswith(".pdf"):
return guidelines_text_from_file("g.pdf", r.content)
return html_to_text(r.text)
@dataclass
class FormatSource:
preset: Optional[str] = None # preset key chosen/derived from journal name
journal: Optional[dict] = None # lookup_journal() result
guidelines: Optional[dict] = None # overrides from guidelines
template_overrides: Optional[dict] = None # overrides from template/sample
profile: object = None # TemplateProfile (docx templates only)
manual: dict = field(default_factory=dict) # user edits in the UI
found: list = field(default_factory=list) # human-readable detection notes
def build_spec(src: FormatSource) -> dict:
spec = get_preset(src.preset) if src.preset else DEFAULT_SPEC
if src.guidelines:
spec = deep_merge(spec, {k: v for k, v in src.guidelines.items() if not k.startswith("_")})
if src.template_overrides:
spec = deep_merge(spec, {k: v for k, v in src.template_overrides.items() if not k.startswith("_")})
if src.manual:
spec = deep_merge(spec, src.manual)
if src.journal and src.journal.get("title"):
spec["name"] = src.journal["title"] + (f" ({spec.get('name')})" if spec.get("name") else "")
return spec
def learn_template(name: str, data: bytes):
"""Returns (overrides, profile_or_None, found_notes)."""
ext = name.rsplit(".", 1)[-1].lower()
if ext in ("dotx", "dotm"):
data = dotx_to_docx(data)
ext = "docx"
if ext == "docx":
ov, prof = analyze_docx_template(data)
return ov, prof, list(prof.found)
if ext == "pdf":
ov = analyze_pdf_sample(data)
return ov, None, ov.pop("_found", [])
if ext in ("doc", "odt", "rtf"):
from .read_any import _office_to_docx
d2 = _office_to_docx(data, ext)
ov, prof = analyze_docx_template(d2)
return ov, prof, list(prof.found)
raise RuntimeError("Template/sample must be .docx, .dotx, .doc or a published-paper .pdf")
@dataclass
class Result:
docx: Optional[bytes]
latex_zip: Optional[bytes]
report: Report
report_md: str
spec: dict
preview_pdf: Optional[bytes] = None
def format_manuscript(filename: str, data: bytes, spec: dict, profile=None, want_docx=True, want_latex=True,
crossref=False, progress=None, preview=False) -> Result:
def prog(f, msg):
if progress:
progress(f, msg)
prog(0.05, "Reading manuscript")
doc = read_any(filename, data)
prog(0.2, "Restructuring: headings, captions, citations, references")
prepared, rep = prepare(doc, spec, crossref=crossref,
progress=(lambda f, m: prog(0.2 + 0.4 * f, m)) if crossref else None)
docx_bytes = latex_bytes = None
if want_docx:
prog(0.65, "Writing Word document")
docx_bytes, w = render_docx(prepared, spec, profile)
for x in w:
rep.warn(x)
if want_latex:
prog(0.8, "Writing LaTeX project")
lw = []
latex_bytes = render_latex(prepared, spec, rep.refs, lw)
for x in lw:
rep.warn(x)
pdf = None
if preview and docx_bytes:
prog(0.9, "Rendering preview")
pdf = docx_to_pdf(docx_bytes)
prog(1.0, "Done")
return Result(docx_bytes, latex_bytes, rep, report_markdown(rep, spec, filename), spec, pdf)
def docx_to_pdf(data: bytes) -> Optional[bytes]:
soffice = shutil.which("soffice") or shutil.which("libreoffice")
if not soffice:
return None
with tempfile.TemporaryDirectory() as td:
src = os.path.join(td, "out.docx")
open(src, "wb").write(data)
try:
subprocess.run([soffice, "--headless", "--convert-to", "pdf", "--outdir", td, src],
capture_output=True, timeout=180)
except Exception:
return None
pdf = os.path.join(td, "out.pdf")
return open(pdf, "rb").read() if os.path.exists(pdf) else None
def report_markdown(rep: Report, spec: dict, filename: str) -> str:
L = [f"# Formatting report — {filename}", "", f"**Target format:** {spec.get('name', 'Custom')}", ""]
L += ["## Compliance checks", "", "| Check | Status | Detail |", "|---|---|---|"]
for name, ok, detail in rep.checks:
L.append(f"| {name} | {'✅ Pass' if ok else '⚠️ Fix'} | {detail} |")
L += ["", "## Changes made", ""] + [f"- {c}" for c in rep.changes]
if rep.warnings:
L += ["", "## Needs your attention", ""] + [f"- {w}" for w in rep.warnings]
S = spec
L += ["", "## Format applied", "",
f"- Page: {S['page']['size']}, {S['page']['columns']} column(s), margins (T/B/L/R) {S['page']['margins_cm']} cm",
f"- Body: {S['font']['family']} {S['font']['size']} pt, line spacing {S['font']['line_spacing']}",
f"- Headings: {S['headings']['scheme']} numbering",
f"- References: {S['references']['style']}; citations: {S['citations']['mode']}",
"", "_Generated by FormatMatch — CS Software Solutions (cssoftwaresolutions.in)_"]
return "\n".join(L)
|