Thao Pham commited on
Commit
9f62184
·
1 Parent(s): ffa15c5

Remove minimum frequency and add highlighted output in app.py

Browse files
app.py CHANGED
@@ -2,22 +2,40 @@ import gradio as gr
2
  import torch
3
  import os
4
 
 
5
  from pipeline import KeywordExtractorPipeline
6
 
7
  DIR_PATH = os.path.dirname(os.path.realpath(__file__))
8
 
9
 
10
- def extract_keyword(title, text, top_n, ngram_low_range, ngram_high_range, min_freq, diversify_result):
11
- # title = None
12
- # keyword_ls = kw_model.extract_keywords(title, text, ngram_range=(ngram_low_range, ngram_high_range), top_n=top_n,
13
- # min_freq=min_freq, use_kmeans=use_kmeans)
14
  inp = {"text": text, "title": title}
15
- keyword_ls = kw_pipeline(inputs=inp, min_freq=min_freq, ngram_n=(ngram_low_range, ngram_high_range),
16
  top_n=top_n, diversify_result=diversify_result)
17
  result = ''
18
  for kw, score in keyword_ls:
19
  result += f'{kw}: {score}\n'
20
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  if gr.NO_RELOAD:
@@ -36,12 +54,14 @@ if __name__ == "__main__":
36
  gr.Text(
37
  label="Title",
38
  lines=1,
39
- value="Enter title here",
40
  ),
41
  gr.Textbox(
42
  label="Text",
43
  lines=5,
44
- value="Enter text here",
 
 
45
  ),
46
  gr.Number(
47
  label="Top N keywords",
@@ -56,18 +76,12 @@ if __name__ == "__main__":
56
  label="Ngram high range",
57
  value=3
58
  ),
59
- gr.Number(
60
- label="Ngram minimum frequency",
61
- value=1
62
- ),
63
  gr.Checkbox(
64
  label="Diversify result"
65
  )
66
  ],
67
- # inputs=["text", "textbox", "number", "number", "number", "number", "checkbox"],
68
- outputs=gr.Textbox(
69
- label="Keywords Extracted",
70
- )
71
  )
72
 
73
  demo.launch(share=True) # Share your demo with just 1 extra parameter 🚀
 
2
  import torch
3
  import os
4
 
5
+ from model.process_text import process_text_pipeline
6
  from pipeline import KeywordExtractorPipeline
7
 
8
  DIR_PATH = os.path.dirname(os.path.realpath(__file__))
9
 
10
 
11
+ def extract_keyword(title, text, top_n, ngram_low_range, ngram_high_range, diversify_result):
 
 
 
12
  inp = {"text": text, "title": title}
13
+ keyword_ls = kw_pipeline(inputs=inp, min_freq=1, ngram_n=(ngram_low_range, ngram_high_range),
14
  top_n=top_n, diversify_result=diversify_result)
15
  result = ''
16
  for kw, score in keyword_ls:
17
  result += f'{kw}: {score}\n'
18
+ highlight_dict = get_highlighted_text(text, keyword_ls)
19
+ return result, highlight_dict
20
+
21
+
22
+ def get_highlighted_text(text, keyword_ls):
23
+ text = process_text_pipeline(text)
24
+ entities = []
25
+ for kw, score in keyword_ls:
26
+
27
+ kw = kw.replace('_', ' ')
28
+ kw_dict = {"entity": "KEY"}
29
+ start_idx = text.find(kw)
30
+
31
+ while start_idx != -1:
32
+ end_idx = start_idx + len(kw)
33
+ kw_dict["start"] = start_idx
34
+ kw_dict["end"] = end_idx
35
+ entities.append(kw_dict)
36
+ start_idx = text.find(kw, end_idx)
37
+
38
+ return {"text": text, "entities": entities}
39
 
40
 
41
  if gr.NO_RELOAD:
 
54
  gr.Text(
55
  label="Title",
56
  lines=1,
57
+ value="Truyền thuyết và hiện tại Thành Cổ Loa",
58
  ),
