StickyText 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- StickyText/BuildGraph.py +144 -0
- StickyText/CoRef.py +42 -0
- StickyText/DepParse.py +30 -0
- StickyText/EdgeMerge.py +336 -0
- StickyText/Graph.py +44 -0
- StickyText/MergeCoref.py +34 -0
- StickyText/MergeDF.py +46 -0
- StickyText/ModelManager.py +34 -0
- StickyText/NER.py +41 -0
- StickyText/NodeAttributes.py +17 -0
- StickyText/NodeMerge.py +87 -0
- StickyText/NounPhrases.py +26 -0
- StickyText/NounPhrasesAppend.py +28 -0
- StickyText/PhrasalVerbs.py +58 -0
- StickyText/Pipeline.py +48 -0
- StickyText/SpacyProcess.py +17 -0
- StickyText/TextTokenizer.py +21 -0
- StickyText/__init__.py +3 -0
- StickyText/load_models.py +24 -0
- StickyText/model_config.py +14 -0
- stickytext-0.1.1.dist-info/METADATA +70 -0
- stickytext-0.1.1.dist-info/RECORD +25 -0
- stickytext-0.1.1.dist-info/WHEEL +5 -0
- stickytext-0.1.1.dist-info/licenses/Licence.md +89 -0
- stickytext-0.1.1.dist-info/top_level.txt +2 -0
StickyText/BuildGraph.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import networkx as nx
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import itertools
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_L2_graph(token_df, merge_id, merge_text, merge_head, G=None):
|
|
7
|
+
"""
|
|
8
|
+
Build a directed dependency graph from a token DataFrame.
|
|
9
|
+
|
|
10
|
+
Required columns:
|
|
11
|
+
token_id, text, dep, head, pos, noun_phrase, ner,
|
|
12
|
+
coref, representative text
|
|
13
|
+
|
|
14
|
+
Edge direction:
|
|
15
|
+
head -> dependent
|
|
16
|
+
"""
|
|
17
|
+
if not G:
|
|
18
|
+
G = nx.DiGraph()
|
|
19
|
+
|
|
20
|
+
# Add every token as a node
|
|
21
|
+
for _, row in token_df.iterrows():
|
|
22
|
+
token_id = row[merge_id]
|
|
23
|
+
|
|
24
|
+
G.add_node(
|
|
25
|
+
token_id,
|
|
26
|
+
text=row[merge_text],
|
|
27
|
+
attributes = row.get("attributes")
|
|
28
|
+
#dep=row["dep"],
|
|
29
|
+
#pos=row["pos"],
|
|
30
|
+
#noun_phrase=row["noun_phrase"],
|
|
31
|
+
#ner= row["ner"],
|
|
32
|
+
#coref=row["coref"],
|
|
33
|
+
#representative_text=row["representative_text"],
|
|
34
|
+
)
|
|
35
|
+
# Add dependency edges
|
|
36
|
+
|
|
37
|
+
for _, row in token_df.iterrows():
|
|
38
|
+
token_id = row[merge_id]
|
|
39
|
+
heads = row[merge_head]
|
|
40
|
+
|
|
41
|
+
# Handle missing heads
|
|
42
|
+
if heads is None:
|
|
43
|
+
continue
|
|
44
|
+
for head in heads:
|
|
45
|
+
try:
|
|
46
|
+
if head != token_id and head in G:
|
|
47
|
+
G.add_edge(
|
|
48
|
+
head,
|
|
49
|
+
token_id,
|
|
50
|
+
#dep=row["dep"]
|
|
51
|
+
)
|
|
52
|
+
except TypeError:
|
|
53
|
+
# Handles NaN / non-hashable values
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
return G
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def visualize_dependency_graph(G, figsize=(16, 10)):
|
|
60
|
+
"""
|
|
61
|
+
Visualize a dependency graph.
|
|
62
|
+
|
|
63
|
+
Node label:
|
|
64
|
+
token_id: text
|
|
65
|
+
|
|
66
|
+
Edge label:
|
|
67
|
+
dependency relation
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
plt.figure(figsize=figsize)
|
|
71
|
+
|
|
72
|
+
# Hierarchical-ish layout
|
|
73
|
+
pos = nx.spring_layout(
|
|
74
|
+
G,
|
|
75
|
+
seed=42,
|
|
76
|
+
k=2.0,
|
|
77
|
+
iterations=100
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Node labels
|
|
81
|
+
node_labels = {
|
|
82
|
+
node: f"{data['text']}:{', '.join(data.get('attributes'))}" if data.get('attributes') is not None else f"{data['text']}"
|
|
83
|
+
for node, data in G.nodes(data=True)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Draw nodes
|
|
87
|
+
nx.draw_networkx_nodes(
|
|
88
|
+
G,
|
|
89
|
+
pos,
|
|
90
|
+
node_color="lightblue",
|
|
91
|
+
node_size=2000,
|
|
92
|
+
edgecolors="black",
|
|
93
|
+
linewidths=1
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Draw edges
|
|
97
|
+
nx.draw_networkx_edges(
|
|
98
|
+
G,
|
|
99
|
+
pos,
|
|
100
|
+
arrows=True,
|
|
101
|
+
arrowsize=20,
|
|
102
|
+
arrowstyle="-|>",
|
|
103
|
+
edge_color="gray",
|
|
104
|
+
width=1.5,
|
|
105
|
+
connectionstyle="arc3,rad=0.05"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Draw node labels
|
|
109
|
+
nx.draw_networkx_labels(
|
|
110
|
+
G,
|
|
111
|
+
pos,
|
|
112
|
+
labels=node_labels,
|
|
113
|
+
font_size=9
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Dependency labels
|
|
117
|
+
edge_labels = nx.get_edge_attributes(G, "dep")
|
|
118
|
+
|
|
119
|
+
nx.draw_networkx_edge_labels(
|
|
120
|
+
G,
|
|
121
|
+
pos,
|
|
122
|
+
edge_labels=edge_labels,
|
|
123
|
+
font_size=8,
|
|
124
|
+
label_pos=0.5
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
plt.title("Dependency Graph")
|
|
128
|
+
plt.axis("off")
|
|
129
|
+
plt.tight_layout()
|
|
130
|
+
plt.show()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def subside_nodes(G, node_ids):
|
|
134
|
+
def remove_forward(node):
|
|
135
|
+
pred = list(G.predecessors(node))
|
|
136
|
+
succ = list(G.successors(node))
|
|
137
|
+
conn_nodes = pred + succ
|
|
138
|
+
G.add_edges_from((p, s) for p in conn_nodes for s in conn_nodes if p != s and not G.has_edge(s, p))
|
|
139
|
+
G.add_edges_from((u, v) for u, v in itertools.combinations(conn_nodes, 2)
|
|
140
|
+
)
|
|
141
|
+
G.remove_node(node)
|
|
142
|
+
list(map(remove_forward, node_ids))
|
|
143
|
+
|
|
144
|
+
return G
|
StickyText/CoRef.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
from .ModelManager import ModelManager
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
class CoRef:
|
|
6
|
+
def __init__(self, model_manager: ModelManager, model_name, text:str, doc_id, chunk_id):
|
|
7
|
+
self.text = text
|
|
8
|
+
self.pipe = model_manager.load(model_name)
|
|
9
|
+
self.doc_id = doc_id ; self.chunk_id = chunk_id
|
|
10
|
+
|
|
11
|
+
def process_text(self):
|
|
12
|
+
self.res = self.pipe(self.text)
|
|
13
|
+
d = self.res.__dict__
|
|
14
|
+
|
|
15
|
+
coref_df = pd.DataFrame(
|
|
16
|
+
{
|
|
17
|
+
"coref": chain_id,
|
|
18
|
+
"representative_text": str(chain.representative_text).strip().lower(),
|
|
19
|
+
"is_representative": (
|
|
20
|
+
m == chain.mentions[0]
|
|
21
|
+
),
|
|
22
|
+
"sentence_id": m.sentence,
|
|
23
|
+
"start_word": m.start_word,
|
|
24
|
+
"end_word": m.end_word,
|
|
25
|
+
"start_char": d["_sentences"][m.sentence].words[m.start_word:m.end_word][0].start_char,
|
|
26
|
+
"end_char": d["_sentences"][m.sentence].words[m.start_word:m.end_word][-1].end_char,
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
for chain_id, chain in enumerate(d["_coref"])
|
|
31
|
+
for m in chain.mentions
|
|
32
|
+
)
|
|
33
|
+
# coref_df['doc_id'] = self.doc_id
|
|
34
|
+
return coref_df
|
|
35
|
+
|
|
36
|
+
class NER:
|
|
37
|
+
def __init__(self, coref_stanza):
|
|
38
|
+
self.coref_stanza = coref_stanza
|
|
39
|
+
def process_text(self):
|
|
40
|
+
self.coref_stanza.res
|
|
41
|
+
|
|
42
|
+
|
StickyText/DepParse.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is abstract class with methods initialize or load model , process text , process batch
|
|
3
|
+
"""
|
|
4
|
+
from .ModelManager import ModelManager
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
class DependencyParser:
|
|
8
|
+
def __init__(self, model_manager: ModelManager, text:str):
|
|
9
|
+
self.model_manager = model_manager
|
|
10
|
+
self.text = text
|
|
11
|
+
|
|
12
|
+
def process_text(self):
|
|
13
|
+
"""Returns DataFrame or JSON records with columns token_text (lower),
|
|
14
|
+
sentence_num, token_num, pos, dep
|
|
15
|
+
"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class SpacyDepParser(DependencyParser):
|
|
19
|
+
def __init__(self, model_manager, spacy_model_name, text):
|
|
20
|
+
super().__init__(model_manager, text)
|
|
21
|
+
self.model_name = spacy_model_name
|
|
22
|
+
self.model_manager.load(self.model_name)
|
|
23
|
+
def process_text(self):
|
|
24
|
+
self.res = self.model_manager.models[self.model_name](self.text)
|
|
25
|
+
df = pd.DataFrame(self.res.to_json()["tokens"])[['start','end',"head","pos", "dep"]]
|
|
26
|
+
return df
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
StickyText/EdgeMerge.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
class EdgeMerge:
|
|
4
|
+
def __init__(self, token_df):
|
|
5
|
+
self.token_df = token_df
|
|
6
|
+
|
|
7
|
+
def head_to_coref(self, coref_nodes_df, token_id_col="token_id",
|
|
8
|
+
coref_token_ids_col ="coref_token_ids", coref_id_col="coref_id",
|
|
9
|
+
coref_head_col="coref_head", token_head_col="head"):
|
|
10
|
+
# token_id -> coref_id
|
|
11
|
+
token_df = self.token_df.copy()
|
|
12
|
+
coref_nodes_df = coref_nodes_df.copy()
|
|
13
|
+
|
|
14
|
+
# token_id -> coref_id
|
|
15
|
+
token_to_coref = (
|
|
16
|
+
coref_nodes_df
|
|
17
|
+
.explode(coref_token_ids_col)
|
|
18
|
+
.rename(columns={coref_token_ids_col: token_id_col})
|
|
19
|
+
[[token_id_col, coref_id_col]]
|
|
20
|
+
.drop_duplicates(token_id_col)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
coref_map = token_to_coref.set_index(token_id_col)[coref_id_col]
|
|
24
|
+
|
|
25
|
+
# --------------------------------------------------
|
|
26
|
+
# token_df
|
|
27
|
+
# --------------------------------------------------
|
|
28
|
+
token_df[coref_head_col] = (
|
|
29
|
+
token_df[token_head_col]
|
|
30
|
+
.map(coref_map)
|
|
31
|
+
.fillna(token_df[token_head_col])
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# --------------------------------------------------
|
|
35
|
+
# coref_nodes_df
|
|
36
|
+
# --------------------------------------------------
|
|
37
|
+
token_to_head = token_df.set_index(token_id_col)[token_head_col]
|
|
38
|
+
|
|
39
|
+
def get_external_heads(token_ids):
|
|
40
|
+
external_heads = []
|
|
41
|
+
|
|
42
|
+
# Get the coref_id of this group
|
|
43
|
+
current_coref_id = coref_map.get(token_ids[0])
|
|
44
|
+
|
|
45
|
+
for token_id in token_ids:
|
|
46
|
+
head = token_to_head.get(token_id)
|
|
47
|
+
|
|
48
|
+
if head is None:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
# If head is not part of the same coref group,
|
|
52
|
+
# it is an external head.
|
|
53
|
+
head_coref_id = coref_map.get(head)
|
|
54
|
+
|
|
55
|
+
if head_coref_id != current_coref_id:
|
|
56
|
+
external_heads.append(head)
|
|
57
|
+
|
|
58
|
+
# Remove duplicates while preserving order
|
|
59
|
+
return list(dict.fromkeys(external_heads))
|
|
60
|
+
|
|
61
|
+
coref_nodes_df[coref_head_col] = (
|
|
62
|
+
coref_nodes_df[coref_token_ids_col]
|
|
63
|
+
.apply(get_external_heads)
|
|
64
|
+
)
|
|
65
|
+
token_df = token_df[
|
|
66
|
+
~token_df[coref_id_col].isin(coref_nodes_df[coref_id_col])
|
|
67
|
+
].copy()
|
|
68
|
+
return token_df, coref_nodes_df
|
|
69
|
+
|
|
70
|
+
def head_to_verb(self, verb_df, head_col="coref_head"):
|
|
71
|
+
token_df = self.token_df.copy()
|
|
72
|
+
verb_df = verb_df.copy()
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------
|
|
75
|
+
# token_id -> verb_id
|
|
76
|
+
# --------------------------------------------------
|
|
77
|
+
token_to_verb = (
|
|
78
|
+
verb_df
|
|
79
|
+
.explode("verb_token_ids")
|
|
80
|
+
.rename(columns={"verb_token_ids": "token_id"})
|
|
81
|
+
[["token_id", "verb_id"]]
|
|
82
|
+
.drop_duplicates("token_id")
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
verb_map = token_to_verb.set_index("token_id")["verb_id"]
|
|
86
|
+
|
|
87
|
+
# --------------------------------------------------
|
|
88
|
+
# token_df
|
|
89
|
+
# --------------------------------------------------
|
|
90
|
+
token_df["verb_head"] = (
|
|
91
|
+
token_df[head_col]
|
|
92
|
+
.map(verb_map)
|
|
93
|
+
.fillna(token_df[head_col])
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# --------------------------------------------------
|
|
97
|
+
# verb_df
|
|
98
|
+
# --------------------------------------------------
|
|
99
|
+
token_to_head = token_df.set_index("token_id")[head_col]
|
|
100
|
+
|
|
101
|
+
def get_external_heads(token_ids):
|
|
102
|
+
external_heads = []
|
|
103
|
+
|
|
104
|
+
current_verb_id = verb_map.get(token_ids[0])
|
|
105
|
+
|
|
106
|
+
for token_id in token_ids:
|
|
107
|
+
head = token_to_head.get(token_id)
|
|
108
|
+
|
|
109
|
+
if head is None:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
head_verb_id = verb_map.get(head)
|
|
113
|
+
|
|
114
|
+
if head_verb_id != current_verb_id:
|
|
115
|
+
external_heads.append(head)
|
|
116
|
+
|
|
117
|
+
return list(dict.fromkeys(external_heads))
|
|
118
|
+
|
|
119
|
+
verb_df["verb_head"] = (
|
|
120
|
+
verb_df["verb_token_ids"]
|
|
121
|
+
.apply(get_external_heads)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Remove tokens belonging to verb groups
|
|
125
|
+
token_df = token_df[
|
|
126
|
+
~token_df["token_id"].isin(token_to_verb["token_id"])
|
|
127
|
+
].copy()
|
|
128
|
+
|
|
129
|
+
return token_df, verb_df
|
|
130
|
+
|
|
131
|
+
def head_to_merge(self, merge_df, head_col="head", merge_id_col='merge_id', merge_token_ids_col='merge_token_ids',
|
|
132
|
+
merge_head_col='merge_head', new_tokens='token_id'):
|
|
133
|
+
token_df = self.token_df.copy()
|
|
134
|
+
merge_df = merge_df.copy()
|
|
135
|
+
|
|
136
|
+
# --------------------------------------------------
|
|
137
|
+
# token_id -> verb_id
|
|
138
|
+
# --------------------------------------------------
|
|
139
|
+
token_to_verb = (
|
|
140
|
+
merge_df
|
|
141
|
+
.explode(merge_token_ids_col)
|
|
142
|
+
.rename(columns={merge_token_ids_col: 'token_id'})
|
|
143
|
+
[['token_id', merge_id_col]]
|
|
144
|
+
.drop_duplicates('token_id')
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
verb_map = token_to_verb.set_index('token_id')[merge_id_col]
|
|
148
|
+
|
|
149
|
+
# --------------------------------------------------
|
|
150
|
+
# token_df
|
|
151
|
+
# --------------------------------------------------
|
|
152
|
+
token_df[merge_head_col] = (
|
|
153
|
+
token_df[head_col]
|
|
154
|
+
.map(verb_map)
|
|
155
|
+
.fillna(token_df[head_col])
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# --------------------------------------------------
|
|
159
|
+
# verb_df
|
|
160
|
+
# --------------------------------------------------
|
|
161
|
+
token_to_head = token_df.set_index("token_id")[head_col]
|
|
162
|
+
|
|
163
|
+
def get_external_heads(token_ids):
|
|
164
|
+
external_heads = []
|
|
165
|
+
|
|
166
|
+
current_verb_id = verb_map.get(token_ids[0])
|
|
167
|
+
|
|
168
|
+
for token_id in token_ids:
|
|
169
|
+
head = token_to_head.get(token_id)
|
|
170
|
+
|
|
171
|
+
if head is None:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
head_verb_id = verb_map.get(head)
|
|
175
|
+
|
|
176
|
+
if head_verb_id != current_verb_id:
|
|
177
|
+
external_heads.append(head)
|
|
178
|
+
|
|
179
|
+
return list(dict.fromkeys(external_heads))
|
|
180
|
+
|
|
181
|
+
merge_df[merge_head_col] = (
|
|
182
|
+
merge_df[merge_token_ids_col]
|
|
183
|
+
.apply(get_external_heads)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Remove tokens belonging to verb groups
|
|
187
|
+
token_df = token_df[
|
|
188
|
+
~token_df["token_id"].isin(token_to_verb["token_id"])
|
|
189
|
+
].copy()
|
|
190
|
+
|
|
191
|
+
return token_df, merge_df
|
|
192
|
+
|
|
193
|
+
def head_to_merge(
|
|
194
|
+
self,
|
|
195
|
+
merge_df,
|
|
196
|
+
head_col="head",
|
|
197
|
+
merge_id_col="merge_id",
|
|
198
|
+
merge_token_ids_col="merge_token_ids",
|
|
199
|
+
merge_head_col="merge_head",
|
|
200
|
+
new_tokens="token_id",
|
|
201
|
+
):
|
|
202
|
+
token_df = self.token_df.copy()
|
|
203
|
+
merge_df = merge_df.copy()
|
|
204
|
+
|
|
205
|
+
# --------------------------------------------------
|
|
206
|
+
# 1. token_id -> merge_id
|
|
207
|
+
# --------------------------------------------------
|
|
208
|
+
token_to_merge = (
|
|
209
|
+
merge_df[[merge_id_col, merge_token_ids_col]]
|
|
210
|
+
.explode(merge_token_ids_col)
|
|
211
|
+
.rename(columns={merge_token_ids_col: "token_id"})
|
|
212
|
+
.drop_duplicates("token_id")
|
|
213
|
+
.set_index("token_id")[merge_id_col]
|
|
214
|
+
.to_dict()
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# --------------------------------------------------
|
|
218
|
+
# 2. Resolve a token/head to its merged node
|
|
219
|
+
#
|
|
220
|
+
# If token belongs to a merge, return merge_id.
|
|
221
|
+
# Otherwise return original token_id.
|
|
222
|
+
# --------------------------------------------------
|
|
223
|
+
def resolve_node(token_id):
|
|
224
|
+
if token_id in token_to_merge:
|
|
225
|
+
return token_to_merge[token_id]
|
|
226
|
+
return token_id
|
|
227
|
+
|
|
228
|
+
# --------------------------------------------------
|
|
229
|
+
# 3. Build merged edges
|
|
230
|
+
#
|
|
231
|
+
# For every token in every merge:
|
|
232
|
+
#
|
|
233
|
+
# token -> head
|
|
234
|
+
#
|
|
235
|
+
# becomes:
|
|
236
|
+
#
|
|
237
|
+
# merge_id -> resolved(head)
|
|
238
|
+
#
|
|
239
|
+
# We only keep heads that are outside the same merge.
|
|
240
|
+
# --------------------------------------------------
|
|
241
|
+
merge_heads = {}
|
|
242
|
+
|
|
243
|
+
token_to_head = (
|
|
244
|
+
token_df
|
|
245
|
+
.set_index(new_tokens)[head_col]
|
|
246
|
+
.to_dict()
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
for merge_id, row in merge_df.set_index(merge_id_col).iterrows():
|
|
250
|
+
|
|
251
|
+
token_ids = row[merge_token_ids_col]
|
|
252
|
+
|
|
253
|
+
if not token_ids:
|
|
254
|
+
merge_heads[merge_id] = []
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
external_heads = []
|
|
258
|
+
|
|
259
|
+
for token_id in token_ids:
|
|
260
|
+
|
|
261
|
+
head = token_to_head.get(token_id)
|
|
262
|
+
|
|
263
|
+
# Root / missing head
|
|
264
|
+
if head is None:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
resolved_head = resolve_node(head)
|
|
268
|
+
|
|
269
|
+
# Ignore edges internal to this merge
|
|
270
|
+
if resolved_head == merge_id:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
external_heads.append(resolved_head)
|
|
274
|
+
|
|
275
|
+
# Preserve order while removing duplicates
|
|
276
|
+
merge_heads[merge_id] = list(dict.fromkeys(external_heads))
|
|
277
|
+
|
|
278
|
+
merge_df[merge_head_col] = (
|
|
279
|
+
merge_df[merge_id_col]
|
|
280
|
+
.map(merge_heads)
|
|
281
|
+
.apply(lambda x: x if isinstance(x, list) else [])
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# --------------------------------------------------
|
|
285
|
+
# 4. Update token_df heads
|
|
286
|
+
#
|
|
287
|
+
# This is useful if you want the remaining token-level
|
|
288
|
+
# edges to point to merged nodes.
|
|
289
|
+
# --------------------------------------------------
|
|
290
|
+
token_df[merge_head_col] = (
|
|
291
|
+
token_df[head_col]
|
|
292
|
+
.map(resolve_node)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# --------------------------------------------------
|
|
296
|
+
# 5. Remove tokens absorbed into merge groups
|
|
297
|
+
# --------------------------------------------------
|
|
298
|
+
merged_token_ids = set(token_to_merge.keys())
|
|
299
|
+
|
|
300
|
+
token_df = token_df[
|
|
301
|
+
~token_df[new_tokens].isin(merged_token_ids)
|
|
302
|
+
].copy()
|
|
303
|
+
|
|
304
|
+
# --------------------------------------------------
|
|
305
|
+
# 6. IMPORTANT:
|
|
306
|
+
# If the resulting dataframe should contain the
|
|
307
|
+
# merged nodes themselves, create one row per merge.
|
|
308
|
+
# --------------------------------------------------
|
|
309
|
+
merged_rows = []
|
|
310
|
+
|
|
311
|
+
for _, row in merge_df.iterrows():
|
|
312
|
+
merge_id = row[merge_id_col]
|
|
313
|
+
heads = row[merge_head_col]
|
|
314
|
+
|
|
315
|
+
# A merge can have multiple external heads.
|
|
316
|
+
for head in heads:
|
|
317
|
+
merged_rows.append({
|
|
318
|
+
new_tokens: merge_id,
|
|
319
|
+
head_col: head,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
merged_token_df = pd.DataFrame(
|
|
323
|
+
merged_rows,
|
|
324
|
+
columns=[new_tokens, head_col]
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# --------------------------------------------------
|
|
328
|
+
# 7. Combine normal tokens + merged nodes
|
|
329
|
+
# --------------------------------------------------
|
|
330
|
+
token_df = pd.concat(
|
|
331
|
+
[token_df, merged_token_df],
|
|
332
|
+
ignore_index=True,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return token_df, merge_df
|
|
336
|
+
|
StickyText/Graph.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from .ModelManager import ModelManager
|
|
2
|
+
from .Pipeline import Pipeline
|
|
3
|
+
from .NodeMerge import NodeMerge
|
|
4
|
+
from .BuildGraph import build_L2_graph, visualize_dependency_graph, subside_nodes
|
|
5
|
+
from .EdgeMerge import EdgeMerge
|
|
6
|
+
from .NodeAttributes import NodeAttributes
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
def load_models():
|
|
10
|
+
return ModelManager()
|
|
11
|
+
|
|
12
|
+
def show_graph(text:str):
|
|
13
|
+
try:
|
|
14
|
+
model_manager = load_models()
|
|
15
|
+
token_df = Pipeline(text, 1, 1, model_manager, L3=True).process_text()
|
|
16
|
+
if token_df is None:
|
|
17
|
+
print(f"No subject found in : {text}")
|
|
18
|
+
return
|
|
19
|
+
token_df = token_df.drop(token_df[token_df["pos"] == "PUNCT"].index)
|
|
20
|
+
nodes_merge = NodeMerge(token_df=token_df)
|
|
21
|
+
coref_nodes_df = nodes_merge.coref_nodes()
|
|
22
|
+
coref_nodes_df = NodeAttributes(token_df).noun_node_attributes(coref_nodes_df)
|
|
23
|
+
verbs_nodes_df = nodes_merge.merge_nodes(merge_id_col='phrase_id',merge_text_col="verb_text")
|
|
24
|
+
verbs_nodes_df["attributes"] = None
|
|
25
|
+
|
|
26
|
+
coref_nodes_df['merge_id'] = coref_nodes_df['coref_id']
|
|
27
|
+
coref_nodes_df['merge_text'] = coref_nodes_df['representative_text']
|
|
28
|
+
coref_nodes_df['merge_token_ids'] = coref_nodes_df['coref_token_ids']
|
|
29
|
+
verbs_nodes_df['merge_id'] = verbs_nodes_df['phrase_id']
|
|
30
|
+
# aux nodes
|
|
31
|
+
aux_nodes = verbs_nodes_df.loc[verbs_nodes_df["merge_text"] == "", "merge_id"].tolist()
|
|
32
|
+
|
|
33
|
+
merge_df = pd.concat([verbs_nodes_df, coref_nodes_df], axis=0, join="inner")
|
|
34
|
+
|
|
35
|
+
token_df, coref_nodes_df = EdgeMerge(token_df).head_to_merge(merge_df)
|
|
36
|
+
|
|
37
|
+
G = build_L2_graph( coref_nodes_df, 'merge_id', 'merge_text', 'merge_head')
|
|
38
|
+
|
|
39
|
+
G = subside_nodes(G, aux_nodes)
|
|
40
|
+
|
|
41
|
+
visualize_dependency_graph(G)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print("Error in building the graph.")
|
|
44
|
+
|
StickyText/MergeCoref.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
class MergeCoref:
|
|
4
|
+
def __init__(self, coref_df, doc_id, chunk_id):
|
|
5
|
+
self.coref_df = coref_df
|
|
6
|
+
self.doc_id = doc_id
|
|
7
|
+
self.chunk_id = chunk_id
|
|
8
|
+
|
|
9
|
+
def _apply(self, row):
|
|
10
|
+
|
|
11
|
+
matches = self.coref_df[
|
|
12
|
+
(self.coref_df["start_char"] < row["end"]) &
|
|
13
|
+
(self.coref_df["end_char"] > row["start"])
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
if matches.empty:
|
|
17
|
+
return pd.Series({
|
|
18
|
+
"coref": None,
|
|
19
|
+
"representative_text": None,
|
|
20
|
+
"is_representative": False
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
# If multiple chains can overlap, preserve them
|
|
24
|
+
return pd.Series({
|
|
25
|
+
"coref": matches["coref"].tolist(),
|
|
26
|
+
"representative_text": matches["representative_text"].tolist(),
|
|
27
|
+
"is_representative": matches["is_representative"].tolist()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
def merge(self, token_df):
|
|
31
|
+
token_df[
|
|
32
|
+
["coref", "representative_text", "is_representative"]
|
|
33
|
+
] = token_df.apply(self._apply, axis=1)
|
|
34
|
+
return token_df
|
StickyText/MergeDF.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MergeDF:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
given_df,
|
|
9
|
+
token_start_col="start",
|
|
10
|
+
token_end_col="end",
|
|
11
|
+
given_start_col="start",
|
|
12
|
+
given_end_col="end",
|
|
13
|
+
):
|
|
14
|
+
self.given_df = given_df
|
|
15
|
+
self.token_start_col = token_start_col
|
|
16
|
+
self.token_end_col = token_end_col
|
|
17
|
+
self.given_start_col = given_start_col
|
|
18
|
+
self.given_end_col = given_end_col
|
|
19
|
+
|
|
20
|
+
def _apply(self, row):
|
|
21
|
+
matches = self.given_df[
|
|
22
|
+
(self.given_df[self.given_start_col] < row[self.token_end_col]) &
|
|
23
|
+
(self.given_df[self.given_end_col] > row[self.token_start_col])
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# Only return columns that shouldn't overwrite token columns
|
|
27
|
+
merge_cols = [
|
|
28
|
+
col for col in self.given_df.columns
|
|
29
|
+
if col not in (self.given_start_col, self.given_end_col)
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
if matches.empty:
|
|
33
|
+
return pd.Series({col: None for col in merge_cols})
|
|
34
|
+
|
|
35
|
+
return pd.Series({
|
|
36
|
+
col: matches[col].tolist()
|
|
37
|
+
for col in merge_cols
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
def merge(self, token_df):
|
|
41
|
+
merged_columns = token_df.apply(self._apply, axis=1)
|
|
42
|
+
|
|
43
|
+
for col in merged_columns.columns:
|
|
44
|
+
token_df[col] = merged_columns[col]
|
|
45
|
+
|
|
46
|
+
return token_df
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
from .model_config import MODELS
|
|
3
|
+
from .load_models import download_spacy_model, load_spacy_model, load_stanza_pipe
|
|
4
|
+
|
|
5
|
+
class ModelManager:
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.config = MODELS
|
|
9
|
+
self.models = {}
|
|
10
|
+
|
|
11
|
+
def download(self, model_name):
|
|
12
|
+
model_config = self.config[model_name]
|
|
13
|
+
if model_config['type'] == "spacy":
|
|
14
|
+
download_spacy_model(model_config['name'])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load(self, model_name):
|
|
18
|
+
model_config = self.config[model_name]
|
|
19
|
+
#try:
|
|
20
|
+
if model_config['type'] == "spacy":
|
|
21
|
+
model = load_spacy_model(model_config['name'])
|
|
22
|
+
self.models[model_name] = model
|
|
23
|
+
elif model_config['type'] == "stanza":
|
|
24
|
+
model = load_stanza_pipe(model_config['name'], model_config['processor'])
|
|
25
|
+
else:
|
|
26
|
+
raise Exception
|
|
27
|
+
return model
|
|
28
|
+
#except Exception as e:
|
|
29
|
+
# try:
|
|
30
|
+
# self.download(model_name)
|
|
31
|
+
# except Exception as e:
|
|
32
|
+
# print("Error downloading model")
|
|
33
|
+
|
|
34
|
+
|
StickyText/NER.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is abstract class with methods initialize or load model , process text , process batch
|
|
3
|
+
"""
|
|
4
|
+
from .ModelManager import ModelManager
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
class NER:
|
|
8
|
+
def __init__(self, model_manager: ModelManager, text:str):
|
|
9
|
+
self.model_manager = model_manager
|
|
10
|
+
self.text = text
|
|
11
|
+
|
|
12
|
+
def process_text(self):
|
|
13
|
+
"""Returns DataFrame or JSON records with columns token_text (lower),
|
|
14
|
+
sentence_num, token_num, pos, dep
|
|
15
|
+
"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class SpacyNER(NER):
|
|
19
|
+
def __init__(self, model_manager, spacy_model_name, text_tokenizer, text):
|
|
20
|
+
super().__init__(model_manager, text)
|
|
21
|
+
self.model_name = spacy_model_name
|
|
22
|
+
self.model_manager.load(self.model_name)
|
|
23
|
+
self.text_tokenizer = text_tokenizer
|
|
24
|
+
def process_text(self, res):
|
|
25
|
+
# self.res = self.model_manager.models[self.model_name](self.text)
|
|
26
|
+
try:
|
|
27
|
+
self.res = res
|
|
28
|
+
df = pd.DataFrame(self.res.to_json()["ents"])[['start','end',"label"]]
|
|
29
|
+
df.rename(columns={'label':'ner'}, inplace=True)
|
|
30
|
+
return df
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
class SpacyNounPhrases:
|
|
35
|
+
def __init__(self, text_tokenizer):
|
|
36
|
+
self.text_tokenizer = text_tokenizer
|
|
37
|
+
|
|
38
|
+
def process_text(self):
|
|
39
|
+
return list(self.text_tokenizer.res.noun_chunks)
|
|
40
|
+
|
|
41
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
class NodeAttributes:
|
|
3
|
+
def __init__(self, token_df):
|
|
4
|
+
self.token_df = token_df
|
|
5
|
+
|
|
6
|
+
def noun_node_attributes(self, coref_nodes_df, coref_token_ids_col="coref_token_ids"):
|
|
7
|
+
def get_attributes(row):
|
|
8
|
+
token_ids = row[coref_token_ids_col]
|
|
9
|
+
|
|
10
|
+
matched = self.token_df[self.token_df["token_id"].isin(token_ids)]
|
|
11
|
+
|
|
12
|
+
return matched.loc[matched["pos"] != "PRON", "text"].tolist()
|
|
13
|
+
|
|
14
|
+
coref_nodes_df["attributes"] = coref_nodes_df.apply(get_attributes, axis=1)
|
|
15
|
+
return coref_nodes_df
|
|
16
|
+
|
|
17
|
+
|
StickyText/NodeMerge.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
class NodeMerge:
|
|
4
|
+
def __init__(self, token_df):
|
|
5
|
+
self.token_df = token_df
|
|
6
|
+
|
|
7
|
+
def noun_phrase_nodes(self, noun_phrase_column = "noun_phrase"):
|
|
8
|
+
self.token_df[noun_phrase_column] = self.token_df[noun_phrase_column].apply(
|
|
9
|
+
lambda x: x[0] if isinstance(x, (list, tuple)) else x
|
|
10
|
+
)
|
|
11
|
+
noun_df = (
|
|
12
|
+
self.token_df
|
|
13
|
+
.groupby(["noun_phrase", "chunk_id", "doc_id"], sort=False, as_index=False)
|
|
14
|
+
.agg(token_ids=("token_id", list))
|
|
15
|
+
.reset_index(drop=True)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
noun_df.insert(0, "noun_id", range(len(noun_df)))
|
|
19
|
+
return noun_df
|
|
20
|
+
|
|
21
|
+
def coref_nodes(self):
|
|
22
|
+
self.token_df["coref_id"] = self.token_df["coref"].apply(
|
|
23
|
+
lambda x: f"nn_{int(x[0])}" if isinstance(x, (list, tuple)) else x
|
|
24
|
+
)
|
|
25
|
+
self.token_df["representative_text"] = self.token_df["representative_text"].apply(
|
|
26
|
+
lambda x: x[0] if isinstance(x, (list, tuple)) else x
|
|
27
|
+
)
|
|
28
|
+
self.token_df["is_representative"] = self.token_df["is_representative"].apply(
|
|
29
|
+
lambda x: x[0] if isinstance(x, (list, tuple)) else x
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
coref_df = (
|
|
33
|
+
self.token_df
|
|
34
|
+
.groupby(["coref_id", "chunk_id", "doc_id"], sort=False, as_index=False)
|
|
35
|
+
.agg(
|
|
36
|
+
representative_text=("representative_text", "first"),
|
|
37
|
+
representative_text_token_ids=(
|
|
38
|
+
"token_id",
|
|
39
|
+
lambda x: list(x)
|
|
40
|
+
),
|
|
41
|
+
coref_token_ids=("token_id", list)
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
coref_df = (
|
|
45
|
+
self.token_df
|
|
46
|
+
.groupby(["coref_id", "chunk_id", "doc_id"], sort=False)
|
|
47
|
+
.apply(
|
|
48
|
+
lambda g: pd.Series({
|
|
49
|
+
# "token_id":g["coref_id"],
|
|
50
|
+
"text": g["representative_text"].iloc[0],
|
|
51
|
+
"representative_text": g["representative_text"].iloc[0],
|
|
52
|
+
"representative_text_token_ids": g.loc[
|
|
53
|
+
g["is_representative"], "token_id"
|
|
54
|
+
].tolist(),
|
|
55
|
+
"coref_token_ids": g["token_id"].tolist(),
|
|
56
|
+
"ner":g['ner'].iloc[0] ,
|
|
57
|
+
# "coref_head_ids":g['head'].tolist(),
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
.reset_index()
|
|
61
|
+
)
|
|
62
|
+
#coref_df['coref_id'] = coref_df['coref_id'].astype('int')
|
|
63
|
+
return coref_df
|
|
64
|
+
|
|
65
|
+
def merge_nodes(self, merge_id_col = "merge_id", merge_text_col = "merge_text"):
|
|
66
|
+
self.token_df[merge_id_col] = self.token_df[merge_id_col].apply(
|
|
67
|
+
lambda x: x[0] if isinstance(x, (list, tuple)) else x
|
|
68
|
+
)
|
|
69
|
+
self.token_df[merge_text_col] = self.token_df[merge_text_col].apply(
|
|
70
|
+
lambda x: x[0] if isinstance(x, (list, tuple)) else x
|
|
71
|
+
)
|
|
72
|
+
merge_df = (
|
|
73
|
+
self.token_df
|
|
74
|
+
.groupby(
|
|
75
|
+
[merge_id_col, "chunk_id", "doc_id"],
|
|
76
|
+
sort=False, as_index=False
|
|
77
|
+
)
|
|
78
|
+
.agg(
|
|
79
|
+
merge_text = (merge_text_col, "first"),
|
|
80
|
+
merge_token_ids=("token_id", list),
|
|
81
|
+
merge_heads=("head", list),
|
|
82
|
+
#pos = ('pos', " ".join),
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
#noun_df.insert(0, "noun_id", range(len(noun_df)))
|
|
86
|
+
return merge_df
|
|
87
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .DepParse import SpacyDepParser
|
|
2
|
+
from .TextTokenizer import TextTokenizer
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
class NounPhrases:
|
|
6
|
+
def __init__(self, text_tokenizer: TextTokenizer):
|
|
7
|
+
self.text_tokenizer = text_tokenizer
|
|
8
|
+
|
|
9
|
+
def process_text(self, res):
|
|
10
|
+
noun_chunks = res.noun_chunks
|
|
11
|
+
|
|
12
|
+
noun_df = pd.DataFrame(
|
|
13
|
+
{
|
|
14
|
+
"noun_phrase": str(chunk.text).strip().lower(),
|
|
15
|
+
"start": chunk.start_char,
|
|
16
|
+
"end": chunk.end_char,
|
|
17
|
+
#"start_token": chunk.start,
|
|
18
|
+
#"end_token":chunk.end,
|
|
19
|
+
|
|
20
|
+
}
|
|
21
|
+
for chunk in noun_chunks
|
|
22
|
+
|
|
23
|
+
)
|
|
24
|
+
return noun_df
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
class NounPhrasesAppend:
|
|
4
|
+
def __init__(self):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
def add_missing_nouns(self, coref_df, noun_df, representative_text_col='representative_text',
|
|
8
|
+
is_representative_col='is_representative'):
|
|
9
|
+
|
|
10
|
+
noun_df = noun_df.rename(columns={'start':'start_char','end':'end_char'})
|
|
11
|
+
|
|
12
|
+
def check_missed(ref_row, df_sec):
|
|
13
|
+
# Returns True if ANY span in df_sec overlaps with the current ref_row
|
|
14
|
+
overlap = (df_sec["start_char"] < ref_row["end_char"]) & (df_sec["end_char"] > ref_row["start_char"])
|
|
15
|
+
return overlap.any()
|
|
16
|
+
|
|
17
|
+
# Apply the function across rows axis=1, then filter where it evaluates to False (completely missed)
|
|
18
|
+
missed_mask = ~noun_df.apply(lambda row: check_missed(row, coref_df), axis=1)
|
|
19
|
+
|
|
20
|
+
df_to_add = noun_df[missed_mask]
|
|
21
|
+
if df_to_add is not None:
|
|
22
|
+
df_to_add['coref'] = df_to_add.reset_index(drop=True).index + len(coref_df)
|
|
23
|
+
df_to_add[is_representative_col] = True
|
|
24
|
+
df_to_add.rename(columns={'noun_phrase':representative_text_col,}, inplace=True)
|
|
25
|
+
coref_df = pd.concat([coref_df, df_to_add], axis=0, join="outer")
|
|
26
|
+
return coref_df
|
|
27
|
+
return coref_df
|
|
28
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from nltk.stem import PorterStemmer
|
|
3
|
+
|
|
4
|
+
class PhrasalVerbs:
|
|
5
|
+
def __init__(self, token_df):
|
|
6
|
+
self.token_df = token_df
|
|
7
|
+
|
|
8
|
+
def phrasal_verbs_df(self, include_start=True, include_end=True):
|
|
9
|
+
df = self.token_df.copy().sort_values("token_id").reset_index(drop=True)
|
|
10
|
+
|
|
11
|
+
coref_indices = df.index[df["coref"].notna()].tolist()
|
|
12
|
+
length = len(self.token_df)
|
|
13
|
+
|
|
14
|
+
spans = list(zip(coref_indices, coref_indices[1:]))
|
|
15
|
+
|
|
16
|
+
_ = spans.insert(0, (-1, coref_indices[0])) if include_start else None
|
|
17
|
+
|
|
18
|
+
_ = spans.append((coref_indices[-1], length)) if include_end else None
|
|
19
|
+
|
|
20
|
+
between = [
|
|
21
|
+
list(range(a + 1, b))
|
|
22
|
+
for a, b in spans
|
|
23
|
+
if a + 1 < b
|
|
24
|
+
]
|
|
25
|
+
cluster_map = dict(enumerate(between))
|
|
26
|
+
|
|
27
|
+
verbs_df = (
|
|
28
|
+
pd.DataFrame(cluster_map.items(), columns=["phrase_id", "token_id"])
|
|
29
|
+
.explode("token_id")
|
|
30
|
+
.reset_index(drop=True)
|
|
31
|
+
)
|
|
32
|
+
verbs_df['phrase_id'] = verbs_df['phrase_id'].apply(lambda x: f"vv_{x}")
|
|
33
|
+
token_df = self.token_df.merge( verbs_df, on="token_id", how="left" )
|
|
34
|
+
#remove helping verbs or AUX
|
|
35
|
+
token_df.loc[token_df["pos"] == "AUX", "text"] = ""
|
|
36
|
+
verbs_df = ( token_df
|
|
37
|
+
.groupby(
|
|
38
|
+
["phrase_id", "chunk_id", "doc_id"],
|
|
39
|
+
sort=False, as_index=False
|
|
40
|
+
)
|
|
41
|
+
.agg(
|
|
42
|
+
#verb_token_ids=("token_id", list),
|
|
43
|
+
verb_text=("text", " ".join),
|
|
44
|
+
start=("start", "min"),
|
|
45
|
+
end=("end", "max"),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
verbs_df = verbs_df.drop(columns=['chunk_id', 'doc_id'])
|
|
49
|
+
# handle isolated aux
|
|
50
|
+
verbs_df['aux'] = None
|
|
51
|
+
verbs_df.loc[verbs_df["verb_text"] == "", "aux"] = "AUX"
|
|
52
|
+
#verbs_df["verb_text"] = verbs_df["verb_text"].replace("", "#be-form")
|
|
53
|
+
# stem
|
|
54
|
+
stemmer = PorterStemmer()
|
|
55
|
+
verbs_df["verb_text"] = verbs_df["verb_text"].apply(lambda x: stemmer.stem(x) if isinstance(x, str) else x)
|
|
56
|
+
|
|
57
|
+
return verbs_df
|
|
58
|
+
|
StickyText/Pipeline.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from .CoRef import CoRef
|
|
2
|
+
from .MergeCoref import MergeCoref
|
|
3
|
+
from .MergeDF import MergeDF
|
|
4
|
+
from .NER import NER, SpacyNER
|
|
5
|
+
from .NounPhrases import NounPhrases
|
|
6
|
+
from .NounPhrasesAppend import NounPhrasesAppend
|
|
7
|
+
from .PhrasalVerbs import PhrasalVerbs
|
|
8
|
+
from .TextTokenizer import TextTokenizer
|
|
9
|
+
from .SpacyProcess import SpacyProcess
|
|
10
|
+
|
|
11
|
+
class Pipeline:
|
|
12
|
+
def __init__(self, text:str, doc_id, chunk_id, model_manager, L3=False):
|
|
13
|
+
self.spacy_process = SpacyProcess(text, doc_id,chunk_id, "dep_parse", model_manager)
|
|
14
|
+
self.text_tokenizer = TextTokenizer(text, chunk_id, doc_id)
|
|
15
|
+
self.coref = CoRef(model_manager, "coref", text, doc_id, chunk_id)
|
|
16
|
+
self.ner = SpacyNER(model_manager, "dep_parse", self.text_tokenizer, text)
|
|
17
|
+
self.noun_phrases = NounPhrases(self.text_tokenizer)
|
|
18
|
+
self.L3 = L3
|
|
19
|
+
self.merge_coref = None
|
|
20
|
+
self.merge_df = None
|
|
21
|
+
|
|
22
|
+
def process_text(self):
|
|
23
|
+
try:
|
|
24
|
+
coref_df = self.coref.process_text()
|
|
25
|
+
res = self.spacy_process.process_text()
|
|
26
|
+
token_df = self.text_tokenizer.process_text(res=res)
|
|
27
|
+
noun_df = self.noun_phrases.process_text(res)
|
|
28
|
+
coref_df = NounPhrasesAppend().add_missing_nouns(coref_df, noun_df)
|
|
29
|
+
token_df = MergeCoref(coref_df=coref_df, doc_id=None, chunk_id=None).merge(token_df=token_df)
|
|
30
|
+
del coref_df
|
|
31
|
+
|
|
32
|
+
token_df = MergeDF(given_df=noun_df ).merge(token_df=token_df)
|
|
33
|
+
ner_df = self.ner.process_text(res)
|
|
34
|
+
if ner_df is not None:
|
|
35
|
+
token_df = MergeDF(given_df=ner_df).merge(token_df=token_df)
|
|
36
|
+
else:
|
|
37
|
+
token_df['ner'] = None
|
|
38
|
+
if self.L3:
|
|
39
|
+
verbs_df = PhrasalVerbs(token_df).phrasal_verbs_df()
|
|
40
|
+
token_df = MergeDF(verbs_df).merge(token_df=token_df)
|
|
41
|
+
return token_df
|
|
42
|
+
return token_df
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print("Error while processing text.")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This class tokenize text and creates dataframe with doc_id, chunk_id, sentence_num, token_num, text (lower), start, end
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class SpacyProcess:
|
|
6
|
+
def __init__(self, text:str, doc_id, chunk_id, spacy_model_name, model_manager):
|
|
7
|
+
self.text = text
|
|
8
|
+
self.doc_id = doc_id
|
|
9
|
+
self.chunk_id = chunk_id
|
|
10
|
+
self.model_name = spacy_model_name
|
|
11
|
+
self.model_manager = model_manager
|
|
12
|
+
self.model_manager.load(self.model_name)
|
|
13
|
+
|
|
14
|
+
def process_text(self):
|
|
15
|
+
self.res = self.model_manager.load(self.model_name)(self.text)
|
|
16
|
+
|
|
17
|
+
return self.res
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This class tokenize text and creates dataframe with doc_id, chunk_id, sentence_num, token_num, text (lower), start, end
|
|
3
|
+
"""
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
class TextTokenizer:
|
|
7
|
+
def __init__(self, text:str, chunk_id, doc_id):
|
|
8
|
+
self.text = text
|
|
9
|
+
self.chunk_id = chunk_id
|
|
10
|
+
self.doc_id = doc_id
|
|
11
|
+
|
|
12
|
+
def process_text(self, res):
|
|
13
|
+
self.res = res # self.model_manager.load(self.model_name)(self.text)
|
|
14
|
+
df = pd.DataFrame(self.res.to_json()["tokens"])[['id','start','end',"head","pos", "dep"]]
|
|
15
|
+
df.rename(columns={'id':'token_id'}, inplace=True)
|
|
16
|
+
df['text'] = df.apply(lambda r: self.text[int(r['start']) : int(r['end'])], axis=1)
|
|
17
|
+
df['text'] = df['text'].str.lower().str.strip()
|
|
18
|
+
df['chunk_id'] = self.chunk_id
|
|
19
|
+
df['doc_id'] = self.doc_id
|
|
20
|
+
|
|
21
|
+
return df
|
StickyText/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import spacy
|
|
2
|
+
import subprocess, sys
|
|
3
|
+
import stanza
|
|
4
|
+
from functools import cache
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def download_spacy_model(model_name):
|
|
8
|
+
subprocess.run([sys.executable, "-m", "spacy", "download", model_name])
|
|
9
|
+
|
|
10
|
+
@cache
|
|
11
|
+
def load_spacy_model(model_name):
|
|
12
|
+
try:
|
|
13
|
+
nlp = spacy.load(model_name)
|
|
14
|
+
except Exception as e:
|
|
15
|
+
download_spacy_model(model_name)
|
|
16
|
+
try:
|
|
17
|
+
nlp = load_spacy_model(model_name)
|
|
18
|
+
except Exception as e:
|
|
19
|
+
print("Error: Cannot download the model.")
|
|
20
|
+
return nlp
|
|
21
|
+
|
|
22
|
+
@cache
|
|
23
|
+
def load_stanza_pipe(lang, processors):
|
|
24
|
+
return stanza.Pipeline(lang, processors=processors, verbose=False)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
MODELS = {
|
|
3
|
+
"dep_parse": {
|
|
4
|
+
"type": "spacy",
|
|
5
|
+
"name": "en_core_web_sm",
|
|
6
|
+
"cache_dir": "~/.stickY-text/models",
|
|
7
|
+
},
|
|
8
|
+
"coref": {
|
|
9
|
+
"type": "stanza",
|
|
10
|
+
"name": "en",
|
|
11
|
+
"processor": "tokenize,coref",
|
|
12
|
+
"cache_dir": "~/.stickY-text/models",
|
|
13
|
+
},
|
|
14
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: StickyText
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A package for building and visualizing dependency graphs.
|
|
5
|
+
Author-email: Yoga Harshitha D <harshithad731@gmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: Licence.md
|
|
9
|
+
Requires-Dist: networkx
|
|
10
|
+
Requires-Dist: pandas
|
|
11
|
+
Requires-Dist: spacy
|
|
12
|
+
Requires-Dist: nltk
|
|
13
|
+
Requires-Dist: stanza
|
|
14
|
+
Requires-Dist: transformers
|
|
15
|
+
Requires-Dist: peft
|
|
16
|
+
Requires-Dist: matplotlib
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# `sticky-text`
|
|
20
|
+
|
|
21
|
+
A Python package for building and visualizing linguistic dependency graphs from text using custom NLP pipelines and model managers.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
* **Automated Text Processing:** Runs end-to-end text processing pipelines directly from input text.
|
|
28
|
+
* **Triplet Style Knowledge Graph Construction:** Builds entity and dependency graphs from the data.
|
|
29
|
+
* **Dependency Graph Visualization:** Generates visual graphs showing token relationships and coreferences.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install the package directly using `pip`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install sticky-text
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
The package exposes a single entry-point function, `show_graph`:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from StickyText import show_graph
|
|
49
|
+
|
|
50
|
+
# Process text and visualize the generated dependency graph
|
|
51
|
+
text = "Alice went to the store. She bought some apples."
|
|
52
|
+
show_graph(text)
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Package Architecture
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
your_package_name/
|
|
62
|
+
├── __init__.py # Main entry point exposing show_graph
|
|
63
|
+
├── ModelManager.py # Manages NLP model initialization and loading
|
|
64
|
+
├── Pipeline.py # Executes text processing pipelines
|
|
65
|
+
└── BuildGraph.py # Handles graph construction and network visualization
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
## About this version
|
|
69
|
+
|
|
70
|
+
This is a free and trial version. For large documents and premium features contact author.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
StickyText/BuildGraph.py,sha256=2Tuu5hhtZEmV5ag-6qO3FuV3jf6LW0r8vwbGL6bvrdM,3561
|
|
2
|
+
StickyText/CoRef.py,sha256=Law3ndj5vJgkWX8iSZoIbNM1hJWnk5jnR_Jl4BSKoJA,1308
|
|
3
|
+
StickyText/DepParse.py,sha256=wV2gbjv4SnWgs4tqSfrXcc3CvbhA_OgCh_sd8_otf7A,991
|
|
4
|
+
StickyText/EdgeMerge.py,sha256=3abvSLXN3UayfCY8BHUVBraegd6-lrbOAtY3iVE4nuI,11272
|
|
5
|
+
StickyText/Graph.py,sha256=6oYejtqIho75k48Lc0y45A5B9cuEhNdBRpNIQXToXtk,1855
|
|
6
|
+
StickyText/MergeCoref.py,sha256=j508qLyPlSKL3T4R4Vnl9SBYdCmQjl3FCJRNWVOk_3w,1043
|
|
7
|
+
StickyText/MergeDF.py,sha256=cW-icvLDdUVhX6P9pesQ0iT1zuQ0iKxIfZ3kn-CZTgk,1335
|
|
8
|
+
StickyText/ModelManager.py,sha256=OrP4X5YpRAYJek74v35MXTVSwTWrEMugtu1_UmTF3Do,1086
|
|
9
|
+
StickyText/NER.py,sha256=lNGeC1yrpXy1bplorTmzHlgHTVxa3Pm-GAH30Dpaakc,1372
|
|
10
|
+
StickyText/NodeAttributes.py,sha256=p80_eope6QBay_cv-H8JNea_bWxki6TTozNsbAhIFOM,571
|
|
11
|
+
StickyText/NodeMerge.py,sha256=_FO5QdHc-hXZ0tRAIxbVkVwUdo6wtkICdjxpk0f_87Q,3338
|
|
12
|
+
StickyText/NounPhrases.py,sha256=Lj2JTU_MqKTnv5ntiaI6eOM9r6DjKSzkLBlE8E3bhnU,744
|
|
13
|
+
StickyText/NounPhrasesAppend.py,sha256=Wp4GJEo6ggZG6RmsrYmcuDjd88_VXjVcL82XzAyqJt0,1292
|
|
14
|
+
StickyText/PhrasalVerbs.py,sha256=x02B3wetgWkNeFkV4g0Nde6ZIb6Vl8Wu255Iu1ncymI,2333
|
|
15
|
+
StickyText/Pipeline.py,sha256=J48BW20NV3mnGiSCOkobTvGWXl0IeQINN5Xh2qO3VJE,1989
|
|
16
|
+
StickyText/SpacyProcess.py,sha256=VHqJankXn7DYkELYSjwWLMKXgM-n8f4vTvPIbDQY98s,605
|
|
17
|
+
StickyText/TextTokenizer.py,sha256=QBu93pFYM5WC0bjOriItv6FCuP9GrTtugaf9XK_UH4I,854
|
|
18
|
+
StickyText/__init__.py,sha256=Q47vpfQ7_9cMlkFB0mvKW_GMUhB1Z9c3-G34gYDWl_c,57
|
|
19
|
+
StickyText/load_models.py,sha256=MSH-RxWPchp0aXnShiVbVzkv1fliLZ0939dDr7jK5q8,656
|
|
20
|
+
StickyText/model_config.py,sha256=t_6P4ELmQMYyQcizvsugaUbG8GTmI5U1AXZlk3G5FVw,312
|
|
21
|
+
stickytext-0.1.1.dist-info/licenses/Licence.md,sha256=Wu3M8G8ovXxcdSPlUw5IbDEC0o8RkYSC4xjybmxPw1U,4637
|
|
22
|
+
stickytext-0.1.1.dist-info/METADATA,sha256=GFCsbQJxlNVCM8t5zZLDEkOoWNiANGv2owSXuiXDLHM,1871
|
|
23
|
+
stickytext-0.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
24
|
+
stickytext-0.1.1.dist-info/top_level.txt,sha256=cFZ_lHIuNP8ePwPH1-SqSIamgWlrdulsPUDOTl8Z0Fs,16
|
|
25
|
+
stickytext-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Proprietary Software License
|
|
2
|
+
|
|
3
|
+
**Copyright © 2026 Yoga Harshitha D. All Rights Reserved.**
|
|
4
|
+
|
|
5
|
+
This software package, including all source code, object code, documentation, files, modules, and related materials (collectively, the **"Software"**) is proprietary property of **Yoga Harshitha D.**
|
|
6
|
+
|
|
7
|
+
## 1. Grant of License
|
|
8
|
+
|
|
9
|
+
Subject to the terms of this license, Yoga Harshitha D. grants the authorized user a limited, non-exclusive, non-transferable, non-sublicensable license to **install and use** the Software solely for its intended purpose.
|
|
10
|
+
|
|
11
|
+
This license does not transfer ownership of the Software or any intellectual property rights to the user.
|
|
12
|
+
|
|
13
|
+
## 2. Restrictions
|
|
14
|
+
|
|
15
|
+
Except with prior written permission from Yoga Harshitha D., you **may not**:
|
|
16
|
+
|
|
17
|
+
* Copy, reproduce, duplicate, or redistribute the Software or any portion of it.
|
|
18
|
+
* Modify, alter, adapt, translate, or create derivative works from the Software.
|
|
19
|
+
* Publish, distribute, sell, rent, lease, sublicense, or transfer the Software.
|
|
20
|
+
* Share the Software with any unauthorized third party.
|
|
21
|
+
* Upload, mirror, or redistribute the Software through public or private package repositories.
|
|
22
|
+
* Include the Software or any portion of it in another software package or product.
|
|
23
|
+
* Remove, alter, or obscure copyright, license, or proprietary notices.
|
|
24
|
+
* Reverse engineer, decompile, disassemble, or attempt to derive the source code, algorithms, or underlying implementation of the Software, except where such restriction is prohibited by applicable law.
|
|
25
|
+
* Use the Software to create a competing product or service.
|
|
26
|
+
* Use the Software outside the scope of the license granted to you.
|
|
27
|
+
|
|
28
|
+
## 3. Ownership and Intellectual Property
|
|
29
|
+
|
|
30
|
+
The Software and all associated intellectual property rights remain the exclusive property of **Yoga Harshitha D.**
|
|
31
|
+
|
|
32
|
+
No copyright, patent, trademark, trade secret, or other intellectual property rights are transferred or assigned to the user by this license.
|
|
33
|
+
|
|
34
|
+
All rights not expressly granted under this license are reserved.
|
|
35
|
+
|
|
36
|
+
## 4. Source Code
|
|
37
|
+
|
|
38
|
+
The source code of the Software is proprietary.
|
|
39
|
+
|
|
40
|
+
Possession of a copy of the Software does not grant any right to access, obtain, copy, modify, or redistribute its source code.
|
|
41
|
+
|
|
42
|
+
Any attempt to circumvent technical measures intended to protect the Software or its source code is prohibited, except where such restriction is prohibited by applicable law.
|
|
43
|
+
|
|
44
|
+
## 5. Third-Party Components
|
|
45
|
+
|
|
46
|
+
The Software may contain or depend upon third-party libraries, packages, or other components.
|
|
47
|
+
|
|
48
|
+
Such components remain subject to their respective licenses. This license does not modify or restrict rights granted by applicable third-party licenses.
|
|
49
|
+
|
|
50
|
+
## 6. Termination
|
|
51
|
+
|
|
52
|
+
This license automatically terminates if you materially breach any of its terms.
|
|
53
|
+
|
|
54
|
+
Upon termination, you must immediately cease using the Software and delete copies of the Software in your possession or control, to the extent permitted and required by applicable law.
|
|
55
|
+
|
|
56
|
+
Termination does not limit any other rights or remedies available to Yoga Harshitha D.
|
|
57
|
+
|
|
58
|
+
## 7. Disclaimer of Warranty
|
|
59
|
+
|
|
60
|
+
THE SOFTWARE IS PROVIDED **"AS IS"** AND **"AS AVAILABLE"**, WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW.
|
|
61
|
+
|
|
62
|
+
Yoga Harshitha D. disclaims all warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, accuracy, title, and non-infringement.
|
|
63
|
+
|
|
64
|
+
## 8. Limitation of Liability
|
|
65
|
+
|
|
66
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, YOGA HARSHITHA D. SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES ARISING OUT OF OR RELATING TO THE USE OF THE SOFTWARE.
|
|
67
|
+
|
|
68
|
+
## 9. Governing Law
|
|
69
|
+
|
|
70
|
+
This license shall be governed by and construed in accordance with the laws of **India**.
|
|
71
|
+
|
|
72
|
+
Subject to applicable law, disputes arising out of or relating to this license shall be subject to the jurisdiction of the competent courts in **Bengaluru, Karnataka, India**.
|
|
73
|
+
|
|
74
|
+
## 10. No Implied Rights
|
|
75
|
+
|
|
76
|
+
No rights are granted by implication, estoppel, or otherwise.
|
|
77
|
+
|
|
78
|
+
Any permission to copy, modify, distribute, sublicense, or otherwise use the Software beyond the rights expressly granted in this license must be obtained through prior written authorization from **Yoga Harshitha D.**
|
|
79
|
+
|
|
80
|
+
## 11. Reservation of Rights
|
|
81
|
+
|
|
82
|
+
**Yoga Harshitha D. reserves all rights not expressly granted under this license.**
|
|
83
|
+
|
|
84
|
+
Unauthorized copying, modification, distribution, redistribution, or use of the Software is prohibited.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
**Copyright © 2026 Yoga Harshitha D.**
|
|
89
|
+
**All Rights Reserved.**
|