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.
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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,3 @@
1
+ from .Graph import show_graph
2
+
3
+ __all__ = ["show_graph"]
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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.**
@@ -0,0 +1,2 @@
1
+ StickyText
2
+ dist