59
  gr.Textbox(
60
  label="Text",
61
  lines=5,
62
+ value="""Nhắc đến Cổ Loa, người ta nghĩ ngay đến truyền thuyết về An Dương Vương được thần Kim Quy bày cho cách xây thành, về chiếc lẫy nỏ thần làm từ móng chân rùa thần và mối tình bi thương Mỵ Châu – Trọng Thủy. Đằng sau những câu chuyện thiên về tâm linh ấy, thế hệ con cháu còn khám phá được những giá trị khảo cổ to lớn của Cổ Loa.
63
+ Khu di tích Cổ Loa cách trung – tâm Hà Nội 17km thuộc huyện Đông Anh, Hà Nội, có diện tích bảo tồn gần 500ha được coi là địa chỉ văn hóa đặc biệt của thủ đô và cả nước. Cổ Loa có hàng loạt di chỉ khảo cổ học đã được phát hiện, phản ánh quá trình phát triển liên tục của dân tộc ta từ sơ khai qua các thời kỳ đồ đồng, đồ đá và đồ sắt mà đỉnh cao là văn hóa Đông Sơn, vẫn được coi là nền văn minh sông Hồng thời kỳ tiền sử của dân tộc Việt Nam.
64
+ Cổ Loa từng là kinh đô của nhà nước Âu Lạc thời kỳ An Dương Vương (thế kỷ III TCN) và của nước Đại Việt thời Ngô Quyền (thế kỷ X) mà thành Cổ Loa là một di tích minh chứng còn lại cho đến ngày nay. Thành Cổ Loa được các nhà khảo cổ học đánh giá là “tòa thành cổ nhất, quy mô lớn vào bậc nhất, cấu trúc cũng thuộc loại độc đáo nhất trong lịch sử xây dựng thành lũy của người Việt cổ”.""",
65
  ),
66
  gr.Number(
67
  label="Top N keywords",
 
76
  label="Ngram high range",
77
  value=3
78
  ),
 
 
 
 
79
  gr.Checkbox(
80
  label="Diversify result"
81
  )
82
  ],
83
+ outputs=[gr.Textbox(label="Keywords Extracted"),
84
+ gr.HighlightedText(label="Highlight")]
 
 
85
  )
86
 
87
  demo.launch(share=True) # Share your demo with just 1 extra parameter 🚀
model/keyword_extraction_utils.py CHANGED
@@ -118,7 +118,6 @@ def get_segmentised_doc(nlp, rdrsegmenter, title, doc):
118
 
119
  if title is not None:
120
  segmentised_doc = rdrsegmenter.word_segment(title) + rdrsegmenter.word_segment(doc)
121
- print(segmentised_doc)
122
  ne_ls = set(get_named_entities(nlp, doc))
123
 
124
  segmentised_doc_ne = []
@@ -145,13 +144,6 @@ def compute_ngram_embeddings(tokenizer, phobert, ngram_list):
145
  return ngram_embeddings
146
 
147
 
148
- # def normalised_cosine_similarity(ngram_embedding, document_embedding):
149
- # similarity_score = cosine_similarity(ngram_embedding, document_embedding)
150
- # magnitude_ngram = np.linalg.norm(ngram_embedding)
151
- # magnitude_doc = np.linalg.norm(document_embedding)
152
- # return similarity_score / np.sqrt(magnitude_ngram * magnitude_doc)
153
-
154
-
155
  def compute_ngram_similarity(ngram_list, ngram_embeddings, doc_embedding):
156
  ngram_similarity_dict = {}
157
 
@@ -211,7 +203,7 @@ def compute_filtered_text(annotator, title, text):
211
  if title is not None:
212
  annotated = annotator.annotate_text(title + '. ' + text)
213
  filtered_sentences = []
214
- keep_tags = ['N', 'Np', 'V']
215
  for key in annotated.keys():
216
  sent = ' '.join([dict_['wordForm'] for dict_ in annotated[key] if dict_['posTag'] in keep_tags])
217
  filtered_sentences.append(sent)
@@ -220,43 +212,22 @@ def compute_filtered_text(annotator, title, text):
220
 
221
  def get_candidate_ngrams(segmentised_doc, filtered_segmentised_doc, ngram_n, stopwords_ls):
222
  # get actual ngrams
223
- # segmentised_doc = get_segmentised_doc(nlp, annotator, title, text)
224
  actual_ngram_list = compute_ngram_list(segmentised_doc, ngram_n, stopwords_ls, subsentences=True)
225
 
226
  # get filtered ngrams
