stark-engine 4.4.0__tar.gz → 4.4.1__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.
Files changed (76) hide show
  1. {stark_engine-4.4.0 → stark_engine-4.4.1}/PKG-INFO +1 -1
  2. {stark_engine-4.4.0 → stark_engine-4.4.1}/pyproject.toml +1 -1
  3. stark_engine-4.4.1/stark/__init__.py +79 -0
  4. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/command.py +2 -2
  5. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/commands_context.py +11 -4
  6. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/health_check.py +9 -5
  7. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/parsing.py +81 -2
  8. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/processors/__init__.py +1 -0
  9. stark_engine-4.4.1/stark/core/processors/corrections_processor.py +81 -0
  10. stark_engine-4.4.1/stark/core/processors/search_processor.py +176 -0
  11. stark_engine-4.4.1/stark/general/feature_flags.py +17 -0
  12. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/__init__.py +1 -0
  13. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/locale_string.py +63 -34
  14. stark_engine-4.4.1/stark/general/localisation/localizer.py +154 -0
  15. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/strings.py +5 -3
  16. stark_engine-4.4.1/stark/interfaces/microphone.py +61 -0
  17. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/interfaces/protocols.py +5 -2
  18. stark_engine-4.4.1/stark/interfaces/recognizer_relay.py +207 -0
  19. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/interfaces/vosk.py +99 -57
  20. stark_engine-4.4.1/stark/models/__init__.py +0 -0
  21. stark_engine-4.4.1/stark/models/transcription_string.py +201 -0
  22. stark_engine-4.4.1/stark/models/voice_transcription.py +291 -0
  23. stark_engine-4.4.1/stark/models/voice_transcription_string.py +133 -0
  24. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/__init__.py +1 -0
  25. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/dictionary.py +9 -8
  26. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/models.py +4 -3
  27. stark_engine-4.4.1/stark/tools/dictionary/recognizable_dictionary.py +28 -0
  28. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/storage/storage_memory.py +4 -2
  29. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/storage/storage_sqlite.py +4 -2
  30. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/__init__.py +7 -3
  31. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/epitran.py +5 -3
  32. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/espeak.py +3 -1
  33. stark_engine-4.4.1/stark/tools/phonetic/transcription/latin_passthrough.py +25 -0
  34. stark_engine-4.4.1/stark/tools/phonetic/transcription/protocol.py +7 -0
  35. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/voice_assistant/voice_assistant.py +21 -10
  36. stark_engine-4.4.0/stark/__init__.py +0 -50
  37. stark_engine-4.4.0/stark/core/processors/search_processor.py +0 -96
  38. stark_engine-4.4.0/stark/general/localisation/localizer.py +0 -105
  39. stark_engine-4.4.0/stark/tools/phonetic/transcription/protocol.py +0 -5
  40. {stark_engine-4.4.0 → stark_engine-4.4.1}/LICENSE.md +0 -0
  41. {stark_engine-4.4.0 → stark_engine-4.4.1}/README.md +0 -0
  42. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/__init__.py +0 -0
  43. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/commands_context_processor.py +0 -0
  44. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/commands_manager.py +0 -0
  45. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/patterns/__init__.py +0 -0
  46. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/patterns/pattern.py +0 -0
  47. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/patterns/rules.py +0 -0
  48. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/processors/spacy_ner_processor.py +0 -0
  49. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/__init__.py +0 -0
  50. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/location.py +0 -0
  51. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/object.py +0 -0
  52. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/slots.py +0 -0
  53. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/string.py +0 -0
  54. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/word.py +0 -0
  55. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/blockage_detector.py +0 -0
  56. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/cache.py +0 -0
  57. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/classproperty.py +0 -0
  58. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/dependencies.py +0 -0
  59. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/json_encoder.py +0 -0
  60. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/language_code.py +0 -0
  61. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/localizable_string.py +0 -0
  62. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/interfaces/gcloud.py +0 -0
  63. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/interfaces/silero.py +0 -0
  64. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/common/span.py +0 -0
  65. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/!examples.py +0 -0
  66. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/nl_dictionary_name.py +0 -0
  67. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/storage/__init__.py +0 -0
  68. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/levenshtein/__init__.py +0 -0
  69. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/levenshtein/levenshtein.pyi +0 -0
  70. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/levenshtein/levenshtein.pyx +0 -0
  71. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/simplephone.py +0 -0
  72. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/ipa2lat.py +0 -0
  73. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/sliding_window_parser.py +0 -0
  74. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/strtools.py +0 -0
  75. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/voice_assistant/__init__.py +0 -0
  76. {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/voice_assistant/mode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stark-engine
3
- Version: 4.4.0
3
+ Version: 4.4.1
4
4
  Summary: S.T.A.R.K - Speech and Text Algorithmic Recognition Kit. Modern framework for creating powerfull voice assistants.
5
5
  License: CC BY-NC-SA 4.0
6
6
  License-File: LICENSE.md
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "stark-engine"
3
- version = "4.4.0"
3
+ version = "4.4.1"
4
4
  description = "S.T.A.R.K - Speech and Text Algorithmic Recognition Kit. Modern framework for creating powerfull voice assistants."
5
5
  authors = ["MarkParker5 <mark@parker-programs.com>"]
6
6
  license = "CC BY-NC-SA 4.0"
@@ -0,0 +1,79 @@
1
+ import asyncer
2
+
3
+ from stark.core import (
4
+ Command,
5
+ CommandsContext,
6
+ CommandsManager,
7
+ Pattern,
8
+ Response,
9
+ ResponseStatus,
10
+ )
11
+ from stark.core.commands_context_processor import CommandsContextProcessor
12
+ from stark.core.health_check import health_check
13
+ from stark.core.processors.search_processor import SearchProcessor
14
+ from stark.general.blockage_detector import BlockageDetector
15
+ from stark.general.localisation import Localizer
16
+ from stark.interfaces.protocols import (
17
+ SpeechRecognizer,
18
+ SpeechRecognizerDelegate,
19
+ SpeechSynthesizer,
20
+ SpeechSynthesizerResult,
21
+ )
22
+ from stark.voice_assistant import Mode, VoiceAssistant
23
+
24
+
25
+ async def run(
26
+ manager: CommandsManager,
27
+ speech_recognizer: SpeechRecognizer | list[SpeechRecognizer],
28
+ speech_synthesizer: SpeechSynthesizer,
29
+ processors: list[CommandsContextProcessor] | None = None,
30
+ localizer: Localizer | None = None,
31
+ ):
32
+ if processors is None:
33
+ if localizer:
34
+ from stark.core.processors.corrections_processor import CorrectionsProcessor
35
+ processors = [CorrectionsProcessor(), SearchProcessor()]
36
+ else:
37
+ processors = [SearchProcessor()]
38
+
39
+ async with asyncer.create_task_group() as main_task_group:
40
+ context = CommandsContext(
41
+ task_group=main_task_group,
42
+ commands_manager=manager,
43
+ processors=processors,
44
+ localizer=localizer,
45
+ )
46
+
47
+ from stark.interfaces.microphone import Microphone
48
+
49
+ recognizers = speech_recognizer if isinstance(speech_recognizer, list) else [speech_recognizer]
50
+ use_relay = len(recognizers) > 1
51
+
52
+ if use_relay:
53
+ from stark.interfaces.recognizer_relay import SpeechRecognizerRelay
54
+ effective_recognizer = SpeechRecognizerRelay(recognizers)
55
+ else:
56
+ effective_recognizer = recognizers[0]
57
+
58
+ voice_assistant = VoiceAssistant(
59
+ speech_recognizer=effective_recognizer,
60
+ speech_synthesizer=speech_synthesizer,
61
+ commands_context=context,
62
+ )
63
+ effective_recognizer.delegate = voice_assistant
64
+ context.delegate = voice_assistant
65
+
66
+ health_check(context.pattern_parser, manager.commands)
67
+
68
+ if use_relay:
69
+ effective_recognizer.start_speech_recognizers(main_task_group)
70
+ else:
71
+ main_task_group.soonify(effective_recognizer.start_listening)()
72
+
73
+ microphone = Microphone(effective_recognizer.microphone_did_receive_sample)
74
+ main_task_group.soonify(microphone.start_listening)()
75
+
76
+ main_task_group.soonify(context.handle_responses)()
77
+
78
+ detector = BlockageDetector()
79
+ main_task_group.soonify(detector.monitor)()
@@ -43,10 +43,10 @@ CommandRunner = TypeVar("CommandRunner", bound=SyncCommandRunner | AsyncCommandR
43
43
 
44
44
  class Command(Generic[CommandRunner]):
45
45
  name: str
46
- patterns: dict[str, Pattern]
46
+ patterns: dict[LanguageCode, Pattern]
47
47
  _runner: CommandRunner
48
48
 
49
- def __init__(self, name: str, patterns: dict[str, Pattern], runner: CommandRunner):
49
+ def __init__(self, name: str, patterns: dict[LanguageCode, Pattern], runner: CommandRunner):
50
50
  assert patterns and all(isinstance(p, Pattern) for p in patterns.values())
51
51
  self.name = name
52
52
  self.patterns = patterns
@@ -16,8 +16,9 @@ from stark.core.commands_context_processor import (
16
16
  from stark.core.parsing import PatternParser
17
17
  from stark.core.types.object import Object
18
18
  from stark.general.localisation import LocaleString, Localizer
19
+ from stark.general.localisation.language_code import LanguageCode
19
20
 
20
- from ..general.dependencies import DependencyManager, default_dependency_manager
21
+ from ..general.dependencies import Dependency, DependencyManager, default_dependency_manager
21
22
  from .command import (
22
23
  AsyncResponseHandler,
23
24
  Command,
@@ -104,9 +105,7 @@ class CommandsContext:
104
105
 
105
106
  for processor in self.processors:
106
107
  logger.debug(f"Processing context {processor=} with {string=} {recognized_entities=} {self.context_queue=}")
107
- search_results, context_pops = await processor.process_string(
108
- string, self, recognized_entities
109
- )
108
+ search_results, context_pops = await processor.process_string(string, self, recognized_entities)
110
109
  if search_results:
111
110
  # Pop contexts as directed by processor
112
111
  for _ in range(context_pops):
@@ -123,7 +122,15 @@ class CommandsContext:
123
122
  parameters: dict[str, Object] = {}
124
123
  parameters.update(current_context.parameters)
125
124
  parameters.update(search_result.match_result.parameters)
125
+
126
+ substring = search_result.match_result.substring
127
+ lang = substring.language_code if isinstance(substring, LocaleString) else string.language_code
128
+ lang_dep = Dependency(None, LanguageCode, lang)
129
+ self.dependency_manager.dependencies.add(lang_dep)
126
130
  parameters.update(self.dependency_manager.resolve(search_result.command._runner))
131
+ # language is a command-specific temporary dependency
132
+ self.dependency_manager.dependencies.discard(lang_dep)
133
+
127
134
  self.run_command(search_result.command, parameters)
128
135
 
129
136
  return search_results or []
@@ -1,7 +1,7 @@
1
1
  import warnings
2
2
 
3
3
  from stark.core.command import Command
4
- from stark.core.parsing import PatternParser, _LOCALIZER_KEY_REGEX
4
+ from stark.core.parsing import _LOCALIZER_KEY_REGEX, PatternParser
5
5
 
6
6
 
7
7
  def health_check(pattern_parser: PatternParser, commands: list[Command]) -> None:
@@ -32,17 +32,19 @@ def health_check(pattern_parser: PatternParser, commands: list[Command]) -> None
32
32
  for lang, cmd_pattern in command.patterns.items():
33
33
  if pattern_parser.localizer and pattern_parser._has_localizer_keys(cmd_pattern):
34
34
  for key in _LOCALIZER_KEY_REGEX.findall(cmd_pattern._origin):
35
- pattern_parser.localizer.verify_key_exists(key)
35
+ pattern_parser.localizer.verify_recognizable(key)
36
36
  for param in command.get_pattern("base").parameters.values():
37
37
  assert param.type_name in pattern_parser.parameter_types_by_name, (
38
- f"Unknown parameter type '{param.type_name}' in pattern '{command.get_pattern("base")}' of command '{command.name}'. Known types: {sorted(pattern_parser.parameter_types_by_name.keys())}"
38
+ f"Unknown parameter type '{param.type_name}' in pattern '{command.get_pattern('base')}' of command '{command.name}'. Known types: {sorted(pattern_parser.parameter_types_by_name.keys())}"
39
39
  )
40
40
 
41
41
  # 5. Commands: all pattern params are expected by the runner func
42
42
  for command in commands:
43
43
  pattern_params = set(command.get_pattern("base").parameters.keys())
44
44
  runner_params = set(command._runner.__annotations__.keys())
45
- assert pattern_params <= runner_params, f"Command '{command.name}' function missing parameters: {pattern_params - runner_params}"
45
+ assert pattern_params <= runner_params, (
46
+ f"Command '{command.name}' function missing parameters: {pattern_params - runner_params}"
47
+ )
46
48
 
47
49
  # 6. Pattern syntax validity (TODO)
48
50
  # for command in commands:
@@ -54,7 +56,9 @@ def health_check(pattern_parser: PatternParser, commands: list[Command]) -> None
54
56
  try:
55
57
  pattern_parser._compile_pattern(p, language_code=lang)
56
58
  except Exception as e:
57
- warnings.warn(f"Failed to compile pattern '{p}' of type '{reg_type.type.__name__}' (language '{lang}'): {e}")
59
+ warnings.warn(
60
+ f"Failed to compile pattern '{p}' of type '{reg_type.type.__name__}' (language '{lang}'): {e}"
61
+ )
58
62
  for command in commands:
59
63
  for lang, p in command.patterns.items():
60
64
  try:
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from abc import ABC
5
- from dataclasses import dataclass
5
+ from dataclasses import dataclass, field
6
6
  from typing import AsyncGenerator
7
7
 
8
8
  from stark.core.patterns.pattern import Pattern
@@ -11,7 +11,16 @@ from stark.core.types import Object
11
11
  from stark.core.types.string import String
12
12
  from stark.core.types.word import Word
13
13
  from stark.general.cache import alru_cache
14
+ from typing import NamedTuple
15
+
14
16
  from stark.general.localisation import LanguageCode, LocaleString, Localizer
17
+ from stark.models.transcription_string import Correction
18
+ from stark.tools.common.span import Span
19
+
20
+
21
+ class CorrectionMatch(NamedTuple):
22
+ span: Span
23
+ correction: Correction
15
24
 
16
25
  type ObjectType = type[Object]
17
26
 
@@ -46,6 +55,8 @@ class MatchResult:
46
55
  start: int
47
56
  end: int
48
57
  parameters: dict[str, Object | None] # TODO: use ParameterMatch?
58
+ corrections: list[CorrectionMatch] = field(default_factory=list)
59
+ corrected_string: str = ""
49
60
 
50
61
 
51
62
  @dataclass
@@ -127,7 +138,7 @@ class PatternParser:
127
138
  f"Pattern '{pattern._origin}' of type '{object_type.__name__}' contains @key references but no Localizer is configured on PatternParser"
128
139
  )
129
140
  for key in keys:
130
- self.localizer.verify_key_exists(key)
141
+ self.localizer.verify_recognizable(key)
131
142
 
132
143
  @staticmethod
133
144
  def _has_localizer_keys(pattern: Pattern) -> bool:
@@ -203,6 +214,9 @@ class PatternParser:
203
214
  language_code = string.language_code
204
215
  recognized_entities = recognized_entities or []
205
216
  compiled = self._compile_pattern(pattern, language_code=language_code)
217
+
218
+ compiled, correction_groups = self._expand_corrections(compiled, string)
219
+
206
220
  logger.debug(f'Starting looking for "{pattern=}" "{compiled=}" in "{string}"')
207
221
 
208
222
  matches = []
@@ -237,6 +251,21 @@ class PatternParser:
237
251
  end = match.start() + new_match.end()
238
252
  command_str = string[start:end].strip()
239
253
 
254
+ # Backtrack corrections via named groups
255
+ match_corrections: list[CorrectionMatch] = []
256
+ for group_name, keyword in correction_groups.items():
257
+ if group_name not in match.groupdict():
258
+ continue
259
+ captured = match.group(group_name)
260
+ if captured and captured != keyword:
261
+ span = Span(match.start(group_name) - start, match.end(group_name) - start)
262
+ match_corrections.append(CorrectionMatch(span, Correction(captured, keyword)))
263
+
264
+ # Reconstruct corrected string by applying replacements right-to-left
265
+ corrected = str(command_str)
266
+ for cm in sorted(match_corrections, key=lambda cm: cm.span.start, reverse=True):
267
+ corrected = corrected[: cm.span.start] + cm.correction.keyword + corrected[cm.span.end :]
268
+
240
269
  matches.append(
241
270
  MatchResult(
242
271
  substring=command_str,
@@ -246,6 +275,8 @@ class PatternParser:
246
275
  name: (parameter.parsed_obj if parameter else None)
247
276
  for name, parameter in all_parameters.items()
248
277
  },
278
+ corrections=match_corrections,
279
+ corrected_string=corrected,
249
280
  )
250
281
  )
251
282
  logger.debug(f"Match result: {matches[-1]}")
@@ -253,6 +284,53 @@ class PatternParser:
253
284
  matches = self._filter_overlapping_matches(matches)
254
285
  return sorted(matches, key=lambda m: len(m.substring), reverse=True)
255
286
 
287
+ def _expand_corrections(self, compiled: str, string: LocaleString) -> tuple[str, dict[str, str]]:
288
+ """Inject corrections into compiled regex as named groups.
289
+
290
+ Returns (expanded_regex, group_to_keyword_map) where group_to_keyword_map
291
+ maps named group names to their correction keywords for back-tracking.
292
+ """
293
+ from stark.models.transcription_string import TranscriptionString
294
+
295
+ empty_map: dict[str, str] = {}
296
+ if not isinstance(string, TranscriptionString):
297
+ return compiled, empty_map
298
+
299
+ all_corrections = list(string.corrections)
300
+ for track_corrections in string._corrections_by_track.values():
301
+ all_corrections.extend(track_corrections)
302
+
303
+ if not all_corrections:
304
+ return compiled, empty_map
305
+
306
+ grouped: dict[str, set[str]] = {}
307
+ for c in all_corrections:
308
+ grouped.setdefault(c.keyword, set()).add(c.variant)
309
+
310
+ # each occurrence of a keyword gets a unique named group because re module
311
+ # doesn't allow duplicate group names
312
+ # (the third-party `regex` module does via DUPNAMES, but some bechmarks show it's ~2x slower — not worth swapping for the hot path)
313
+ # TODO: review regex vs re for STARK again later
314
+ group_map: dict[str, str] = {}
315
+ for keyword, variants in grouped.items():
316
+ alternatives = "|".join([re.escape(v) for v in variants])
317
+ result_parts: list[str] = []
318
+ remaining = compiled
319
+ found = False
320
+ while keyword in remaining:
321
+ idx = remaining.index(keyword)
322
+ group_name = f"_corr{len(group_map)}"
323
+ group_map[group_name] = keyword
324
+ replacement = f"(?P<{group_name}>{re.escape(keyword)}|{alternatives})"
325
+ result_parts.append(remaining[:idx])
326
+ result_parts.append(replacement)
327
+ remaining = remaining[idx + len(keyword) :]
328
+ found = True
329
+ if found:
330
+ result_parts.append(remaining)
331
+ compiled = "".join(result_parts)
332
+ return compiled, group_map
333
+
256
334
  def _find_initial_matches(self, compiled: str, string: str) -> list[re.Match]:
257
335
  return sorted(re.finditer(compiled, string), key=lambda match: match.start())
258
336
 
@@ -278,6 +356,7 @@ class PatternParser:
278
356
 
279
357
  # re-run regex only in the current command_str
280
358
  compiled = self._compile_pattern(pattern, prefill=prefill, language_code=language_code)
359
+ compiled, _ = self._expand_corrections(compiled, string)
281
360
  new_matches = list(re.finditer(compiled, string))
282
361
 
283
362
  logger.debug(f'Re capturing parameters {string} prefill={prefill} compiled="{compiled}"')
@@ -1,2 +1,3 @@
1
+ from .corrections_processor import CorrectionsProcessor
1
2
  from .search_processor import SearchProcessor
2
3
  from .spacy_ner_processor import SpacyNERProcessor
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, override
5
+
6
+ from stark.core.commands_context_processor import CommandsContextProcessor
7
+ from stark.core.commands_manager import SearchResult
8
+ from stark.core.parsing import RecognizedEntity
9
+ from stark.general.localisation import LocaleString
10
+
11
+ from ..commands_context import CommandsContext
12
+
13
+ if TYPE_CHECKING:
14
+ from stark.tools.dictionary.dictionary import Dictionary, LookupMode
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CorrectionsProcessor(CommandsContextProcessor):
20
+ """Pre-processor that generates phonetic corrections from dictionary lookups.
21
+
22
+ Accepts any Dictionary instance(s) — including one built from recognizable.strings
23
+ via build_recognizable_dictionary(). For each input word/phrase, runs dictionary
24
+ sentence search and appends matching corrections to TranscriptionString.corrections.
25
+ These are used by PatternParser._expand_corrections to widen compiled regexes.
26
+
27
+ For multilingual input with alternative tracks, runs dictionary search per track
28
+ and stores per-track corrections.
29
+
30
+ Place this processor before SearchProcessor in the pipeline.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ dictionaries: list[Dictionary] | None = None,
36
+ mode: LookupMode | None = None,
37
+ ):
38
+ self._dictionaries = dictionaries or []
39
+ if mode is None:
40
+ from stark.tools.dictionary.dictionary import LookupMode
41
+ mode = LookupMode.AUTO
42
+ self._mode = mode
43
+
44
+ @override
45
+ async def process_string(
46
+ self,
47
+ string: LocaleString,
48
+ context: CommandsContext,
49
+ recognized_entities: list[RecognizedEntity],
50
+ ) -> tuple[list[SearchResult], int]:
51
+ if not self._dictionaries:
52
+ return [], 0
53
+
54
+ from stark.models.transcription_string import TranscriptionString
55
+ from stark.models.transcription_string import Correction
56
+
57
+ if not isinstance(string, TranscriptionString):
58
+ return [], 0
59
+
60
+ self._find_corrections(str(string), string.language_code, string.corrections)
61
+
62
+ if string.alternative_texts:
63
+ for alt_lang, alt_text in string.alternative_texts.items():
64
+ if alt_lang == string.language_code:
65
+ continue
66
+ corrections: list[Correction] = []
67
+ self._find_corrections(str(alt_text), alt_lang, corrections)
68
+ if corrections:
69
+ string._corrections_by_track[alt_lang] = corrections
70
+
71
+ return [], 0
72
+
73
+ def _find_corrections(self, text: str, language_code: str, output: list):
74
+ from stark.models.transcription_string import Correction
75
+
76
+ for dictionary in self._dictionaries:
77
+ for result in dictionary.search_in_sentence(text, language_code, self._mode):
78
+ variant = text[result.span.slice]
79
+ keyword = result.item.name
80
+ if variant != keyword:
81
+ output.append(Correction(variant=variant, keyword=keyword))
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import override
4
+
5
+ from asyncer import SoonValue, create_task_group
6
+
7
+ from stark.core.parsing import MatchResult, PatternParser, RecognizedEntity
8
+ from stark.general.feature_flags import FeatureFlag, get_flag
9
+ from stark.general.localisation import LocaleString
10
+ from stark.general.localisation.language_code import LanguageCode
11
+
12
+ from ..command import Command
13
+ from ..commands_context import CommandsContext, CommandsContextLayer
14
+ from ..commands_context_processor import CommandsContextProcessor
15
+ from ..commands_manager import SearchResult
16
+
17
+
18
+ class SearchProcessor(CommandsContextProcessor):
19
+ async def search(
20
+ self,
21
+ string: str | LocaleString,
22
+ pattern_parser: PatternParser,
23
+ commands: list[Command],
24
+ recognized_entities: list[RecognizedEntity],
25
+ ) -> list[SearchResult]:
26
+ string = string if isinstance(string, LocaleString) else LocaleString(string)
27
+ language_code = string.language_code
28
+
29
+ # collect all tracks to match (primary + alternatives)
30
+ tracks_to_match: list[tuple[str | LocaleString, str]] = [(string, language_code)]
31
+
32
+ if get_flag(FeatureFlag.ENABLE_MULTILANG_MATRIX):
33
+ from stark.models.transcription_string import TranscriptionString
34
+
35
+ if isinstance(string, TranscriptionString) and string.alternative_texts:
36
+ for alt_lang, alt_text in string.alternative_texts.items():
37
+ if alt_lang == language_code:
38
+ continue
39
+ tracks_to_match.append((alt_text, alt_lang))
40
+
41
+ # match all tracks concurrently, tag results with source text and language
42
+ all_futures: list[tuple[str, str, SoonValue[list[SearchResult]]]] = []
43
+ async with create_task_group() as group:
44
+ for track_string, track_lang in tracks_to_match:
45
+ all_futures.append(
46
+ (
47
+ str(track_string),
48
+ track_lang,
49
+ group.soonify(self._match_commands)(
50
+ track_string, track_lang, pattern_parser, commands, recognized_entities
51
+ ),
52
+ )
53
+ )
54
+
55
+ tagged: list[tuple[str, str, SearchResult]] = []
56
+ global_index = 0
57
+ for source_text, track_lang, future in all_futures:
58
+ for r in future.value:
59
+ r.index = global_index
60
+ global_index += 1
61
+ tagged.append((source_text, track_lang, r))
62
+
63
+ # sort by start position (translated to primary track for ordering)
64
+ # primary = str(string)
65
+ # tagged.sort(key=lambda t: string.translate_position(t[2].match_result.start, t[0], primary))
66
+ tagged.sort(key=lambda t: t[2].match_result.start)
67
+
68
+ # resolve overlaps — each result lives in its own source track's coordinates
69
+ # translate between tracks on demand for comparison and cutting
70
+ for prev_entry, current_entry in zip(tagged.copy(), tagged[1:]):
71
+ prev_src, prev_lang, prev = prev_entry
72
+ current_src, current_lang, current = current_entry
73
+
74
+ # check for overlaps
75
+ try:
76
+ current_start_in_prev = string.translate_position(current.match_result.start, current_src, prev_src)
77
+ except ValueError:
78
+ # can't translate between tracks — drop lower priority
79
+ loser_entry = current_entry if prev.index < current.index else prev_entry
80
+ if loser_entry in tagged:
81
+ tagged.remove(loser_entry)
82
+ continue
83
+
84
+ if current_start_in_prev is None or prev.match_result.end <= current_start_in_prev:
85
+ # no overlap
86
+ continue
87
+
88
+ # overlap — try to cut each in its own source text
89
+ try:
90
+ prev_end_in_current = string.translate_position(prev.match_result.end, prev_src, current_src)
91
+ except ValueError:
92
+ loser_entry = current_entry if prev.index < current.index else prev_entry
93
+ if loser_entry in tagged:
94
+ tagged.remove(loser_entry)
95
+ continue
96
+
97
+ prev_cut = (
98
+ await pattern_parser.match(
99
+ prev.command.get_pattern(prev_lang),
100
+ prev_src[prev.match_result.start : current_start_in_prev],
101
+ recognized_entities,
102
+ )
103
+ if current_start_in_prev > prev.match_result.start
104
+ else []
105
+ )
106
+
107
+ current_cut = (
108
+ await pattern_parser.match(
109
+ current.command.get_pattern(current_lang),
110
+ current_src[prev_end_in_current : current.match_result.end],
111
+ recognized_entities,
112
+ )
113
+ if prev_end_in_current is not None and prev_end_in_current < current.match_result.end
114
+ else []
115
+ )
116
+
117
+ # less index = more priority to save full match
118
+ is_prev_priority = prev.index < current.index
119
+ loser_cut = current_cut if is_prev_priority else prev_cut
120
+ winner_cut = prev_cut if is_prev_priority else current_cut
121
+ loser_entry = current_entry if is_prev_priority else prev_entry
122
+ winner_entry = prev_entry if is_prev_priority else current_entry
123
+
124
+ if new_matches := loser_cut:
125
+ loser_entry[2].match_result = new_matches[0]
126
+ elif new_matches := winner_cut:
127
+ winner_entry[2].match_result = new_matches[0]
128
+ else:
129
+ if loser_entry in tagged:
130
+ tagged.remove(loser_entry)
131
+
132
+ return [r for _, _, r in tagged]
133
+
134
+ async def _match_commands(
135
+ self,
136
+ string: str | LocaleString,
137
+ language_code: LanguageCode,
138
+ pattern_parser: PatternParser,
139
+ commands: list[Command],
140
+ recognized_entities: list[RecognizedEntity],
141
+ ) -> list[SearchResult]:
142
+ results: list[SearchResult] = []
143
+ futures: list[tuple[Command, SoonValue[list[MatchResult]]]] = []
144
+
145
+ # run concurent commands match
146
+ async with create_task_group() as group:
147
+ for command in commands:
148
+ futures.append(
149
+ (
150
+ command,
151
+ group.soonify(pattern_parser.match)(
152
+ command.get_pattern(language_code), string, recognized_entities
153
+ ),
154
+ )
155
+ )
156
+
157
+ # read all finished matches
158
+ i = 0
159
+ for command, future in futures:
160
+ for match in future.value: # empty for most of commands (not matched)
161
+ results.append(SearchResult(command=command, match_result=match, index=i))
162
+ i += 1
163
+
164
+ return results
165
+
166
+ # Implement CommandsContextProcessor
167
+
168
+ @override
169
+ async def process_context_layer(
170
+ self,
171
+ string: LocaleString,
172
+ context: CommandsContext,
173
+ context_layer: CommandsContextLayer,
174
+ recognized_entities: list[RecognizedEntity],
175
+ ) -> list[SearchResult]:
176
+ return await self.search(string, context.pattern_parser, context_layer.commands, recognized_entities)
@@ -0,0 +1,17 @@
1
+ import os
2
+ from enum import StrEnum
3
+
4
+
5
+ class FeatureFlag(StrEnum):
6
+ ENABLE_VOICE_CLI = "STARK_ENABLE_VOICE_CLI"
7
+ ENABLE_MULTILANG_MATRIX = "STARK_ENABLE_MULTILANG_MATRIX"
8
+
9
+
10
+ _DEFAULTS: dict[FeatureFlag, str] = {
11
+ FeatureFlag.ENABLE_VOICE_CLI: "0",
12
+ FeatureFlag.ENABLE_MULTILANG_MATRIX: "1",
13
+ }
14
+
15
+
16
+ def get_flag(flag: FeatureFlag) -> bool:
17
+ return os.getenv(flag.value, _DEFAULTS.get(flag, "0")) == "1"
@@ -3,3 +3,4 @@ from .locale_string import LocaleString
3
3
  from .localizable_string import LocalizableString
4
4
  from .localizer import Localizer
5
5
  from .strings import StringsFile, String
6
+ from stark.models.transcription_string import TranscriptionString, TranscriptionWord