Safoura Banihashemi commited on
Commit
fb3203b
·
1 Parent(s): e661e27

Support pipeline result JSON format

Browse files
Files changed (1) hide show
  1. app.py +158 -40
app.py CHANGED
@@ -1,7 +1,9 @@
1
  """
2
  Argumentation Graph Visualizer — Hugging Face Space
3
- Supports .xml (Demosthenes) and .json (structured sections with inline tags).
4
- Graph is rendered in an iframe to allow full JavaScript execution.
 
 
5
  """
6
 
7
  import re
@@ -24,7 +26,7 @@ def parse_xml(filepath):
24
  nid = elem.get("ID")
25
  if not nid:
26
  continue
27
- nodes[nid] = elem.tag
28
  for t in filter(None, elem.get("SUP", "").split("|")):
29
  sup_edges.append((nid, t.strip()))
30
  for t in filter(None, elem.get("ATT", "").split("|")):
@@ -32,7 +34,8 @@ def parse_xml(filepath):
32
  return nodes, sup_edges, att_edges
33
 
34
 
35
- def parse_json(filepath):
 
36
  with open(filepath, "r", encoding="utf-8") as f:
37
  data = json.load(f)
38
  nodes, sup_edges, att_edges = {}, [], []
@@ -46,7 +49,7 @@ def parse_json(filepath):
46
  if not id_m:
47
  continue
48
  nid = id_m.group(1)
49
- nodes[nid] = tag
50
  sup_m = re.search(r'SUP="([^"]+)"', attrs)
51
  if sup_m:
52
  for t in filter(None, sup_m.group(1).split("|")):
@@ -58,13 +61,126 @@ def parse_json(filepath):
58
  return nodes, sup_edges, att_edges, data.get("document", "")
59
 
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  # ── HTML builder ──────────────────────────────────────────────────────────────
62
 