227
- # filtered_segmentised_doc = compute_filtered_text(annotator, title, text)
228
  filtered_ngram_list = compute_ngram_list(filtered_segmentised_doc, ngram_n, stopwords_ls,
229
  subsentences=False)
230
 
231
- # get candiate ngrams
232
  candidate_ngram = [ngram for ngram in filtered_ngram_list if ngram in actual_ngram_list]
233
  return candidate_ngram
234
 
235
 
236
- def limit_minimum_frequency(doc_segmentised, ngram_list, min_freq=1):
237
- ngram_dict_freq = {}
238
- for ngram in ngram_list:
239
- ngram_n = len(ngram.split())
240
- count = 0
241
- for sentence in doc_segmentised:
242
- sent = sentence.split()
243
- for i in range(len(sent) - ngram_n + 1):
244
- pair = ' '.join(sent[i:i + ngram_n])
245
- if pair == ngram:
246
- count += 1
247
- if count >= min_freq:
248
- ngram_dict_freq[ngram] = count
249
-
250
- return ngram_dict_freq
251
-
252
-
253
  def remove_overlapping_ngrams(ngram_list):
254
  to_remove = set()
255
  for ngram1 in ngram_list:
256
  for ngram2 in ngram_list:
257
  if len(ngram1.split()) > len(ngram2.split()) and (ngram1.startswith(ngram2) or ngram1.endswith(ngram2)):
258
- # print(ngram1, ngram2)
259
- # print()
260
  to_remove.add(ngram2)
261
 
262
  for kw in to_remove:
 
118
 
119
  if title is not None:
120
  segmentised_doc = rdrsegmenter.word_segment(title) + rdrsegmenter.word_segment(doc)
 
121
  ne_ls = set(get_named_entities(nlp, doc))
122
 
123
  segmentised_doc_ne = []
 
144
  return ngram_embeddings
145
 
146
 
 
 
 
 
 
 
 
147
  def compute_ngram_similarity(ngram_list, ngram_embeddings, doc_embedding):
148
  ngram_similarity_dict = {}
149
 
 
203
  if title is not None:
204
  annotated = annotator.annotate_text(title + '. ' + text)
205
  filtered_sentences = []
206
+ keep_tags = ['N', 'Np', 'V', 'Nc']
207
  for key in annotated.keys():
208
  sent = ' '.join([dict_['wordForm'] for dict_ in annotated[key] if dict_['posTag'] in keep_tags])
209
  filtered_sentences.append(sent)
 
212
 
213
  def get_candidate_ngrams(segmentised_doc, filtered_segmentised_doc, ngram_n, stopwords_ls):
214
  # get actual ngrams
 
215
  actual_ngram_list = compute_ngram_list(segmentised_doc, ngram_n, stopwords_ls, subsentences=True)
216
 
217
  # get filtered ngrams
 
218
  filtered_ngram_list = compute_ngram_list(filtered_segmentised_doc, ngram_n, stopwords_ls,
219
  subsentences=False)
220
 
221
+ # get candidate ngrams
222
  candidate_ngram = [ngram for ngram in filtered_ngram_list if ngram in actual_ngram_list]
223
  return candidate_ngram
224
 
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  def remove_overlapping_ngrams(ngram_list):
227
  to_remove = set()
228
  for ngram1 in ngram_list:
229
  for ngram2 in ngram_list:
230
  if len(ngram1.split()) > len(ngram2.split()) and (ngram1.startswith(ngram2) or ngram1.endswith(ngram2)):
 
 
231
  to_remove.add(ngram2)
232
 
233
  for kw in to_remove:
model/named_entities.py CHANGED
@@ -32,8 +32,6 @@ def get_named_entities(nlp, doc):
32
  if len(sent_ner_result) > 0:
33
  ner_lists += get_ner_phrases(sent_ner_result)
34
 
35
- # print(ner_lists)
36
-
37
  ner_list_non_dup = []
38
  for (entity, ner_type) in ner_lists:
39
  if entity not in ner_list_non_dup and ner_type.startswith('I'):
 
32
  if len(sent_ner_result) > 0:
33
  ner_lists += get_ner_phrases(sent_ner_result)
34
 
 
 
35
  ner_list_non_dup = []
36
  for (entity, ner_type) in ner_lists:
