stream2sentence 0.2.2__tar.gz → 0.2.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stream2sentence
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Real-time processing and delivery of sentences from a continuous stream of characters or text chunks.
5
5
  Home-page: https://github.com/KoljaB/stream2sentence
6
6
  Author: Kolja Beigel
@@ -19,6 +19,8 @@ Requires-Dist: stanza==1.6.1
19
19
 
20
20
  Real-time processing and delivery of sentences from a continuous stream of characters or text chunks.
21
21
 
22
+ > **Hint:** *If you're interested in state-of-the-art voice solutions you might also want to <strong>have a look at [Linguflex](https://github.com/KoljaB/Linguflex)</strong>, the original project from which stream2sentence is spun off. It lets you control your environment by speaking and is one of the most capable and sophisticated open-source assistants currently available.*
23
+
22
24
  ## Table of Contents
23
25
 
24
26
  - [Features](#features)
@@ -76,9 +78,6 @@ The `generate_sentences()` function has the following parameters:
76
78
  - `generator`: Input character generator.
77
79
  Iterator that emits chunks of text. These chunks can be of any size, and they'll be processed one by one to extract sentences from them. It forms the primary source from which the function reads and generates sentences.
78
80
 
79
- - `quick_yield_single_sentence_fragment`: Whether to return a sentence fragment as fast as possible.
80
- This is a feature for realtime speech synthesis. In some use cases you want to audio stream a minimal chunk of text as fast as possible, even when it means to synthesize mid-sentence. In this case you set this flag to True which will yield a synthesizable sentence fragment as early as possible.
81
-
82
81
  - `context_size`: Context size for sentence detection.
83
82
  This controls how much context is looked at to detect sentence boundaries. It determines the number of characters around a potential delimiter (like a period) that are considered when detecting sentence boundaries. A larger context size allows more reliable sentence boundary detection, but requires buffering more characters before emitting a sentence.
84
83
  Default is 12 characters. Increasing this can help detect sentences more accurately, at the cost of added latency.
@@ -103,6 +102,18 @@ The `generate_sentences()` function has the following parameters:
103
102
  If True, any Unicode emoji characters are identified and removed from the emitted output sentences. This can help to clean up the output.
104
103
  Default is False. Set to True if emojis are not required in the output.
105
104
 
105
+ - `tokenize_sentences`: (Optional) Function for sentence tokenization. Default is None.
106
+
107
+ - `tokenizer`: Specifies the tokenizer to use ('nltk' or 'stanza'). Default is 'nltk'.
108
+
109
+ - `language`: Language setting for the tokenizer ('en' or 'multilingual' for stanza). Default is 'en'.
110
+
111
+ - `log_characters`: Logs each processed character to the console for debugging. Default is False.
112
+
113
+ - `sentence_fragment_delimiters`: Characters considered as sentence delimiters for yielding quick fragment.
114
+
115
+ - `force_first_fragment_after_words`: Forces the first sentence fragment to yield after a specified number of words. Default is 10 words.
116
+
106
117
  - `log_characters`: Option to log characters to the console.
107
118
  When enabled, each character processed by the function is printed to the console. This is mainly for debugging purposes to observe the flow of characters through the function.
108
119
  Default is False. Set to True for a visual representation of characters being processed. Example: allows printing llm output to console when using stream2sentence to prepare input generation for text to speech synthesis.
@@ -2,6 +2,8 @@
2
2
 
3
3
  Real-time processing and delivery of sentences from a continuous stream of characters or text chunks.
4
4
 
5
+ > **Hint:** *If you're interested in state-of-the-art voice solutions you might also want to <strong>have a look at [Linguflex](https://github.com/KoljaB/Linguflex)</strong>, the original project from which stream2sentence is spun off. It lets you control your environment by speaking and is one of the most capable and sophisticated open-source assistants currently available.*
6
+
5
7
  ## Table of Contents
6
8
 
7
9
  - [Features](#features)
@@ -59,9 +61,6 @@ The `generate_sentences()` function has the following parameters:
59
61
  - `generator`: Input character generator.
60
62
  Iterator that emits chunks of text. These chunks can be of any size, and they'll be processed one by one to extract sentences from them. It forms the primary source from which the function reads and generates sentences.
61
63
 
62
- - `quick_yield_single_sentence_fragment`: Whether to return a sentence fragment as fast as possible.
63
- This is a feature for realtime speech synthesis. In some use cases you want to audio stream a minimal chunk of text as fast as possible, even when it means to synthesize mid-sentence. In this case you set this flag to True which will yield a synthesizable sentence fragment as early as possible.
64
-
65
64
  - `context_size`: Context size for sentence detection.
66
65
  This controls how much context is looked at to detect sentence boundaries. It determines the number of characters around a potential delimiter (like a period) that are considered when detecting sentence boundaries. A larger context size allows more reliable sentence boundary detection, but requires buffering more characters before emitting a sentence.
67
66
  Default is 12 characters. Increasing this can help detect sentences more accurately, at the cost of added latency.
@@ -86,6 +85,18 @@ The `generate_sentences()` function has the following parameters:
86
85
  If True, any Unicode emoji characters are identified and removed from the emitted output sentences. This can help to clean up the output.
87
86
  Default is False. Set to True if emojis are not required in the output.
88
87
 
88
+ - `tokenize_sentences`: (Optional) Function for sentence tokenization. Default is None.
89
+
90
+ - `tokenizer`: Specifies the tokenizer to use ('nltk' or 'stanza'). Default is 'nltk'.
91
+
92
+ - `language`: Language setting for the tokenizer ('en' or 'multilingual' for stanza). Default is 'en'.
93
+
94
+ - `log_characters`: Logs each processed character to the console for debugging. Default is False.
95
+
96
+ - `sentence_fragment_delimiters`: Characters considered as sentence delimiters for yielding quick fragment.
97
+
98
+ - `force_first_fragment_after_words`: Forces the first sentence fragment to yield after a specified number of words. Default is 10 words.
99
+
89
100
  - `log_characters`: Option to log characters to the console.
90
101
  When enabled, each character processed by the function is printed to the console. This is mainly for debugging purposes to observe the flow of characters through the function.
91
102
  Default is False. Set to True for a visual representation of characters being processed. Example: allows printing llm output to console when using stream2sentence to prepare input generation for text to speech synthesis.
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="stream2sentence",
8
- version="0.2.2",
8
+ version="0.2.4",
9
9
  author="Kolja Beigel",
10
10
  author_email="kolja.beigel@web.de",
11
11
  description="Real-time processing and delivery of sentences from a continuous stream of characters or text chunks.",
@@ -0,0 +1,427 @@
1
+ """
2
+ Real-time processing and delivery of sentences
3
+ from a continuous stream of characters or text chunks
4
+ """
5
+
6
+ from typing import Iterator
7
+ import logging
8
+ import emoji
9
+ import time
10
+ import re
11
+
12
+ current_tokenizer = "nltk"
13
+ stanza_initialized = False
14
+ nltk_initialized = False
15
+ nlp = None
16
+ debug_logging = True
17
+
18
+
19
+ def initialize_nltk():
20
+ """
21
+ Initializes NLTK by downloading required data for sentence tokenization.
22
+ """
23
+ global nltk_initialized
24
+ if nltk_initialized:
25
+ return
26
+
27
+ logging.info("Initializing NLTK")
28
+
29
+ import nltk
30
+ try:
31
+ _ = nltk.data.find('tokenizers/punkt')
32
+ except LookupError:
33
+ nltk.download('punkt')
34
+ nltk_initialized = True
35
+
36
+
37
+ def initialize_stanza(language: str = "en"):
38
+ """
39
+ Initializes Stanza by downloading required data for sentence tokenization.
40
+ """
41
+ global nlp, stanza_initialized
42
+ if stanza_initialized:
43
+ return
44
+
45
+ logging.info("Initializing Stanza")
46
+
47
+ import stanza
48
+ stanza.download(language)
49
+ nlp = stanza.Pipeline(language)
50
+ stanza_initialized = True
51
+
52
+
53
+ def _remove_links(text: str) -> str:
54
+ """
55
+ Removes any links from the input text.
56
+
57
+ Args:
58
+ text (str): Input text
59
+
60
+ Returns:
61
+ str: Text with links removed
62
+ """
63
+ pattern = (
64
+ r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|'
65
+ r'[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
66
+ )
67
+
68
+ return re.sub(pattern, '', text)
69
+
70
+
71
+ def _remove_emojis(text: str) -> str:
72
+ """
73
+ Removes emojis from the input text.
74
+
75
+ Args:
76
+ text (str): Input text
77
+
78
+ Returns:
79
+ str: Text with emojis removed
80
+ """
81
+ return emoji.replace_emoji(text, u'')
82
+
83
+
84
+ def _generate_characters(generator: Iterator[str],
85
+ log_characters: bool = False) -> Iterator[str]:
86
+ """
87
+ Generates individual characters from a text generator.
88
+
89
+ Args:
90
+ generator (Iterator[str]): Input text generator
91
+ log_characters (bool): Whether to log the characters to the console
92
+
93
+ Yields:
94
+ Individual characters from the generator
95
+ """
96
+ if log_characters:
97
+ print("Stream: ", end="", flush=True)
98
+ for chunk in generator:
99
+ for char in chunk:
100
+ if log_characters:
101
+ print(char, end="", flush=True)
102
+ yield char
103
+ if log_characters:
104
+ print()
105
+
106
+
107
+ def _clean_text(text: str,
108
+ cleanup_text_links: bool = False,
109
+ cleanup_text_emojis: bool = False,
110
+ strip_text: bool = True) -> str:
111
+ """
112
+ Cleans the text by removing links and emojis.
113
+
114
+ Args:
115
+ text (str): Input text
116
+ cleanup_text_links (boolean, optional): Remove non-desired links from
117
+ the stream.
118
+ cleanup_text_emojis (boolean, optional): Remove non-desired emojis
119
+ from the stream.
120
+
121
+ Returns:
122
+ str: Cleaned text
123
+ """
124
+ if cleanup_text_links:
125
+ text = _remove_links(text)
126
+ if cleanup_text_emojis:
127
+ text = _remove_emojis(text)
128
+ if strip_text:
129
+ text = text.strip()
130
+ return text
131
+
132
+
133
+ def _tokenize_sentences(text: str, tokenize_sentences=None) -> Iterator[str]:
134
+ """
135
+ Tokenizes sentences from the input text.
136
+
137
+ Args:
138
+ text (str): Input text
139
+ tokenize_sentences (Callable, optional): A function that tokenizes
140
+ sentences from the input text. Defaults to None.
141
+
142
+ Yields:
143
+ Iterator[str]: An iterator of sentences
144
+ """
145
+ if tokenize_sentences:
146
+ sentences = tokenize_sentences(text)
147
+ else:
148
+ nlp_start_time = time.time()
149
+ if current_tokenizer == "nltk":
150
+ import nltk
151
+ sentences = nltk.tokenize.sent_tokenize(text)
152
+ elif current_tokenizer == "stanza":
153
+ import stanza
154
+ global nlp
155
+ doc = nlp(text)
156
+ sentences = [sentence.text for sentence in doc.sentences]
157
+ else:
158
+ raise ValueError(f"Unknown tokenizer: {current_tokenizer}")
159
+ nlp_end_time = time.time()
160
+ logging.info("Time to split sentences: "
161
+ f"{nlp_end_time - nlp_start_time}")
162
+ return sentences
163
+
164
+
165
+ def init_tokenizer(tokenizer: str, language: str = "en"):
166
+ """
167
+ Initializes the sentence tokenizer.
168
+ """
169
+ if tokenizer == "nltk":
170
+ initialize_nltk()
171
+ elif tokenizer == "stanza":
172
+ initialize_stanza(language)
173
+ else:
174
+ logging.warning(f"Unknown tokenizer: {tokenizer}")
175
+
176
+
177
+ def generate_sentences(generator: Iterator[str],
178
+ context_size: int = 12,
179
+ minimum_sentence_length: int = 10,
180
+ minimum_first_fragment_length=10,
181
+ quick_yield_single_sentence_fragment: bool = False,
182
+ cleanup_text_links: bool = False,
183
+ cleanup_text_emojis: bool = False,
184
+ tokenize_sentences=None,
185
+ tokenizer: str = "nltk",
186
+ language: str = "en",
187
+ log_characters: bool = False,
188
+ sentence_fragment_delimiters: str = ".?!;:,\n…)]}。-",
189
+ force_first_fragment_after_words=15,
190
+ ) -> Iterator[str]:
191
+ """
192
+ Generates well-formed sentences from a stream of characters or text chunks
193
+ provided by an input generator.
194
+
195
+ Args:
196
+ generator (Iterator[str]): A generator that yields chunks of text as a
197
+ stream of characters.
198
+ context_size (int): The number of characters used to establish context
199
+ for sentence boundary detection. A larger context improves the
200
+ accuracy of detecting sentence boundaries.
201
+ Default is 12 characters.
202
+ minimum_sentence_length (int): The minimum number of characters a
203
+ sentence must have. If a sentence is shorter, it will be
204
+ concatenated with the following one, improving the overall
205
+ readability. This parameter does not apply to the first sentence
206
+ fragment, which is governed by `minimum_first_fragment_length`.
207
+ Default is 10 characters.
208
+ minimum_first_fragment_length (int): The minimum number of characters
209
+ required for the first sentence fragment before yielding.
210
+ Default is 10 characters.
211
+ quick_yield_single_sentence_fragment (bool): If set to True, the
212
+ generator will yield the first sentence fragment as quickly as
213
+ possible. This is particularly useful for real-time applications
214
+ such as speech synthesis.
215
+ cleanup_text_links (bool): If True, removes hyperlinks from the text
216
+ stream to ensure clean output.
217
+ cleanup_text_emojis (bool): If True, filters out emojis from the text
218
+ stream for clear textual content.
219
+ tokenize_sentences (Callable): A function that tokenizes sentences
220
+ from the input text. Defaults to None.
221
+ tokenizer (str): The tokenizer to use for sentence tokenization.
222
+ Default is "nltk". Can be "nltk" or "stanza".
223
+ language (str): The language to use for sentence tokenization.
224
+ Default is "en". Can be "multilingual" for stanze tokenizer.
225
+ log_characters (bool): If True, logs each character to the console as
226
+ they are processed.
227
+ sentence_fragment_delimiters (str): A string of characters that are
228
+ considered sentence delimiters. Default is ".?!;:,\n…)]}。-".
229
+ force_first_fragment_after_words (int): The number of words after
230
+ which the first sentence fragment is forced to be yielded.
231
+ Default is 15 words.
232
+
233
+ Yields:
234
+ Iterator[str]: An iterator of complete sentences constructed from the
235
+ input text stream. Each yielded sentence meets the specified minimum
236
+ length requirements and is cleaned up if specified.
237
+
238
+ The function maintains a buffer to accumulate text chunks and applies
239
+ natural language processing to detect sentence boundaries.
240
+ It employs various heuristics, such as minimum sentence length and
241
+ sentence delimiters, to ensure the quality of the output sentences.
242
+ The function also provides options to clean up the text stream,
243
+ making it versatile for different types of text processing applications.
244
+ """
245
+
246
+ global current_tokenizer
247
+ current_tokenizer = tokenizer
248
+ init_tokenizer(current_tokenizer, language)
249
+
250
+ buffer = ''
251
+ is_first_sentence = True
252
+ word_count = 0 # Initialize word count
253
+
254
+ for char in _generate_characters(generator, log_characters):
255
+
256
+ if char:
257
+ buffer += char
258
+ buffer = buffer.lstrip()
259
+
260
+ # Update word count
261
+ if char.isspace() or char in sentence_fragment_delimiters:
262
+ word_count += 1
263
+
264
+ print(f"DBG # buffer: {buffer}")
265
+
266
+ if (is_first_sentence
267
+ and len(buffer) > minimum_first_fragment_length
268
+ and quick_yield_single_sentence_fragment):
269
+
270
+ if (buffer[-1] in sentence_fragment_delimiters
271
+ or word_count >= force_first_fragment_after_words):
272
+
273
+ yield_text = _clean_text(
274
+ buffer,
275
+ cleanup_text_links,
276
+ cleanup_text_emojis)
277
+ if debug_logging:
278
+ print(f"s2s: yield 1st {yield_text}, set buffer='', is_first_sentence = False")
279
+ yield yield_text
280
+ buffer = ""
281
+ is_first_sentence = False
282
+ continue
283
+
284
+ # Check if minimum length reached
285
+ if len(buffer) <= minimum_sentence_length + context_size:
286
+ continue
287
+
288
+ # Potential delimiter character has to be a bit away from the end
289
+ # of the buffer. For reliable sentence detection the engine needs
290
+ # enough context to work with
291
+ delimiter_char = buffer[-context_size]
292
+
293
+ if delimiter_char in sentence_fragment_delimiters:
294
+ sentences = _tokenize_sentences(buffer, tokenize_sentences)
295
+ if debug_logging:
296
+ print(f"s2s: sentences tokenizer {sentences}")
297
+ if len(sentences) > 1:
298
+ total_length_except_first = sum(len(sentence) for sentence in sentences[1:])
299
+ if total_length_except_first >= context_size:
300
+ for sentence in sentences[:-1]:
301
+ yield sentence
302
+ #last_sentence = sentences[-1]
303
+ buffer = sentences[-1]
304
+
305
+ # total_length_except_last = sum(len(sentence) for sentence in sentences[:-1])
306
+ # print(f"DBG # total_length_except_last: {total_length_except_last}, len(buffer): {len(buffer)}, context_size: {context_size}")
307
+ # print(f"DBG # total_length_except_last: {total_length_except_last}, len(buffer) - context_size + 1: {len(buffer) - context_size + 1}")
308
+
309
+ # # print(f"DBG # len(sentences[0]): {len(sentences[0])}, len(buffer): {len(buffer)}, context_size: {context_size}")
310
+ # # print(f"DBG # len(sentences[0]): {len(sentences[0])}, len(buffer) - context_size + 1: {len(buffer) - context_size + 1}")
311
+ # if total_length_except_last == len(buffer) - context_size + 1:
312
+ # if debug_logging:
313
+ # print(f"s2s: yield tokenizer {buffer[:-context_size + 1]}")
314
+ # yield_text = _clean_text(
315
+ # buffer[:-context_size + 1],
316
+ # cleanup_text_links,
317
+ # cleanup_text_emojis)
318
+ # yield yield_text
319
+ # buffer = buffer[-context_size + 1:]
320
+ # if debug_logging:
321
+ # print(f"s2s: AFTER yield tokenizer buffer {buffer}")
322
+ # is_first_sentence = False
323
+
324
+ # Yield remaining buffer
325
+ if buffer:
326
+ sentences = _tokenize_sentences(buffer, tokenize_sentences)
327
+ sentence_buffer = ""
328
+
329
+ for sentence in sentences:
330
+ sentence_buffer += sentence
331
+ if len(sentence_buffer) < minimum_sentence_length:
332
+ sentence_buffer += " "
333
+ continue
334
+ yield_text = _clean_text(
335
+ sentence_buffer,
336
+ cleanup_text_links,
337
+ cleanup_text_emojis)
338
+ if debug_logging:
339
+ print(f"s2s: yield remaining {yield_text}, set sentence_buffer=''")
340
+ yield yield_text
341
+
342
+ sentence_buffer = ""
343
+
344
+ if sentence_buffer:
345
+ yield_text = _clean_text(
346
+ sentence_buffer,
347
+ cleanup_text_links,
348
+ cleanup_text_emojis)
349
+ if debug_logging:
350
+ print(f"s2s: yield last sentence_buffer {yield_text}")
351
+ yield yield_text
352
+
353
+
354
+ # def generate_sentences(generator: Iterator[str],
355
+ # context_size: int = 12,
356
+ # minimum_sentence_length: int = 10,
357
+ # minimum_first_fragment_length=10,
358
+ # quick_yield_single_sentence_fragment: bool = False,
359
+ # cleanup_text_links: bool = False,
360
+ # cleanup_text_emojis: bool = False,
361
+ # tokenize_sentences=None,
362
+ # tokenizer: str = "nltk",
363
+ # language: str = "en",
364
+ # log_characters: bool = False,
365
+ # sentence_fragment_delimiters: str = ".?!;:,\n…)]}。-",
366
+ # force_first_fragment_after_words=15,
367
+ # ) -> Iterator[str]:
368
+ # global current_tokenizer
369
+ # current_tokenizer = tokenizer
370
+ # init_tokenizer(current_tokenizer, language)
371
+
372
+ # buffer = ''
373
+ # is_first_sentence = True
374
+ # word_count = 0
375
+
376
+ # def yield_sentence(text):
377
+ # return _clean_text(text, cleanup_text_links, cleanup_text_emojis)
378
+
379
+ # for char in _generate_characters(generator, log_characters):
380
+ # if char:
381
+ # buffer += char
382
+ # buffer = buffer.lstrip()
383
+
384
+ # if char.isspace() or char in sentence_fragment_delimiters:
385
+ # word_count += 1
386
+
387
+ # # Handle first sentence fragment
388
+ # if is_first_sentence and len(buffer) >= minimum_first_fragment_length:
389
+ # if quick_yield_single_sentence_fragment:
390
+ # if (buffer[-1] in sentence_fragment_delimiters
391
+ # or word_count >= force_first_fragment_after_words):
392
+ # yield yield_sentence(buffer)
393
+ # buffer = ""
394
+ # is_first_sentence = False
395
+ # word_count = 0
396
+ # continue
397
+ # elif len(buffer) >= minimum_sentence_length:
398
+ # sentences = _tokenize_sentences(buffer, tokenize_sentences)
399
+ # if sentences:
400
+ # yield yield_sentence(sentences[0])
401
+ # buffer = buffer[len(sentences[0]):].lstrip()
402
+ # is_first_sentence = False
403
+ # word_count = 0
404
+ # continue
405
+
406
+ # # Process remaining text
407
+ # if len(buffer) > context_size:
408
+ # sentences = _tokenize_sentences(buffer[:-context_size], tokenize_sentences)
409
+ # if sentences:
410
+ # complete_sentences = sentences[:-1]
411
+ # for sentence in complete_sentences:
412
+ # if len(sentence) >= minimum_sentence_length:
413
+ # yield yield_sentence(sentence)
414
+ # else:
415
+ # buffer = sentence + ' ' + buffer[len(sentence):].lstrip()
416
+ # break
417
+ # else:
418
+ # buffer = sentences[-1] + buffer[-context_size:]
419
+
420
+ # # Handle remaining buffer
421
+ # if buffer:
422
+ # sentences = _tokenize_sentences(buffer, tokenize_sentences)
423
+ # for sentence in sentences:
424
+ # if len(sentence) >= minimum_sentence_length or sentence == buffer:
425
+ # yield yield_sentence(sentence)
426
+ # elif sentences[-1] == sentence:
427
+ # yield yield_sentence(sentence)
@@ -1,5 +1,6 @@
1
1
  """
2
- Real-time processing and delivery of sentences from a continuous stream of characters or text chunks
2
+ Real-time processing and delivery of sentences
3
+ from a continuous stream of characters or text chunks
3
4
  """
4
5
 
5
6
  from typing import Iterator
@@ -13,6 +14,7 @@ stanza_initialized = False
13
14
  nltk_initialized = False
14
15
  nlp = None
15
16
 
17
+
16
18
  def initialize_nltk():
17
19
  """
18
20
  Initializes NLTK by downloading required data for sentence tokenization.
@@ -20,16 +22,17 @@ def initialize_nltk():
20
22
  global nltk_initialized
21
23
  if nltk_initialized:
22
24
  return
23
-
25
+
24
26
  logging.info("Initializing NLTK")
25
27
 
26
28
  import nltk
27
29
  try:
28
- _ = nltk.data.find('tokenizers/punkt')
30
+ _ = nltk.data.find('tokenizers/punkt')
29
31
  except LookupError:
30
32
  nltk.download('punkt')
31
33
  nltk_initialized = True
32
34
 
35
+
33
36
  def initialize_stanza(language: str = "en"):
34
37
  """
35
38
  Initializes Stanza by downloading required data for sentence tokenization.
@@ -37,7 +40,7 @@ def initialize_stanza(language: str = "en"):
37
40
  global nlp, stanza_initialized
38
41
  if stanza_initialized:
39
42
  return
40
-
43
+
41
44
  logging.info("Initializing Stanza")
42
45
 
43
46
  import stanza
@@ -45,6 +48,7 @@ def initialize_stanza(language: str = "en"):
45
48
  nlp = stanza.Pipeline(language)
46
49
  stanza_initialized = True
47
50
 
51
+
48
52
  def _remove_links(text: str) -> str:
49
53
  """
50
54
  Removes any links from the input text.
@@ -55,22 +59,28 @@ def _remove_links(text: str) -> str:
55
59
  Returns:
56
60
  str: Text with links removed
57
61
  """
58
- pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
62
+ pattern = (
63
+ r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|'
64
+ r'[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
65
+ )
66
+
59
67
  return re.sub(pattern, '', text)
60
68
 
69
+
61
70
  def _remove_emojis(text: str) -> str:
62
71
  """
63
72
  Removes emojis from the input text.
64
73
 
65
74
  Args:
66
75
  text (str): Input text
67
-
76
+
68
77
  Returns:
69
78
  str: Text with emojis removed
70
79
  """
71
80
  return emoji.replace_emoji(text, u'')
72
81
 
73
- def _generate_characters(generator: Iterator[str],
82
+
83
+ def _generate_characters(generator: Iterator[str],
74
84
  log_characters: bool = False) -> Iterator[str]:
75
85
  """
76
86
  Generates individual characters from a text generator.
@@ -82,14 +92,19 @@ def _generate_characters(generator: Iterator[str],
82
92
  Yields:
83
93
  Individual characters from the generator
84
94
  """
95
+ if log_characters:
96
+ print("Stream: ", end="", flush=True)
85
97
  for chunk in generator:
86
98
  for char in chunk:
87
99
  if log_characters:
88
100
  print(char, end="", flush=True)
89
101
  yield char
90
-
91
- def _clean_text(text: str,
92
- cleanup_text_links: bool = False,
102
+ if log_characters:
103
+ print()
104
+
105
+
106
+ def _clean_text(text: str,
107
+ cleanup_text_links: bool = False,
93
108
  cleanup_text_emojis: bool = False,
94
109
  strip_text: bool = True) -> str:
95
110
  """
@@ -97,9 +112,11 @@ def _clean_text(text: str,
97
112
 
98
113
  Args:
99
114
  text (str): Input text
100
- cleanup_text_links (boolean, optional): Remove non-desired links from the stream.
101
- cleanup_text_emojis (boolean, optional): Remove non-desired emojis from the stream.
102
-
115
+ cleanup_text_links (boolean, optional): Remove non-desired links from
116
+ the stream.
117
+ cleanup_text_emojis (boolean, optional): Remove non-desired emojis
118
+ from the stream.
119
+
103
120
  Returns:
104
121
  str: Cleaned text
105
122
  """
@@ -108,19 +125,21 @@ def _clean_text(text: str,
108
125
  if cleanup_text_emojis:
109
126
  text = _remove_emojis(text)
110
127
  if strip_text:
111
- text = text.strip()
128
+ text = text.strip()
112
129
  return text
113
130
 
114
- def _tokenize_sentences(text: str, tokenize_sentences = None) -> Iterator[str]:
131
+
132
+ def _tokenize_sentences(text: str, tokenize_sentences=None) -> Iterator[str]:
115
133
  """
116
134
  Tokenizes sentences from the input text.
117
135
 
118
136
  Args:
119
137
  text (str): Input text
120
- tokenize_sentences (Callable, optional): A function that tokenizes sentences from the input text. Defaults to None.
138
+ tokenize_sentences (Callable, optional): A function that tokenizes
139
+ sentences from the input text. Defaults to None.
121
140
 
122
141
  Yields:
123
- Iterator[str]: An iterator of sentences
142
+ Iterator[str]: An iterator of sentences
124
143
  """
125
144
  if tokenize_sentences:
126
145
  sentences = tokenize_sentences(text)
@@ -136,10 +155,12 @@ def _tokenize_sentences(text: str, tokenize_sentences = None) -> Iterator[str]:
136
155
  sentences = [sentence.text for sentence in doc.sentences]
137
156
  else:
138
157
  raise ValueError(f"Unknown tokenizer: {current_tokenizer}")
139
- nlp_end_time = time.time()
140
- logging.info(f"Time to split sentences: {nlp_end_time - nlp_start_time}")
158
+ nlp_end_time = time.time()
159
+ logging.info("Time to split sentences: "
160
+ f"{nlp_end_time - nlp_start_time}")
141
161
  return sentences
142
162
 
163
+
143
164
  def init_tokenizer(tokenizer: str, language: str = "en"):
144
165
  """
145
166
  Initializes the sentence tokenizer.
@@ -151,34 +172,74 @@ def init_tokenizer(tokenizer: str, language: str = "en"):
151
172
  else:
152
173
  logging.warning(f"Unknown tokenizer: {tokenizer}")
153
174
 
154
- def generate_sentences(generator: Iterator[str],
175
+
176
+ def generate_sentences(generator: Iterator[str],
155
177
  context_size: int = 12,
156
178
  minimum_sentence_length: int = 10,
157
- minimum_first_fragment_length = 10,
179
+ minimum_first_fragment_length=10,
158
180
  quick_yield_single_sentence_fragment: bool = False,
159
181
  cleanup_text_links: bool = False,
160
- cleanup_text_emojis: bool = False,
161
- tokenize_sentences = None,
182
+ cleanup_text_emojis: bool = False,
183
+ tokenize_sentences=None,
162
184
  tokenizer: str = "nltk",
163
185
  language: str = "en",
164
- log_characters: bool = False) -> Iterator[str]:
186
+ log_characters: bool = False,
187
+ sentence_fragment_delimiters: str = ".?!;:,\n…)]}。-",
188
+ force_first_fragment_after_words=15,
189
+ ) -> Iterator[str]:
165
190
  """
166
- Generates well-formed sentences from a stream of characters or text chunks provided by an input generator.
191
+ Generates well-formed sentences from a stream of characters or text chunks
192
+ provided by an input generator.
167
193
 
168
194
  Args:
169
- generator (Iterator[str]): A generator that yields chunks of text as a stream of characters.
170
- context_size (int): The number of characters used to establish context for sentence boundary detection. A larger context improves the accuracy of detecting sentence boundaries. Default is 12 characters.
171
- minimum_sentence_length (int): The minimum number of characters a sentence must have. If a sentence is shorter, it will be concatenated with the following one, improving the overall readability. This parameter does not apply to the first sentence fragment, which is governed by `minimum_first_fragment_length`. Default is 10 characters.
172
- minimum_first_fragment_length (int): The minimum number of characters required for the first sentence fragment before yielding. Default is 10 characters.
173
- quick_yield_single_sentence_fragment (bool): If set to True, the generator will yield the first sentence fragment as quickly as possible. This is particularly useful for real-time applications such as speech synthesis.
174
- cleanup_text_links (bool): If True, removes hyperlinks from the text stream to ensure clean output.
175
- cleanup_text_emojis (bool): If True, filters out emojis from the text stream for clear textual content.
176
- log_characters (bool): If True, logs each character to the console as they are processed.
195
+ generator (Iterator[str]): A generator that yields chunks of text as a
196
+ stream of characters.
197
+ context_size (int): The number of characters used to establish context
198
+ for sentence boundary detection. A larger context improves the
199
+ accuracy of detecting sentence boundaries.
200
+ Default is 12 characters.
201
+ minimum_sentence_length (int): The minimum number of characters a
202
+ sentence must have. If a sentence is shorter, it will be
203
+ concatenated with the following one, improving the overall
204
+ readability. This parameter does not apply to the first sentence
205
+ fragment, which is governed by `minimum_first_fragment_length`.
206
+ Default is 10 characters.
207
+ minimum_first_fragment_length (int): The minimum number of characters
208
+ required for the first sentence fragment before yielding.
209
+ Default is 10 characters.
210
+ quick_yield_single_sentence_fragment (bool): If set to True, the
211
+ generator will yield the first sentence fragment as quickly as
212
+ possible. This is particularly useful for real-time applications
213
+ such as speech synthesis.
214
+ cleanup_text_links (bool): If True, removes hyperlinks from the text
215
+ stream to ensure clean output.
216
+ cleanup_text_emojis (bool): If True, filters out emojis from the text
217
+ stream for clear textual content.
218
+ tokenize_sentences (Callable): A function that tokenizes sentences
219
+ from the input text. Defaults to None.
220
+ tokenizer (str): The tokenizer to use for sentence tokenization.
221
+ Default is "nltk". Can be "nltk" or "stanza".
222
+ language (str): The language to use for sentence tokenization.
223
+ Default is "en". Can be "multilingual" for stanze tokenizer.
224
+ log_characters (bool): If True, logs each character to the console as
225
+ they are processed.
226
+ sentence_fragment_delimiters (str): A string of characters that are
227
+ considered sentence delimiters. Default is ".?!;:,\n…)]}。-".
228
+ force_first_fragment_after_words (int): The number of words after
229
+ which the first sentence fragment is forced to be yielded.
230
+ Default is 15 words.
177
231
 
178
232
  Yields:
179
- Iterator[str]: An iterator of complete sentences constructed from the input text stream. Each yielded sentence meets the specified minimum length requirements and is cleaned up if specified.
180
-
181
- The function maintains a buffer to accumulate text chunks and applies natural language processing to detect sentence boundaries. It employs various heuristics, such as minimum sentence length and sentence delimiters, to ensure the quality of the output sentences. The function also provides options to clean up the text stream, making it versatile for different types of text processing applications.
233
+ Iterator[str]: An iterator of complete sentences constructed from the
234
+ input text stream. Each yielded sentence meets the specified minimum
235
+ length requirements and is cleaned up if specified.
236
+
237
+ The function maintains a buffer to accumulate text chunks and applies
238
+ natural language processing to detect sentence boundaries.
239
+ It employs various heuristics, such as minimum sentence length and
240
+ sentence delimiters, to ensure the quality of the output sentences.
241
+ The function also provides options to clean up the text stream,
242
+ making it versatile for different types of text processing applications.
182
243
  """
183
244
 
184
245
  global current_tokenizer
@@ -187,8 +248,7 @@ def generate_sentences(generator: Iterator[str],
187
248
 
188
249
  buffer = ''
189
250
  is_first_sentence = True
190
-
191
- sentence_delimiters = '.?!;:,\n…)]}。'
251
+ word_count = 0 # Initialize word count
192
252
 
193
253
  for char in _generate_characters(generator, log_characters):
194
254
 
@@ -196,10 +256,21 @@ def generate_sentences(generator: Iterator[str],
196
256
  buffer += char
197
257
  buffer = buffer.lstrip()
198
258
 
199
- if is_first_sentence and len(buffer) > minimum_first_fragment_length and quick_yield_single_sentence_fragment:
259
+ # Update word count
260
+ if char.isspace() or char in sentence_fragment_delimiters:
261
+ word_count += 1
262
+
263
+ if (is_first_sentence
264
+ and len(buffer) > minimum_first_fragment_length
265
+ and quick_yield_single_sentence_fragment):
266
+
267
+ if (buffer[-1] in sentence_fragment_delimiters
268
+ or word_count >= force_first_fragment_after_words):
200
269
 
201
- if buffer[-1] in sentence_delimiters:
202
- yield_text = _clean_text(buffer, cleanup_text_links, cleanup_text_emojis)
270
+ yield_text = _clean_text(
271
+ buffer,
272
+ cleanup_text_links,
273
+ cleanup_text_emojis)
203
274
  yield yield_text
204
275
  buffer = ""
205
276
  is_first_sentence = False
@@ -209,18 +280,19 @@ def generate_sentences(generator: Iterator[str],
209
280
  if len(buffer) <= minimum_sentence_length + context_size:
210
281
  continue
211
282
 
212
- # Potential delimiter character has to be a bit away from the end of the buffer
213
- # For reliable sentence detection the engine needs enough context to work with
283
+ # Potential delimiter character has to be a bit away from the end
284
+ # of the buffer. For reliable sentence detection the engine needs
285
+ # enough context to work with
214
286
  delimiter_char = buffer[-context_size]
215
287
 
216
- if delimiter_char in sentence_delimiters:
288
+ if delimiter_char in sentence_fragment_delimiters:
217
289
  sentences = _tokenize_sentences(buffer, tokenize_sentences)
218
290
  if len(sentences) > 1:
219
- if len(sentences[0]) == len(buffer) - context_size + 1:
220
- yield_text = _clean_text(buffer[:-context_size + 1], cleanup_text_links, cleanup_text_emojis)
221
- yield yield_text
222
- buffer = buffer[-context_size + 1:]
223
- is_first_sentence = False
291
+ total_length_except_first = sum(len(sentence) for sentence in sentences[1:])
292
+ if total_length_except_first >= context_size:
293
+ for sentence in sentences[:-1]:
294
+ yield sentence
295
+ buffer = sentences[-1]
224
296
 
225
297
  # Yield remaining buffer
226
298
  if buffer:
@@ -232,11 +304,17 @@ def generate_sentences(generator: Iterator[str],
232
304
  if len(sentence_buffer) < minimum_sentence_length:
233
305
  sentence_buffer += " "
234
306
  continue
235
- yield_text = _clean_text(sentence_buffer, cleanup_text_links, cleanup_text_emojis)
307
+ yield_text = _clean_text(
308
+ sentence_buffer,
309
+ cleanup_text_links,
310
+ cleanup_text_emojis)
236
311
  yield yield_text
237
312
 
238
313
  sentence_buffer = ""
239
-
314
+
240
315
  if sentence_buffer:
241
- yield_text = _clean_text(sentence_buffer, cleanup_text_links, cleanup_text_emojis)
242
- yield yield_text
316
+ yield_text = _clean_text(
317
+ sentence_buffer,
318
+ cleanup_text_links,
319
+ cleanup_text_emojis)
320
+ yield yield_text
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stream2sentence
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Real-time processing and delivery of sentences from a continuous stream of characters or text chunks.
5
5
  Home-page: https://github.com/KoljaB/stream2sentence
6
6
  Author: Kolja Beigel
@@ -19,6 +19,8 @@ Requires-Dist: stanza==1.6.1
19
19
 
20
20
  Real-time processing and delivery of sentences from a continuous stream of characters or text chunks.
21
21
 
22
+ > **Hint:** *If you're interested in state-of-the-art voice solutions you might also want to <strong>have a look at [Linguflex](https://github.com/KoljaB/Linguflex)</strong>, the original project from which stream2sentence is spun off. It lets you control your environment by speaking and is one of the most capable and sophisticated open-source assistants currently available.*
23
+
22
24
  ## Table of Contents
23
25
 
24
26
  - [Features](#features)
@@ -76,9 +78,6 @@ The `generate_sentences()` function has the following parameters:
76
78
  - `generator`: Input character generator.
77
79
  Iterator that emits chunks of text. These chunks can be of any size, and they'll be processed one by one to extract sentences from them. It forms the primary source from which the function reads and generates sentences.
78
80
 
79
- - `quick_yield_single_sentence_fragment`: Whether to return a sentence fragment as fast as possible.
80
- This is a feature for realtime speech synthesis. In some use cases you want to audio stream a minimal chunk of text as fast as possible, even when it means to synthesize mid-sentence. In this case you set this flag to True which will yield a synthesizable sentence fragment as early as possible.
81
-
82
81
  - `context_size`: Context size for sentence detection.
83
82
  This controls how much context is looked at to detect sentence boundaries. It determines the number of characters around a potential delimiter (like a period) that are considered when detecting sentence boundaries. A larger context size allows more reliable sentence boundary detection, but requires buffering more characters before emitting a sentence.
84
83
  Default is 12 characters. Increasing this can help detect sentences more accurately, at the cost of added latency.
@@ -103,6 +102,18 @@ The `generate_sentences()` function has the following parameters:
103
102
  If True, any Unicode emoji characters are identified and removed from the emitted output sentences. This can help to clean up the output.
104
103
  Default is False. Set to True if emojis are not required in the output.
105
104
 
105
+ - `tokenize_sentences`: (Optional) Function for sentence tokenization. Default is None.
106
+
107
+ - `tokenizer`: Specifies the tokenizer to use ('nltk' or 'stanza'). Default is 'nltk'.
108
+
109
+ - `language`: Language setting for the tokenizer ('en' or 'multilingual' for stanza). Default is 'en'.
110
+
111
+ - `log_characters`: Logs each processed character to the console for debugging. Default is False.
112
+
113
+ - `sentence_fragment_delimiters`: Characters considered as sentence delimiters for yielding quick fragment.
114
+
115
+ - `force_first_fragment_after_words`: Forces the first sentence fragment to yield after a specified number of words. Default is 10 words.
116
+
106
117
  - `log_characters`: Option to log characters to the console.
107
118
  When enabled, each character processed by the function is printed to the console. This is mainly for debugging purposes to observe the flow of characters through the function.
108
119
  Default is False. Set to True for a visual representation of characters being processed. Example: allows printing llm output to console when using stream2sentence to prepare input generation for text to speech synthesis.
@@ -1,6 +1,7 @@
1
1
  README.md
2
2
  setup.py
3
3
  stream2sentence/__init__.py
4
+ stream2sentence/stream2sentence - Kopie.py
4
5
  stream2sentence/stream2sentence.py
5
6
  stream2sentence.egg-info/PKG-INFO
6
7
  stream2sentence.egg-info/SOURCES.txt
@@ -5,8 +5,36 @@ class TestSentenceGenerator(unittest.TestCase):
5
5
 
6
6
  def test_chinese(self):
7
7
  text = "我喜欢读书。天气很好。我们去公园吧。今天是星期五。早上好。这是我的朋友。请帮我。吃饭了吗?我在学中文。晚安。"
8
- expected = ["我喜欢读书。", "天气很好。", "我们去公园吧。", "今天是星期五。", "早上好。", "这是我的朋友。", "请帮我。", "吃饭了吗?", "我在学中文。晚安。"]
9
- sentences = list(generate_sentences(text, minimum_sentence_length = 2, context_size=2, tokenizer="stanza", language="zh"))
8
+ expected = ["我喜欢读书。", "天气很好。", "我们去公园吧。", "今天是星期五。", "早上好。", "这是我的朋友。", "请帮我。吃饭了吗?", "我在学中文。", "晚安。"]
9
+ sentences = list(generate_sentences(text, minimum_sentence_length=2, context_size=2, tokenizer="stanza", language="zh"))
10
+ self.assertEqual(sentences, expected)
11
+
12
+ def test_chinese2(self):
13
+ text = """
14
+ 胡/爷/爷,我/来/给/您/讲/一下/下/周/每/天/的/安/排。
15
+ 周/一/:/9:00-10:00:晨/练/太/极/拳/,/地点/:/活/动/室/。
16
+ 10:30-11:30:园/艺/活/动/菠菜/种/植/,/地点/:/花/园/。
17
+ 14:00-15:00:手/工/制/作/睡/眠/香/囊/,/地点/:/手/工/室/。
18
+ 15:30-16:30:观/看/老/电/影/,/地点/:/影/音/室/。
19
+
20
+ 周/二/:/9:00-10:00:八/段/锦/简/化/版/,/地点/:/大/厅/。
21
+ 10:30-11:30:书/法/练/习/,/地点/:/书/画/室/。
22
+ 14:00-15:00:棋/牌/娱/乐/象/棋/、/围/棋/等/,/地点/:/棋/牌/室/。
23
+ 15:30-16:30:养/生/讲/座/春/天/养/生/1/,/地点/:/会/议/室/。
24
+ 大/厅/"""
25
+ expected = [
26
+ "胡/爷/爷,我/来/给/您/讲/一下/下/周/每/天/的/安/排。",
27
+ "周/一/:/9:00-10:00:晨/练/太/极/拳/,/地点/:/活/动/室/。",
28
+ "10:30-11:30:园/艺/活/动/菠菜/种/植/,/地点/:/花/园/。",
29
+ "14:00-15:00:手/工/制/作/睡/眠/香/囊/,/地点/:/手/工/室/。",
30
+ "15:30-16:30:观/看/老/电/影/,/地点/:/影/音/室/。",
31
+ "周/二/:/9:00-10:00:八/段/锦/简/化/版/,/地点/:/大/厅/。",
32
+ "10:30-11:30:书/法/练/习/,/地点/:/书/画/室/。",
33
+ "14:00-15:00:棋/牌/娱/乐/象/棋/、/围/棋/等/,/地点/:/棋/牌/室/。",
34
+ "15:30-16:30:养/生/讲/座/春/天/养/生/1/,/地点/:/会/议/室/。",
35
+ "大/厅/",
36
+ ]
37
+ sentences = list(generate_sentences(text, minimum_sentence_length=2, context_size=2, tokenizer="stanza", language="zh"))
10
38
  self.assertEqual(sentences, expected)
11
39
 
12
40
  def test_generator(self):