63
- def build_graph_html(nodes, sup_edges, att_edges, doc_name=""):
64
- nodes_js = json.dumps([{"id": k, "type": v} for k, v in nodes.items()])
65
- sup_js = json.dumps([{"source": s, "target": t} for s, t in sup_edges])
66
- att_js = json.dumps([{"source": s, "target": t} for s, t in att_edges])
67
- label = doc_name.replace("_", " ") if doc_name else "argumentation graph"
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  return """<!DOCTYPE html>
70
  <html lang="en">
@@ -117,8 +233,7 @@ def build_graph_html(nodes, sup_edges, att_edges, doc_name=""):
117
  .link-sup { stroke:var(--green); stroke-width:1.8; stroke-opacity:.75; fill:none; }
118
  .link-att { stroke:var(--red); stroke-width:2.2; stroke-opacity:.9;
119
  stroke-dasharray:7 3; fill:none; }
120
- .ng { cursor:grab; }
121
- .ng:active { cursor:grabbing; }
122
  .cp { fill:var(--blue-bg); stroke:var(--blue-br); stroke-width:1.5; transition:fill .15s; }
123
  .cc { fill:var(--pur-bg); stroke:var(--pur-br); stroke-width:1.5; transition:fill .15s; }
124
  .ng:hover .cp { fill:var(--blue-br); }
@@ -127,10 +242,10 @@ def build_graph_html(nodes, sup_edges, att_edges, doc_name=""):
127
  text-anchor:middle; dominant-baseline:central; pointer-events:none; }
128
  #tt {
129
  position:fixed; display:none; background:#1c2030;
130
- border:1px solid var(--border); border-radius:7px; padding:9px 13px;
131
- font-family:'IBM Plex Mono',monospace; font-size:11px; line-height:1.75;
132
- pointer-events:none; z-index:999; max-width:260px; color:var(--text);
133
- box-shadow:0 8px 32px rgba(0,0,0,.55);
134
  }
135
  </style>
136
  </head>
@@ -173,16 +288,15 @@ const g = svg.append("g");
173
  svg.call(d3.zoom().scaleExtent([0.1, 5]).on("zoom", e => g.attr("transform", e.transform)));
174
 
175
  const sim = d3.forceSimulation(NODES)
176
- .force("link", d3.forceLink(links).id(d => d.id).distance(85).strength(0.55))
177
- .force("charge", d3.forceManyBody().strength(-340))
178
  .force("center", d3.forceCenter(W / 2, H / 2))
179
- .force("collision", d3.forceCollide(20));
180
 
181
  const lsel = g.append("g").selectAll("line").data(links).join("line")
182
  .attr("class", d => d.kind === "sup" ? "link-sup" : "link-att")
183
  .attr("marker-end", d => d.kind === "sup" ? "url(#ms)" : "url(#ma)");
184
 
185
- // adjacency for tooltip
186
  const adj = {};
187
  NODES.forEach(n => { adj[n.id] = {sf:[], st:[], af:[], at:[]}; });
188
  SUP.forEach(e => { adj[e.target].sf.push(e.source); adj[e.source].st.push(e.target); });
@@ -198,7 +312,12 @@ const nsel = g.append("g").selectAll("g").data(NODES).join("g")
198
  .on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }))
199
  .on("mouseover", (e, d) => {
200
  const a = adj[d.id];
201
- let h = "<b style='font-size:13px'>" + d.id + "</b> <span style='color:#64748b'>" + d.type + "</span><hr style='border:none;border-top:1px solid #2a2f42;margin:5px 0'>";
 
 
 
 
 
202
  if (a.sf.length) h += "<div style='color:#22c55e'>◀ SUP from: " + a.sf.join(", ") + "</div>";
203
  if (a.st.length) h += "<div style='color:#22c55e'>▶ SUP to: " + a.st.join(", ") + "</div>";
204
  if (a.af.length) h += "<div style='color:#ef4444'>◀ ATT from: " + a.af.join(", ") + "</div>";
@@ -211,9 +330,9 @@ const nsel = g.append("g").selectAll("g").data(NODES).join("g")
211
  .on("mousemove", e => { tt.style.left = (e.clientX + 14) + "px"; tt.style.top = (e.clientY - 12) + "px"; })
212
  .on("mouseleave", () => { tt.style.display = "none"; });
213
 
214
- nsel.append("circle").attr("r", 13).attr("class", d => d.type === "prem" ? "cp" : "cc");
215
  nsel.append("text").attr("class", "nl")
216
- .style("font-size", d => d.id.length > 4 ? "7px" : "9px")
217
  .text(d => d.id);
218
 
219
  sim.on("tick", () => {
@@ -226,10 +345,7 @@ sim.on("tick", () => {
226
  </html>"""
227
 
228
 
229
- # ── File handler ──────────────────────────────────────────────────────────────
230
-
231
- # We save the HTML to a temp file and serve it via gr.File so it opens in browser,
232
- # AND embed it in an iframe inside gr.HTML for inline preview.
233
 
234
  def process_file(file_obj):
235
  if file_obj is None:
@@ -241,26 +357,27 @@ def process_file(file_obj):
241
  try:
242
  if ext == "xml":
243
  nodes, sup_edges, att_edges = parse_xml(path)
244
- doc_name = os.path.basename(path).replace(".xml", "").replace("_", " ")
 
 
245
  elif ext == "json":
246
- nodes, sup_edges, att_edges, doc_name = parse_json(path)
247
  else:
248
  return None, "<p style='color:#ef4444;font-family:monospace;padding:20px'>Upload .xml or .json</p>"
249
  except Exception as ex:
250
- return None, f"<p style='color:#ef4444;font-family:monospace;padding:20px'>Parse error: {ex}</p>"
 
251
 
252
- if not nodes:
253
- return None, "<p style='color:#ef4444;font-family:monospace;padding:20px'>No nodes found.</p>"
254
 
255
- html = build_graph_html(nodes, sup_edges, att_edges, doc_name)
256
 
257
- # Write to a named temp file
258
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8")
259
  tmp.write(html)
260
  tmp.close()