37
  if entity not in ner_list_non_dup and ner_type.startswith('I'):
pipeline.py CHANGED
@@ -13,19 +13,13 @@ class KeywordExtractorPipeline(Pipeline):
13
  super().__init__(model, **kwargs)
14
  self.annotator = py_vncorenlp.VnCoreNLP(annotators=["wseg", "pos"],
15
  save_dir=f'{dir_path}/pretrained-models/vncorenlp')
16
- # model = py_vncorenlp.VnCoreNLP(save_dir='/absolute/path/to/vncorenlp')
17
  print("Loading PhoBERT tokenizer")
18
  self.phobert_tokenizer = AutoTokenizer.from_pretrained("vinai/phobert-base-v2")
19
-
20
- # use absolute path because torch is cached
21
- # self.phobert = torch.load(f'{dir_path}/pretrained-models/phobert.pt')
22
- # self.phobert.eval()
23
  self.phobert = model
24
 
25
  print("Loading NER tokenizer")
26
  ner_tokenizer = AutoTokenizer.from_pretrained("NlpHUST/ner-vietnamese-electra-base")
27
- # ner_model = torch.load(f'{dir_path}/pretrained-models/ner-vietnamese-electra-base.pt')
28
- # ner_model.eval()
29
  self.ner_pipeline = pipeline("ner", model=ner_model, tokenizer=ner_tokenizer)
30
 
31
  stopwords_file_path = f'{dir_path}/vietnamese-stopwords-dash.txt'
@@ -41,7 +35,7 @@ class KeywordExtractorPipeline(Pipeline):
41
  if possible_preprocess_kwarg in kwargs:
42
  preprocess_kwargs[possible_preprocess_kwarg] = kwargs[possible_preprocess_kwarg]
43
 
44
- for possible_forward_kwarg in ["ngram_n", "min_freq"]:
45
  if possible_forward_kwarg in kwargs:
46
  forward_kwargs[possible_forward_kwarg] = kwargs[possible_forward_kwarg]
47
 
@@ -49,8 +43,6 @@ class KeywordExtractorPipeline(Pipeline):
49
  if possible_postprocess_kwarg in kwargs:
50
  postprocess_kwargs[possible_postprocess_kwarg] = kwargs[possible_postprocess_kwarg]
51
 
52
- # print(forward_kwargs)
53
- # print(postprocess_kwargs)
54
  return preprocess_kwargs, forward_kwargs, postprocess_kwargs
55
 
56
  def preprocess(self, inputs):
@@ -58,24 +50,22 @@ class KeywordExtractorPipeline(Pipeline):
58
  if inputs['title']:
59
  title = process_text_pipeline(inputs['title'])
60
  text = process_text_pipeline(inputs['text'])
61
- return {"text": text, "title": title}#, "ngram_n": inputs["ngram_n"], "min_freq":inputs["min_freq"]}
62
 
63
- def _forward(self, model_inputs, ngram_n, min_freq):
64
  text = model_inputs['text']
65
  title = model_inputs['title']
66
- # ngram_n = model_inputs['ngram_n']
67
- # min_freq = model_inputs['min_freq']
68
 
69
  # Getting segmentised document
70
  ne_ls, doc_segmentised = get_segmentised_doc(self.ner_pipeline, self.annotator, title, text)
71
- print(ne_ls)
72
  filtered_doc_segmentised = compute_filtered_text(self.annotator, title, text)
73
 
74
  doc_embedding = get_doc_embeddings(filtered_doc_segmentised, self.phobert_tokenizer, self.phobert,
75
  self.stopwords)
76
 
77
- ngram_list = self.generate_ngram_list(doc_segmentised, filtered_doc_segmentised, ne_ls, ngram_n, min_freq)
78
- print(sorted(ngram_list))
 
79
 
80
  ngram_embeddings = compute_ngram_embeddings(self.phobert_tokenizer, self.phobert, ngram_list)
81
 
@@ -94,7 +84,7 @@ class KeywordExtractorPipeline(Pipeline):
94
  return diversify_result_kmeans(ngram_result, ngram_embeddings, top_n=top_n)
95
  return non_diversified
96
 
97
- def generate_ngram_list(self, doc_segmentised, filtered_doc_segmentised, ne_ls, ngram_n, min_freq):
98
  ngram_low, ngram_high = ngram_n
