smjain commited on
Commit
6a67e8b
·
verified ·
1 Parent(s): f5dc454

Upload drawio_engine.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. drawio_engine.py +179 -0
drawio_engine.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Agentic draw.io build engine.
3
+
4
+ extract_plan(path) : full .drawio -> compact build-plan (style table + nodes +
5
+ containers + edges, real geometry & SAP styles, base64
6
+ image payloads stripped).
7
+ build_drawio(plan) : build-plan -> complete, valid .drawio XML (the executor).
8
+
9
+ The model is trained to emit the build-plan; the engine renders the full file.
10
+ Round-tripping every plan guarantees the target is reconstructible & valid.
11
+ """
12
+ import base64, zlib, urllib.parse, html, re, json
13
+ import xml.etree.ElementTree as ET
14
+ from xml.sax.saxutils import quoteattr
15
+
16
+ IMAGE_DATA_RE = re.compile(r"image=data:[^;,]+;base64,[^;]+;?")
17
+ IMG_RE = re.compile(r"image=(data:[^;]+)") # SAP icons: data:image/png,<b64>
18
+ WS_RE = re.compile(r"\s+")
19
+
20
+ def signature(style):
21
+ """Group styles by icon identity (image payload) else by full string."""
22
+ if not style:
23
+ return "_plain_"
24
+ m = IMG_RE.search(style)
25
+ return ("img:" + m.group(1)) if m else ("sty:" + style.strip())
26
+
27
+ def _inflate(text):
28
+ text = (text or "").strip()
29
+ if text.startswith("<"):
30
+ return text
31
+ try:
32
+ return urllib.parse.unquote(zlib.decompress(base64.b64decode(text), -15).decode("utf-8"))
33
+ except Exception:
34
+ return None
35
+
36
+ def _label(val):
37
+ if not val:
38
+ return ""
39
+ return WS_RE.sub(" ", re.sub(r"<[^>]+>", " ", html.unescape(val))).strip()
40
+
41
+ def _clean_style(style):
42
+ """Strip heavy base64 image payloads but keep SAP shapes/colors/fonts."""
43
+ if not style:
44
+ return "rounded=0;whiteSpace=wrap;html=1;"
45
+ s = IMAGE_DATA_RE.sub("", style)
46
+ s = re.sub(r";{2,}", ";", s).strip(";")
47
+ return (s + ";") if s else "rounded=0;whiteSpace=wrap;html=1;"
48
+
49
+ def _models(path):
50
+ data = open(path, "r", encoding="utf-8", errors="ignore").read()
51
+ try:
52
+ tree = ET.fromstring(data)
53
+ except ET.ParseError:
54
+ return
55
+ for di, diagram in enumerate(tree.iter("diagram")):
56
+ gm = diagram.find("mxGraphModel")
57
+ if gm is None:
58
+ inf = _inflate(diagram.text)
59
+ if not inf:
60
+ continue
61
+ try:
62
+ gm = ET.fromstring(inf)
63
+ except ET.ParseError:
64
+ continue
65
+ yield diagram.get("name", f"Page-{di+1}"), gm
66
+
67
+ def extract_plan(path, sig2key, max_pages=1):
68
+ """Return list of build-plans (one per page). Styles -> library keys."""
69
+ plans = []
70
+ for name, gm in _models(path):
71
+ root = gm.find("root")
72
+ if root is None:
73
+ continue
74
+ def sid(style):
75
+ return sig2key.get(signature(style), "box")
76
+ nodes, edges, idmap = [], [], {}
77
+ order = []
78
+ for cell in root.iter("mxCell"):
79
+ cid = cell.get("id")
80
+ if cid in (None, "0", "1"):
81
+ continue
82
+ order.append(cell)
83
+ # assign compact ids
84
+ for i, cell in enumerate(order):
85
+ idmap[cell.get("id")] = i + 2
86
+ for cell in order:
87
+ cid = idmap[cell.get("id")]
88
+ if cell.get("vertex") == "1":
89
+ geo = cell.find("mxGeometry")
90
+ if geo is None:
91
+ continue
92
+ try:
93
+ x = int(float(geo.get("x", 0) or 0)); y = int(float(geo.get("y", 0) or 0))
94
+ w = int(float(geo.get("width", 80) or 80)); h = int(float(geo.get("height", 60) or 60))
95
+ except ValueError:
96
+ x = y = 0; w, h = 80, 60
97
+ parent = idmap.get(cell.get("parent"), 1)
98
+ n = {"i": cid, "l": _label(cell.get("value")), "k": sid(cell.get("style", "")),
99
+ "x": x, "y": y, "w": w, "h": h}
100
+ if parent != 1:
101
+ n["p"] = parent
102
+ nodes.append(n)
103
+ elif cell.get("edge") == "1":
104
+ s = idmap.get(cell.get("source")); t = idmap.get(cell.get("target"))
105
+ if s is None or t is None:
106
+ continue
107
+ e = {"a": s, "b": t, "k": sid(cell.get("style", ""))}
108
+ lab = _label(cell.get("value"))
109
+ if lab:
110
+ e["l"] = lab
111
+ edges.append(e)
112
+ if len(nodes) < 2:
113
+ continue
114
+ plans.append({"page": name, "nodes": nodes, "edges": edges})
115
+ return plans[:max_pages] if max_pages else plans
116
+
117
+ def build_drawio(plan, lib):
118
+ """Executor: build-plan + style library -> full valid .drawio XML string."""
119
+ cells = ['<mxCell id="0" />', '<mxCell id="1" parent="0" />']
120
+ def st(k):
121
+ return lib.get(k, "rounded=0;whiteSpace=wrap;html=1;")
122
+ for n in plan.get("nodes", []):
123
+ parent = n.get("p", 1)
124
+ cells.append(
125
+ f'<mxCell id="{n["i"]}" value={quoteattr(str(n.get("l","")))} '
126
+ f'style={quoteattr(st(n.get("s",0)))} vertex="1" parent="{parent}">'
127
+ f'<mxGeometry x="{n.get("x",0)}" y="{n.get("y",0)}" '
128
+ f'width="{n.get("w",80)}" height="{n.get("h",60)}" as="geometry" /></mxCell>')
129
+ eid = 100000
130
+ for e in plan.get("edges", []):
131
+ lab = e.get("l", "")
132
+ vattr = f' value={quoteattr(str(lab))}' if lab else ''
133
+ cells.append(
134
+ f'<mxCell id="{eid}"{vattr} style={quoteattr(st(e.get("s",0)))} '
135
+ f'edge="1" parent="1" source="{e["a"]}" target="{e["b"]}">'
136
+ f'<mxGeometry relative="1" as="geometry" /></mxCell>')
137
+ eid += 1
138
+ name = plan.get("page", "Architecture")
139
+ return (f'<mxfile host="archgen"><diagram name={quoteattr(name)}>'
140
+ f'<mxGraphModel dx="1200" dy="800" grid="0" gridSize="10" guides="1" '
141
+ f'tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" '
142
+ f'pageWidth="1600" pageHeight="1100" math="0" shadow="0"><root>'
143
+ + "".join(cells) +
144
+ '</root></mxGraphModel></diagram></mxfile>')
145
+
146
+ def plan_to_text(plan):
147
+ """Compact one-line-per-record serialization the model emits."""
148
+ return json.dumps(plan, ensure_ascii=False, separators=(",", ":"))
149
+
150
+ def validate(xml):
151
+ """True if well-formed and structurally a real diagram."""
152
+ try:
153
+ t = ET.fromstring(xml)
154
+ except ET.ParseError:
155
+ return False, "parse-error"
156
+ cells = list(t.iter("mxCell"))
157
+ verts = [c for c in cells if c.get("vertex") == "1"]
158
+ return (len(verts) >= 2), f"{len(verts)} nodes, {len([c for c in cells if c.get('edge')=='1'])} edges"
159
+
160
+ if __name__ == "__main__":
161
+ import glob, os
162
+ lib = json.load(open("dataset/style_library.json"))
163
+ sig2key = json.load(open("dataset/sig2key.json"))
164
+ toks = []
165
+ for f in sorted(glob.glob("data_refarch/**/*.drawio", recursive=True)):
166
+ if "/archive/" in f:
167
+ continue
168
+ for plan in extract_plan(f, sig2key, max_pages=2):
169
+ txt = plan_to_text(plan)
170
+ xml = build_drawio(plan, lib)
171
+ ok, info = validate(xml)
172
+ t = len(txt) // 3
173
+ toks.append(t)
174
+ print(f"{t:6d}tok plan | nodes={len(plan['nodes']):3d} edges={len(plan['edges']):3d} "
175
+ f"| {'OK ' if ok else 'BAD'} {info:18s} | {os.path.basename(f)[:42]}")
176
+ if toks:
177
+ print(f"\npages={len(toks)} max~tok={max(toks)} "
178
+ f"median~={sorted(toks)[len(toks)//2]} mean~={sum(toks)//len(toks)} "
179
+ f"| >7000tok: {sum(1 for t in toks if t>7000)}")