261
 
262
- # Inline iframe preview (srcdoc)
263
- srcdoc = html.replace('"', "&quot;")
264
  iframe = f'<iframe srcdoc="{srcdoc}" style="width:100%;height:680px;border:none;border-radius:8px;"></iframe>'
265
 
266
  return tmp.name, iframe
@@ -294,7 +411,7 @@ with gr.Blocks(
294
  with gr.Row(elem_id="title-row"):
295
  gr.HTML("""
296
  <h1>⚖ Argumentation Graph Visualizer</h1>
297
- <p>Upload a Demosthenes <b>.xml</b> or structured <b>.json</b> file
298
  → interactive force-directed argument graph.</p>
299
  """)
300
 
@@ -302,7 +419,7 @@ with gr.Blocks(
302
  with gr.Column(scale=1, min_width=260):
303
  file_input = gr.File(label="Upload XML or JSON", file_types=[".xml", ".json"])
304
  run_btn = gr.Button("Build Graph", variant="primary", size="lg")
305
- dl_out = gr.File(label="Download graph HTML", visible=True)
306
  gr.HTML("""
307
  <div style="font-family:monospace;font-size:11px;color:#64748b;
308
  border:1px solid #2a2f42;border-radius:8px;padding:12px;margin-top:6px;">
@@ -330,8 +447,9 @@ with gr.Blocks(
330
  <circle cx="6" cy="6" r="5" fill="#4c1d95" stroke="#a855f7" stroke-width="1.5"/>
331
  </svg> conc (conclusion)
332
  </div>
333
- <div style="margin-top:8px;color:#475569;">
334
- Drag · Scroll to zoom · Hover node
 
335
  </div>
336
  </div>
337
  """)
 
1
  """
2
  Argumentation Graph Visualizer — Hugging Face Space
3
+ Supports:
4
+ - Demosthenes XML (.xml)
5
+ - Structured JSON with inline <prem>/<conc> tags (.json, format A)
6
+ - Pipeline result JSON with segment dicts (.json, format B)
7
  """
8
 
9
  import re
 
26
  nid = elem.get("ID")
27
  if not nid:
28
  continue
29
+ nodes[nid] = elem.tag.lower()
30
  for t in filter(None, elem.get("SUP", "").split("|")):
31
  sup_edges.append((nid, t.strip()))
32
  for t in filter(None, elem.get("ATT", "").split("|")):
 
34
  return nodes, sup_edges, att_edges
35
 
36
 
37
+ def parse_json_inline(filepath):
38
+ """Format A: sections with 'text' fields containing inline <prem>/<conc> XML tags."""
39
  with open(filepath, "r", encoding="utf-8") as f:
40
  data = json.load(f)
41
  nodes, sup_edges, att_edges = {}, [], []
 
49
  if not id_m:
50
  continue
51
  nid = id_m.group(1)
52
+ nodes[nid] = tag.lower()
53
  sup_m = re.search(r'SUP="([^"]+)"', attrs)
54
  if sup_m:
55
  for t in filter(None, sup_m.group(1).split("|")):
 
61
  return nodes, sup_edges, att_edges, data.get("document", "")
62
 
63
 
64
+ def _clean_id(raw_id):
65
+ """Convert '[3]' -> '3', strip brackets."""
66
+ return raw_id.strip().strip("[]").strip()
67
+
68
+
69
+ def parse_json_result(filepath):
70
+ """
71
+ Format B: pipeline result JSON.
72
+ Structure: { doc_name: { section_1: { segment1: {id, role, sup, att, text, ...} } } }
73
+ IDs restart per section, so we prefix them: S1_3, S2_1, etc.
74
+ """
75
+ with open(filepath, "r", encoding="utf-8") as f:
76
+ data = json.load(f)
77
+
78
+ doc_name = list(data.keys())[0]
79
+ doc = data[doc_name]
80
+
81
+ nodes, sup_edges, att_edges = {}, [], []
82
+
83
+ # Section index map: section_1 -> 1, section_2 -> 2, ...
84
+ sec_index = {}
85
+ for i, sec_k in enumerate(sorted(doc.keys(), key=lambda k: int(re.search(r'\d+', k).group())), 1):
86
+ sec_index[sec_k] = i
87
+
88
+ # First pass: register all nodes with prefixed IDs
89
+ # node_map[sec_k][raw_id] = prefixed_id
90
+ node_map = {}
91
+ for sec_k, sec in doc.items():
92
+ si = sec_index[sec_k]
93
+ node_map[sec_k] = {}
94
+ for seg in sec.values():
95
+ raw_id = _clean_id(seg.get("id", ""))
96
+ if not raw_id:
97
+ continue
98
+ role = seg.get("role", "PREM").upper()
99
+ ntype = "conc" if role == "CONC" else "prem"
100
+ pid = f"S{si}_{raw_id}"
101
+ node_map[sec_k][raw_id] = pid
102
+ # Store extra info for tooltip
103
+ nodes[pid] = {
104
+ "type": ntype,
105
+ "text": seg.get("text", "")[:120],
106
+ "scheme": seg.get("scheme", ""),
107
+ "atype": seg.get("type", ""),
108
+ }
109
+
110
+ # Second pass: build edges using prefixed IDs
111
+ for sec_k, sec in doc.items():
112
+ si = sec_index[sec_k]
113
+ nmap = node_map[sec_k]
114
+ for seg in sec.values():
115
+ raw_id = _clean_id(seg.get("id", ""))
116
+ if not raw_id or raw_id not in nmap:
117
+ continue
118
+ src_pid = nmap[raw_id]
119
+
120
+ sup_raw = seg.get("sup", "")
121
+ if sup_raw:
122
+ for t in sup_raw.split(","):
123
+ t = _clean_id(t)
124
+ if t and t in nmap:
125
+ sup_edges.append((src_pid, nmap[t]))
126
+
127
+ att_raw = seg.get("att", "")
128
+ if att_raw:
129
+ for t in att_raw.split(","):
130
+ t = _clean_id(t)
131
+ if t and t in nmap:
132
+ att_edges.append((src_pid, nmap[t]))
133
+
134
+ return nodes, sup_edges, att_edges, doc_name
135
+
136
+
137
+ def detect_and_parse(filepath):
138
+ """Auto-detect JSON format and dispatch to correct parser."""
139
+ with open(filepath, "r", encoding="utf-8") as f:
140
+ data = json.load(f)
141
+
142
+ # Format B: single top-level key whose value has section_N keys
143
+ # containing segment dicts with an 'id' field like '[1]'
144
+ if isinstance(data, dict) and len(data) == 1:
145
+ inner = list(data.values())[0]
146
+ if isinstance(inner, dict):
147
+ first_val = next(iter(inner.values()), None)
148
+ if isinstance(first_val, dict):
149
+ first_seg = next(iter(first_val.values()), None)
150
+ if isinstance(first_seg, dict) and "id" in first_seg:
151
+ nodes, sup_edges, att_edges, doc_name = parse_json_result(filepath)
152
+ # nodes here is dict of pid -> {type, text, ...}
153
+ # convert to simple type map for graph builder
154
+ node_types = {k: v["type"] for k, v in nodes.items()}
155
+ node_meta = nodes
156
+ return node_types, sup_edges, att_edges, doc_name, node_meta
157
+
158
+ # Format A: inline XML tags in text fields
159
+ nodes, sup_edges, att_edges, doc_name = parse_json_inline(filepath)
160
+ node_types = nodes
161
+ node_meta = {k: {"type": v, "text": "", "scheme": "", "atype": ""} for k, v in nodes.items()}
162
+ return node_types, sup_edges, att_edges, doc_name, node_meta
163
+
164
+
165
  # ── HTML builder ──────────────────────────────────────────────────────────────
166
 
167
+ def build_graph_html(node_types, sup_edges, att_edges, doc_name="", node_meta=None):
168
+ if node_meta is None:
169
+ node_meta = {}
170
+
171
+ nodes_js = json.dumps([
172
+ {
173
+ "id": k,
174
+ "type": v,
175
+ "text": node_meta.get(k, {}).get("text", ""),
176
+ "scheme": node_meta.get(k, {}).get("scheme", ""),
177
+ "atype": node_meta.get(k, {}).get("atype", ""),
178
+ }
179
+ for k, v in node_types.items()
180
+ ])
181
+ sup_js = json.dumps([{"source": s, "target": t} for s, t in sup_edges])
182
+ att_js = json.dumps([{"source": s, "target": t} for s, t in att_edges])
183
+ label = doc_name.replace("_", " ") if doc_name else "argumentation graph"
184
 
185
  return """<!DOCTYPE html>
186
  <html lang="en">
 
233
  .link-sup { stroke:var(--green); stroke-width:1.8; stroke-opacity:.75; fill:none; }
234
  .link-att { stroke:var(--red); stroke-width:2.2; stroke-opacity:.9;
235
  stroke-dasharray:7 3; fill:none; }
236
+ .ng { cursor:grab; } .ng:active { cursor:grabbing; }
 
237
  .cp { fill:var(--blue-bg); stroke:var(--blue-br); stroke-width:1.5; transition:fill .15s; }
238
  .cc { fill:var(--pur-bg); stroke:var(--pur-br); stroke-width:1.5; transition:fill .15s; }
239
  .ng:hover .cp { fill:var(--blue-br); }
 
242
  text-anchor:middle; dominant-baseline:central; pointer-events:none; }
243
  #tt {
244
  position:fixed; display:none; background:#1c2030;
245
+ border:1px solid var(--border); border-radius:7px; padding:10px 14px;
246
+ font-family:'IBM Plex Mono',monospace; font-size:11px; line-height:1.8;
247
+ pointer-events:none; z-index:999; max-width:300px; color:var(--text);
248
+ box-shadow:0 8px 32px rgba(0,0,0,.6);
249
  }
250
  </style>
251
  </head>
 
288
  svg.call(d3.zoom().scaleExtent([0.1, 5]).on("zoom", e => g.attr("transform", e.transform)));
289
 
290
  const sim = d3.forceSimulation(NODES)
291
+ .force("link", d3.forceLink(links).id(d => d.id).distance(90).strength(0.5))
292
+ .force("charge", d3.forceManyBody().strength(-360))
293
  .force("center", d3.forceCenter(W / 2, H / 2))
294
+ .force("collision", d3.forceCollide(22));
295
 
296
  const lsel = g.append("g").selectAll("line").data(links).join("line")
297
  .attr("class", d => d.kind === "sup" ? "link-sup" : "link-att")
298
  .attr("marker-end", d => d.kind === "sup" ? "url(#ms)" : "url(#ma)");
299
 
 
300
  const adj = {};
301
  NODES.forEach(n => { adj[n.id] = {sf:[], st:[], af:[], at:[]}; });
302
  SUP.forEach(e => { adj[e.target].sf.push(e.source); adj[e.source].st.push(e.target); });
 
312
  .on("end", (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }))
313
  .on("mouseover", (e, d) => {
314
  const a = adj[d.id];
315
+ let h = "<b style='font-size:13px'>" + d.id + "</b>";
316
+ h += " <span style='color:#64748b'>" + d.type + "</span>";
317
+ if (d.scheme) h += " <span style='color:#94a3b8'>· " + d.scheme + "</span>";
318
+ if (d.atype) h += " <span style='color:#94a3b8'>· type:" + d.atype + "</span>";
319
+ h += "<hr style='border:none;border-top:1px solid #2a2f42;margin:5px 0'>";
320
+ if (d.text) h += "<div style='color:#94a3b8;font-size:10px;margin-bottom:5px;line-height:1.5'>" + d.text + (d.text.length >= 120 ? "…" : "") + "</div>";
321
  if (a.sf.length) h += "<div style='color:#22c55e'>◀ SUP from: " + a.sf.join(", ") + "</div>";
322
  if (a.st.length) h += "<div style='color:#22c55e'>▶ SUP to: " + a.st.join(", ") + "</div>";
323
  if (a.af.length) h += "<div style='color:#ef4444'>◀ ATT from: " + a.af.join(", ") + "</div>";
 
330
  .on("mousemove", e => { tt.style.left = (e.clientX + 14) + "px"; tt.style.top = (e.clientY - 12) + "px"; })
331
  .on("mouseleave", () => { tt.style.display = "none"; });
332
 
333
+ nsel.append("circle").attr("r", 14).attr("class", d => d.type === "prem" ? "cp" : "cc");
334
  nsel.append("text").attr("class", "nl")
335
+ .style("font-size", d => d.id.length > 5 ? "6.5px" : "8px")
336
  .text(d => d.id);
337
 
338
  sim.on("tick", () => {
 
345
  </html>"""
346
 
347
 
348
+ # ── Gradio handler ────────────────────────────────────────────────────────────
 
 
 
349
 
350
  def process_file(file_obj):
351
  if file_obj is None:
 
357
  try:
358
  if ext == "xml":
359
  nodes, sup_edges, att_edges = parse_xml(path)
360
+ doc_name = os.path.basename(path).replace(".xml", "").replace("_", " ")
361
+ node_meta = {k: {"type": v, "text": "", "scheme": "", "atype": ""} for k, v in nodes.items()}
362
+ node_types = nodes
363
  elif ext == "json":
364
+ node_types, sup_edges, att_edges, doc_name, node_meta = detect_and_parse(path)
365
  else:
366
  return None, "<p style='color:#ef4444;font-family:monospace;padding:20px'>Upload .xml or .json</p>"
367
  except Exception as ex:
368
+ import traceback
369
+ return None, f"<pre style='color:#ef4444;font-family:monospace;padding:20px'>{traceback.format_exc()}</pre>"
370
 
371
+ if not node_types:
372
+ return None, "<p style='color:#ef4444;font-family:monospace;padding:20px'>No nodes found in file.</p>"
373
 
374
+ html = build_graph_html(node_types, sup_edges, att_edges, doc_name, node_meta)
375
 
 
376
  tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8")
377
  tmp.write(html)
378
  tmp.close()
379
 
380
+ srcdoc = html.replace("&", "&amp;").replace('"', "&quot;")
 
381
  iframe = f'<iframe srcdoc="{srcdoc}" style="width:100%;height:680px;border:none;border-radius:8px;"></iframe>'
382
 
383
  return tmp.name, iframe
 
411
  with gr.Row(elem_id="title-row"):
412
  gr.HTML("""
413
  <h1>⚖ Argumentation Graph Visualizer</h1>
414
+ <p>Upload a Demosthenes <b>.xml</b> or pipeline result <b>.json</b>
415
  → interactive force-directed argument graph.</p>
416
  """)
417
 
 
419
  with gr.Column(scale=1, min_width=260):
420
  file_input = gr.File(label="Upload XML or JSON", file_types=[".xml", ".json"])
421
  run_btn = gr.Button("Build Graph", variant="primary", size="lg")
422
+ dl_out = gr.File(label="Download graph HTML")
423
  gr.HTML("""
424
  <div style="font-family:monospace;font-size:11px;color:#64748b;
425
  border:1px solid #2a2f42;border-radius:8px;padding:12px;margin-top:6px;">
 
447
  <circle cx="6" cy="6" r="5" fill="#4c1d95" stroke="#a855f7" stroke-width="1.5"/>
448
  </svg> conc (conclusion)
449
  </div>
450
+ <div style="margin-top:8px;color:#475569;line-height:1.6;">
451
+ Accepts .xml · .json (inline tags) · .json (pipeline result)<br><br>
452
+ Hover node �� see text snippet, type, scheme, relations
453
  </div>
454
  </div>
455
  """)