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.
- {stark_engine-4.4.0 → stark_engine-4.4.1}/PKG-INFO +1 -1
- {stark_engine-4.4.0 → stark_engine-4.4.1}/pyproject.toml +1 -1
- stark_engine-4.4.1/stark/__init__.py +79 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/command.py +2 -2
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/commands_context.py +11 -4
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/health_check.py +9 -5
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/parsing.py +81 -2
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/processors/__init__.py +1 -0
- stark_engine-4.4.1/stark/core/processors/corrections_processor.py +81 -0
- stark_engine-4.4.1/stark/core/processors/search_processor.py +176 -0
- stark_engine-4.4.1/stark/general/feature_flags.py +17 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/__init__.py +1 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/locale_string.py +63 -34
- stark_engine-4.4.1/stark/general/localisation/localizer.py +154 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/strings.py +5 -3
- stark_engine-4.4.1/stark/interfaces/microphone.py +61 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/interfaces/protocols.py +5 -2
- stark_engine-4.4.1/stark/interfaces/recognizer_relay.py +207 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/interfaces/vosk.py +99 -57
- stark_engine-4.4.1/stark/models/__init__.py +0 -0
- stark_engine-4.4.1/stark/models/transcription_string.py +201 -0
- stark_engine-4.4.1/stark/models/voice_transcription.py +291 -0
- stark_engine-4.4.1/stark/models/voice_transcription_string.py +133 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/__init__.py +1 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/dictionary.py +9 -8
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/models.py +4 -3
- stark_engine-4.4.1/stark/tools/dictionary/recognizable_dictionary.py +28 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/storage/storage_memory.py +4 -2
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/storage/storage_sqlite.py +4 -2
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/__init__.py +7 -3
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/epitran.py +5 -3
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/espeak.py +3 -1
- stark_engine-4.4.1/stark/tools/phonetic/transcription/latin_passthrough.py +25 -0
- stark_engine-4.4.1/stark/tools/phonetic/transcription/protocol.py +7 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/voice_assistant/voice_assistant.py +21 -10
- stark_engine-4.4.0/stark/__init__.py +0 -50
- stark_engine-4.4.0/stark/core/processors/search_processor.py +0 -96
- stark_engine-4.4.0/stark/general/localisation/localizer.py +0 -105
- stark_engine-4.4.0/stark/tools/phonetic/transcription/protocol.py +0 -5
- {stark_engine-4.4.0 → stark_engine-4.4.1}/LICENSE.md +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/README.md +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/__init__.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/commands_context_processor.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/commands_manager.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/patterns/__init__.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/patterns/pattern.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/patterns/rules.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/processors/spacy_ner_processor.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/__init__.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/location.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/object.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/slots.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/string.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/core/types/word.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/blockage_detector.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/cache.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/classproperty.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/dependencies.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/json_encoder.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/language_code.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/general/localisation/localizable_string.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/interfaces/gcloud.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/interfaces/silero.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/common/span.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/!examples.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/nl_dictionary_name.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/dictionary/storage/__init__.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/levenshtein/__init__.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/levenshtein/levenshtein.pyi +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/levenshtein/levenshtein.pyx +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/simplephone.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/phonetic/transcription/ipa2lat.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/sliding_window_parser.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/tools/strtools.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/voice_assistant/__init__.py +0 -0
- {stark_engine-4.4.0 → stark_engine-4.4.1}/stark/voice_assistant/mode.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "stark-engine"
|
|
3
|
-
version = "4.4.
|
|
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[
|
|
46
|
+
patterns: dict[LanguageCode, Pattern]
|
|
47
47
|
_runner: CommandRunner
|
|
48
48
|
|
|
49
|
-
def __init__(self, name: str, patterns: dict[
|
|
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
|
|
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.
|
|
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(
|
|
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,
|
|
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(
|
|
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.
|
|
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}"')
|
|
@@ -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"
|