Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -2733,95 +2733,114 @@ def expand_paren_lists(sent: str, lists: list[list[str]]) -> list[str]:
|
|
| 2733 |
|
| 2734 |
###############################################################################
|
| 2735 |
def build_shared_dag(sentences):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2736 |
cache = {}
|
| 2737 |
labels = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2738 |
edges = set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2739 |
|
| 2740 |
-
def node_for(words):
|
| 2741 |
-
key = tuple(words)
|
| 2742 |
-
if key in cache:
|
| 2743 |
-
return cache[key]
|
| 2744 |
-
nid = f"n{len(cache)}"
|
| 2745 |
-
cache[key] = nid
|
| 2746 |
-
labels[nid] = " ".join(words) if words else ""
|
| 2747 |
-
return nid
|
| 2748 |
-
|
| 2749 |
-
root = node_for([])
|
| 2750 |
-
for sent in sentences:
|
| 2751 |
-
words = sent.rstrip(".").split()
|
| 2752 |
-
prev = root
|
| 2753 |
-
for i in range(len(words)):
|
| 2754 |
-
nxt = node_for(words[i:])
|
| 2755 |
-
edges.add((prev, nxt))
|
| 2756 |
-
prev = nxt
|
| 2757 |
return labels, edges
|
| 2758 |
###############################################################################
|
| 2759 |
|
| 2760 |
###############################################################################
|
| 2761 |
-
def dag_to_svg(sentences, width=
|
| 2762 |
labels, edges = build_shared_dag(sentences)
|
| 2763 |
-
|
| 2764 |
-
|
| 2765 |
-
# simple layered layout by first appearance
|
| 2766 |
-
order = []
|
| 2767 |
-
seen = set()
|
| 2768 |
-
for sent in sentences:
|
| 2769 |
-
words = sent.rstrip(".").split()
|
| 2770 |
-
for i in range(len(words)):
|
| 2771 |
-
key = tuple(words[i:])
|
| 2772 |
-
# find nid
|
| 2773 |
-
for nid, lab in labels.items():
|
| 2774 |
-
if lab == " ".join(key) and nid not in seen:
|
| 2775 |
-
order.append(nid)
|
| 2776 |
-
seen.add(nid)
|
| 2777 |
-
break
|
| 2778 |
-
for nid in nodes:
|
| 2779 |
-
if nid not in seen:
|
| 2780 |
-
order.append(nid)
|
| 2781 |
|
| 2782 |
-
#
|
| 2783 |
-
|
|
|
|
|
|
|
| 2784 |
for nid in order:
|
| 2785 |
-
|
| 2786 |
-
rows.setdefault(length, []).append(nid)
|
| 2787 |
lengths = sorted(rows.keys(), reverse=True)
|
| 2788 |
|
| 2789 |
positions = {}
|
| 2790 |
-
y =
|
|
|
|
| 2791 |
for length in lengths:
|
| 2792 |
row = rows[length]
|
| 2793 |
n = len(row)
|
|
|
|
| 2794 |
for i, nid in enumerate(row):
|
| 2795 |
-
x =
|
| 2796 |
-
positions[nid] = (x, y)
|
| 2797 |
-
|
|
|
|
| 2798 |
|
| 2799 |
-
# SVG
|
| 2800 |
parts = [
|
| 2801 |
-
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width
|
| 2802 |
-
f'style="font-family:Helvetica,Arial,sans-serif;font-size:13px;">'
|
|
|
|
|
|
|
| 2803 |
]
|
| 2804 |
-
# edges
|
| 2805 |
for a, b in edges:
|
| 2806 |
if a not in positions or b not in positions:
|
| 2807 |
continue
|
| 2808 |
-
x1, y1 = positions[a]
|
| 2809 |
-
x2, y2 = positions[b]
|
| 2810 |
parts.append(
|
| 2811 |
-
f'<line x1="{x1+
|
| 2812 |
f'stroke="#888" stroke-width="1.2" marker-end="url(#arrow)"/>'
|
| 2813 |
)
|
| 2814 |
-
|
| 2815 |
-
'<defs><marker id="arrow" markerWidth="8" markerHeight="8" '
|
| 2816 |
-
'refX="6" refY="3" orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#888"/></marker></defs>'
|
| 2817 |
-
)
|
| 2818 |
-
# nodes
|
| 2819 |
-
for nid, (x, y) in positions.items():
|
| 2820 |
lab = labels[nid].replace("&", "&").replace("<", "<")
|
| 2821 |
parts.append(
|
| 2822 |
-
f'<rect x="{x}" y="{y}" width="
|
| 2823 |
f'fill="#f7f7f7" stroke="#555"/>'
|
| 2824 |
-
f'<text x="{x+
|
| 2825 |
)
|
| 2826 |
parts.append("</svg>")
|
| 2827 |
return "".join(parts)
|
|
|
|
| 2733 |
|
| 2734 |
###############################################################################
|
| 2735 |
def build_shared_dag(sentences):
|
| 2736 |
+
sents = [tuple(s.rstrip(".").split()) for s in sentences if s.strip()]
|
| 2737 |
+
if not sents:
|
| 2738 |
+
return {}, set()
|
| 2739 |
+
|
| 2740 |
+
# count every suffix
|
| 2741 |
+
suffix_count = defaultdict(int)
|
| 2742 |
+
for sent in sents:
|
| 2743 |
+
for i in range(len(sent)):
|
| 2744 |
+
suffix_count[sent[i:]] += 1
|
| 2745 |
+
|
| 2746 |
+
# keep full sentences, and any suffix that is truly shared
|
| 2747 |
+
keep = set()
|
| 2748 |
+
for sent in sents:
|
| 2749 |
+
keep.add(sent)
|
| 2750 |
+
for i in range(1, len(sent)):
|
| 2751 |
+
if suffix_count[sent[i:]] > 1:
|
| 2752 |
+
keep.add(sent[i:])
|
| 2753 |
+
|
| 2754 |
+
# also keep shared prefixes (length >= 2, count >= 2)
|
| 2755 |
+
prefix_count = defaultdict(int)
|
| 2756 |
+
for sent in sents:
|
| 2757 |
+
for i in range(2, len(sent) + 1):
|
| 2758 |
+
prefix_count[sent[:i]] += 1
|
| 2759 |
+
for p, c in prefix_count.items():
|
| 2760 |
+
if c >= 2:
|
| 2761 |
+
keep.add(p)
|
| 2762 |
+
|
| 2763 |
cache = {}
|
| 2764 |
labels = {}
|
| 2765 |
+
def node_for(words):
|
| 2766 |
+
if words not in cache:
|
| 2767 |
+
nid = f"n{len(cache)}"
|
| 2768 |
+
cache[words] = nid
|
| 2769 |
+
labels[nid] = " ".join(words)
|
| 2770 |
+
return cache[words]
|
| 2771 |
+
|
| 2772 |
edges = set()
|
| 2773 |
+
for sent in sents:
|
| 2774 |
+
# walk the sentence, emitting an edge only when we hit a kept span
|
| 2775 |
+
prev = None
|
| 2776 |
+
for i in range(len(sent)):
|
| 2777 |
+
span = sent[i:]
|
| 2778 |
+
if span in keep:
|
| 2779 |
+
nid = node_for(span)
|
| 2780 |
+
if prev is not None:
|
| 2781 |
+
edges.add((prev, nid))
|
| 2782 |
+
prev = nid
|
| 2783 |
+
# also connect shared prefixes into the first kept continuation
|
| 2784 |
+
for i in range(2, len(sent)):
|
| 2785 |
+
pref = sent[:i]
|
| 2786 |
+
if pref in keep:
|
| 2787 |
+
# find the next kept span after this prefix
|
| 2788 |
+
for j in range(i, len(sent)):
|
| 2789 |
+
if sent[j:] in keep:
|
| 2790 |
+
edges.add((node_for(pref), node_for(sent[j:])))
|
| 2791 |
+
break
|
| 2792 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2793 |
return labels, edges
|
| 2794 |
###############################################################################
|
| 2795 |
|
| 2796 |
###############################################################################
|
| 2797 |
+
def dag_to_svg(sentences, width=1000):
|
| 2798 |
labels, edges = build_shared_dag(sentences)
|
| 2799 |
+
if not labels:
|
| 2800 |
+
return "<p>No sentences.</p>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2801 |
|
| 2802 |
+
# order nodes by descending length so shared tails sit lower
|
| 2803 |
+
order = sorted(labels.keys(), key=lambda n: -len(labels[n].split()))
|
| 2804 |
+
# group into rows by word count
|
| 2805 |
+
rows = defaultdict(list)
|
| 2806 |
for nid in order:
|
| 2807 |
+
rows[len(labels[nid].split())].append(nid)
|
|
|
|
| 2808 |
lengths = sorted(rows.keys(), reverse=True)
|
| 2809 |
|
| 2810 |
positions = {}
|
| 2811 |
+
y = 30
|
| 2812 |
+
max_w = 0
|
| 2813 |
for length in lengths:
|
| 2814 |
row = rows[length]
|
| 2815 |
n = len(row)
|
| 2816 |
+
box_w = min(280, max(140, width // max(n, 1) - 20))
|
| 2817 |
for i, nid in enumerate(row):
|
| 2818 |
+
x = 20 + i * (box_w + 30)
|
| 2819 |
+
positions[nid] = (x, y, box_w)
|
| 2820 |
+
max_w = max(max_w, x + box_w)
|
| 2821 |
+
y += 70
|
| 2822 |
|
|
|
|
| 2823 |
parts = [
|
| 2824 |
+
f'<svg xmlns="http://www.w3.org/2000/svg" width="{max(width, max_w + 40)}" '
|
| 2825 |
+
f'height="{y + 20}" style="font-family:Helvetica,Arial,sans-serif;font-size:13px;">',
|
| 2826 |
+
'<defs><marker id="arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" '
|
| 2827 |
+
'orient="auto"><path d="M0,0 L0,6 L8,3 z" fill="#888"/></marker></defs>',
|
| 2828 |
]
|
|
|
|
| 2829 |
for a, b in edges:
|
| 2830 |
if a not in positions or b not in positions:
|
| 2831 |
continue
|
| 2832 |
+
x1, y1, w1 = positions[a]
|
| 2833 |
+
x2, y2, w2 = positions[b]
|
| 2834 |
parts.append(
|
| 2835 |
+
f'<line x1="{x1 + w1/2}" y1="{y1 + 32}" x2="{x2 + w2/2}" y2="{y2}" '
|
| 2836 |
f'stroke="#888" stroke-width="1.2" marker-end="url(#arrow)"/>'
|
| 2837 |
)
|
| 2838 |
+
for nid, (x, y, w) in positions.items():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2839 |
lab = labels[nid].replace("&", "&").replace("<", "<")
|
| 2840 |
parts.append(
|
| 2841 |
+
f'<rect x="{x}" y="{y}" width="{w}" height="32" rx="5" '
|
| 2842 |
f'fill="#f7f7f7" stroke="#555"/>'
|
| 2843 |
+
f'<text x="{x + w/2}" y="{y + 20}" text-anchor="middle">{lab}</text>'
|
| 2844 |
)
|
| 2845 |
parts.append("</svg>")
|
| 2846 |
return "".join(parts)
|