99
 
100
  # Adding ngram
@@ -103,14 +93,13 @@ class KeywordExtractorPipeline(Pipeline):
103
  ngram_list.update(get_candidate_ngrams(doc_segmentised, filtered_doc_segmentised, n, self.stopwords))
104
 
105
  # Adding named entities ngram list
106
- ngram_list.update([self.annotator.word_segment(ne)[0] for ne in ne_ls])
 
107
 
108
  # Removing overlapping ngrams
109
  ngram_list = remove_overlapping_ngrams(ngram_list)
110
 
111
- # Limit ngrams by minimum frequency
112
- ngram_list = limit_minimum_frequency(doc_segmentised, ngram_list, min_freq=min_freq)
113
- return ngram_list.keys()
114
 
115
  def extract_keywords(self, doc_embedding, ngram_list, ngram_embeddings):
116
  ngram_result = compute_ngram_similarity(ngram_list, ngram_embeddings, doc_embedding)
@@ -119,17 +108,19 @@ class KeywordExtractorPipeline(Pipeline):
119
 
120
 
121
  if __name__ == "__main__":
122
-
123
  phobert = torch.load(f'{dir_path}/pretrained-models/phobert.pt')
124
  phobert.eval()
125
  ner_model = torch.load(f'{dir_path}/pretrained-models/ner-vietnamese-electra-base.pt')
126
  ner_model.eval()
127
  kw_pipeline = KeywordExtractorPipeline(phobert, ner_model)
128
 
129
- text_file_path = f'{dir_path}/test_file.txt'
130
- with open(text_file_path, 'r') as f:
131
- text = ' '.join([ln.strip() for ln in f.readlines()])
 
 
 
132
 
133
- inp = {"text": text,"title": None}
134
- kws = kw_pipeline(inputs=inp, min_freq=1, ngram_n=(1,3), top_n=5, diversify_result=False)
135
  print(kws)
 
13
  super().__init__(model, **kwargs)
14
  self.annotator = py_vncorenlp.VnCoreNLP(annotators=["wseg", "pos"],
15
  save_dir=f'{dir_path}/pretrained-models/vncorenlp')
16
+
17
  print("Loading PhoBERT tokenizer")
18
  self.phobert_tokenizer = AutoTokenizer.from_pretrained("vinai/phobert-base-v2")
 
 
 
 
19
  self.phobert = model
20
 
21
  print("Loading NER tokenizer")
22
  ner_tokenizer = AutoTokenizer.from_pretrained("NlpHUST/ner-vietnamese-electra-base")
 
 
23
  self.ner_pipeline = pipeline("ner", model=ner_model, tokenizer=ner_tokenizer)
24
 
25
  stopwords_file_path = f'{dir_path}/vietnamese-stopwords-dash.txt'
 
35
  if possible_preprocess_kwarg in kwargs:
36
  preprocess_kwargs[possible_preprocess_kwarg] = kwargs[possible_preprocess_kwarg]
37
 
38
+ for possible_forward_kwarg in ["ngram_n"]:
39
  if possible_forward_kwarg in kwargs:
40
  forward_kwargs[possible_forward_kwarg] = kwargs[possible_forward_kwarg]
41
 
 
43
  if possible_postprocess_kwarg in kwargs:
44
  postprocess_kwargs[possible_postprocess_kwarg] = kwargs[possible_postprocess_kwarg]
45
 
 
 
46
  return preprocess_kwargs, forward_kwargs, postprocess_kwargs
47
 
48
  def preprocess(self, inputs):
 
50
  if inputs['title']:
51
  title = process_text_pipeline(inputs['title'])
52
  text = process_text_pipeline(inputs['text'])
53
+ return {"text": text, "title": title}
54
 
55
+ def _forward(self, model_inputs, ngram_n):
56
  text = model_inputs['text']
57
  title = model_inputs['title']
 
 
58
 
59
  # Getting segmentised document
60
  ne_ls, doc_segmentised = get_segmentised_doc(self.ner_pipeline, self.annotator, title, text)
 
61
  filtered_doc_segmentised = compute_filtered_text(self.annotator, title, text)
62
 
63
  doc_embedding = get_doc_embeddings(filtered_doc_segmentised, self.phobert_tokenizer, self.phobert,
64
  self.stopwords)
65
 
66
+ ngram_list = self.generate_ngram_list(doc_segmentised, filtered_doc_segmentised, ne_ls, ngram_n)
67
+ # print("Final ngram list")
68
+ # print(sorted(ngram_list))
69
 
70
  ngram_embeddings = compute_ngram_embeddings(self.phobert_tokenizer, self.phobert, ngram_list)
71
 
 
84
  return diversify_result_kmeans(ngram_result, ngram_embeddings, top_n=top_n)
85
  return non_diversified
86
 
87
+ def generate_ngram_list(self, doc_segmentised, filtered_doc_segmentised, ne_ls, ngram_n):
88
  ngram_low, ngram_high = ngram_n
89
 
90
  # Adding ngram
 
93
  ngram_list.update(get_candidate_ngrams(doc_segmentised, filtered_doc_segmentised, n, self.stopwords))
94
 
95
  # Adding named entities ngram list
96
+ ne_ls_segmented = [self.annotator.word_segment(ne)[0] for ne in ne_ls]
97
+ ngram_list.update(ne_ls_segmented)
98
 
99
  # Removing overlapping ngrams
100
  ngram_list = remove_overlapping_ngrams(ngram_list)
101
 
102
+ return ngram_list
 
 
103
 
104
  def extract_keywords(self, doc_embedding, ngram_list, ngram_embeddings):
105
  ngram_result = compute_ngram_similarity(ngram_list, ngram_embeddings, doc_embedding)
 
108
 
109
 
110
  if __name__ == "__main__":
 
111
  phobert = torch.load(f'{dir_path}/pretrained-models/phobert.pt')
112
  phobert.eval()
113
  ner_model = torch.load(f'{dir_path}/pretrained-models/ner-vietnamese-electra-base.pt')
114
  ner_model.eval()
115
  kw_pipeline = KeywordExtractorPipeline(phobert, ner_model)
116
 
117
+ title = "Truyền thuyết và hiện tại Thành Cổ Loa"
118
+ text = """
119
+ Nhắc đến Cổ Loa, người ta nghĩ ngay đến truyền thuyết về An Dương Vương được thần Kim Quy bày cho cách xây thành, về chiếc lẫy nỏ thần làm từ móng chân rùa thần và mối tình bi thương Mỵ Châu – Trọng Thủy. Đằng sau những câu chuyện thiên về tâm linh ấy, thế hệ con cháu còn khám phá được những giá trị khảo cổ to lớn của Cổ Loa.
120
+ Khu di tích Cổ Loa cách trung – tâm Hà Nội 17km thuộc huyện Đông Anh, Hà Nội, có diện tích bảo tồn gần 500ha được coi là địa chỉ văn hóa đặc biệt của thủ đô và cả nước. Cổ Loa có hàng loạt di chỉ khảo cổ học đã được phát hiện, phản ánh quá trình phát triển liên tục của dân tộc ta từ sơ khai qua các thời kỳ đồ đồng, đồ đá và đồ sắt mà đỉnh cao là văn hóa Đông Sơn, vẫn được coi là nền văn minh sông Hồng thời kỳ tiền sử của dân tộc Việt Nam.
121
+ Cổ Loa từng là kinh đô của nhà nước Âu Lạc thời kỳ An Dương Vương (thế kỷ III TCN) và của nước Đại Việt thời Ngô Quyền (thế kỷ X) mà thành Cổ Loa là một di tích minh chứng còn lại cho đến ngày nay. Thành Cổ Loa được các nhà khảo cổ học đánh giá là “tòa thành cổ nhất, quy mô lớn vào bậc nhất, cấu trúc cũng thuộc loại độc đáo nhất trong lịch sử xây dựng thành lũy của người Việt cổ”.
122
+ """
123
 
124
+ inp = {"text": text, "title": title}
125
+ kws = kw_pipeline(inputs=inp, ngram_n=(1, 3), top_n=5, diversify_result=False)
126
  print(kws)
requirements.txt CHANGED
@@ -4,4 +4,4 @@ gradio
4
  scikit-learn
5
  numpy
6
  underthesea
7
- py_vncorenlp
 
4
  scikit-learn
5
  numpy
6
  underthesea
7
+ py_vncorenlp