Skip to content

Commit 9ab5ec5

Browse files
authored
fix: improve language disambiguation (#704)
* tests/increase_coverage coverage for language disambiguation * improve lang matching * test invalid lang detection
1 parent 7dbb8d3 commit 9ab5ec5

File tree

2 files changed

+145
-8
lines changed

2 files changed

+145
-8
lines changed

ovos_core/intent_services/service.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from typing import Tuple, Callable, List
2121

2222
import requests
23+
from langcodes import closest_match
2324
from ovos_bus_client.message import Message
2425
from ovos_bus_client.session import SessionManager
2526
from ovos_bus_client.util import get_message_lang
@@ -151,20 +152,24 @@ def disambiguate_lang(message):
151152
4 - config lang (or from message.data)
152153
"""
153154
default_lang = get_message_lang(message)
154-
valid_langs = get_valid_languages()
155+
valid_langs = message.context.get("valid_langs") or get_valid_languages()
155156
valid_langs = [standardize_lang_tag(l) for l in valid_langs]
156157
lang_keys = ["stt_lang",
157158
"request_lang",
158159
"detected_lang"]
159160
for k in lang_keys:
160161
if k in message.context:
161-
v = standardize_lang_tag(message.context[k])
162-
if v in valid_langs: # TODO - use lang distance instead to choose best dialect
163-
if v != default_lang:
164-
LOG.info(f"replaced {default_lang} with {k}: {v}")
165-
return v
166-
else:
162+
try:
163+
v = standardize_lang_tag(message.context[k])
164+
best_lang, _ = closest_match(v, valid_langs, max_distance=10)
165+
except:
166+
v = message.context[k]
167+
best_lang = "und"
168+
if best_lang == "und":
167169
LOG.warning(f"ignoring {k}, {v} is not in enabled languages: {valid_langs}")
170+
continue
171+
LOG.info(f"replaced {default_lang} with {k}: {v}")
172+
return v
168173

169174
return default_lang
170175

@@ -484,6 +489,7 @@ def handle_utterance(self, message: Message):
484489
else:
485490
# Nothing was able to handle the intent
486491
# Ask politely for forgiveness for failing in this vital task
492+
message.data["lang"] = lang
487493
self.send_complete_intent_failure(message)
488494

489495
LOG.debug(f"intent matching took: {stopwatch.time}")
@@ -504,7 +510,7 @@ def send_complete_intent_failure(self, message):
504510
sound = Configuration().get('sounds', {}).get('error', "snd/error.mp3")
505511
# NOTE: message.reply to ensure correct message destination
506512
self.bus.emit(message.reply('mycroft.audio.play_sound', {"uri": sound}))
507-
self.bus.emit(message.reply('complete_intent_failure'))
513+
self.bus.emit(message.reply('complete_intent_failure', message.data))
508514
self.bus.emit(message.reply("ovos.utterance.handled"))
509515

510516
@staticmethod

test/end2end/test_lang_detect.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
from unittest import TestCase
2+
3+
from ovos_bus_client.message import Message
4+
from ovos_bus_client.session import Session
5+
from ovos_utils.log import LOG
6+
7+
from ovoscope import End2EndTest, get_minicroft
8+
9+
10+
class TestLangDisambiguation(TestCase):
11+
12+
def setUp(self):
13+
LOG.set_level("DEBUG")
14+
self.minicroft = get_minicroft([]) # reuse for speed, but beware if skills keeping internal state
15+
16+
def tearDown(self):
17+
if self.minicroft:
18+
self.minicroft.stop()
19+
LOG.set_level("CRITICAL")
20+
21+
def test_stt_lang(self):
22+
session = Session("123")
23+
session.lang = "en-US"
24+
message = Message("recognizer_loop:utterance",
25+
{"utterances": ["hello world"], "lang": session.lang},
26+
{"session": session.serialize()})
27+
lang_keys = {
28+
"stt_lang": "ca-ES", # lang detection from audio plugin
29+
"request_lang": "pt-PT", # lang tagged in source message (wake word config)
30+
"detected_lang": "nl-NL" # lang detection from utterance (text) plugin
31+
}
32+
message.context.update(lang_keys)
33+
message.context["valid_langs"] = list(lang_keys.values())
34+
test = End2EndTest(
35+
minicroft=self.minicroft,
36+
skill_ids=[],
37+
eof_msgs=["ovos.utterance.handled"],
38+
flip_points=["recognizer_loop:utterance"],
39+
source_message=message,
40+
expected_messages=[
41+
message,
42+
Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}),
43+
Message("complete_intent_failure", {"lang": lang_keys["stt_lang"]}),
44+
Message("ovos.utterance.handled", {}),
45+
]
46+
)
47+
48+
test.execute()
49+
50+
51+
def test_lang_text_detection(self):
52+
session = Session("123")
53+
session.lang = "en-US"
54+
message = Message("recognizer_loop:utterance",
55+
{"utterances": ["hello world"], "lang": session.lang},
56+
{"session": session.serialize()})
57+
lang_keys = {
58+
"detected_lang": "nl-NL" # lang detection from utterance (text) plugin
59+
}
60+
message.context.update(lang_keys)
61+
message.context["valid_langs"] = list(lang_keys.values())
62+
test = End2EndTest(
63+
minicroft=self.minicroft,
64+
skill_ids=[],
65+
eof_msgs=["ovos.utterance.handled"],
66+
flip_points=["recognizer_loop:utterance"],
67+
source_message=message,
68+
expected_messages=[
69+
message,
70+
Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}),
71+
Message("complete_intent_failure", {"lang": lang_keys["detected_lang"]}),
72+
Message("ovos.utterance.handled", {}),
73+
]
74+
)
75+
76+
test.execute()
77+
78+
def test_metadata_preferred_over_text_detection(self):
79+
session = Session("123")
80+
session.lang = "en-US"
81+
message = Message("recognizer_loop:utterance",
82+
{"utterances": ["hello world"], "lang": session.lang},
83+
{"session": session.serialize()})
84+
lang_keys = {
85+
"request_lang": "pt-PT", # lang tagged in source message (wake word config)
86+
"detected_lang": "nl-NL" # lang detection from utterance (text) plugin
87+
}
88+
message.context.update(lang_keys)
89+
message.context["valid_langs"] = list(lang_keys.values())
90+
test = End2EndTest(
91+
minicroft=self.minicroft,
92+
skill_ids=[],
93+
eof_msgs=["ovos.utterance.handled"],
94+
flip_points=["recognizer_loop:utterance"],
95+
source_message=message,
96+
expected_messages=[
97+
message,
98+
Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}),
99+
Message("complete_intent_failure", {"lang": lang_keys["request_lang"]}),
100+
Message("ovos.utterance.handled", {}),
101+
]
102+
)
103+
104+
test.execute()
105+
106+
def test_invalid_lang_detection(self):
107+
session = Session("123")
108+
session.lang = "en-US"
109+
message = Message("recognizer_loop:utterance",
110+
{"utterances": ["hello world"], "lang": session.lang},
111+
{"session": session.serialize()})
112+
lang_keys = {
113+
"detected_lang": "nl-NL"
114+
}
115+
message.context.update(lang_keys)
116+
message.context["valid_langs"] = [session.lang] # no nl-NL
117+
test = End2EndTest(
118+
minicroft=self.minicroft,
119+
skill_ids=[],
120+
eof_msgs=["ovos.utterance.handled"],
121+
flip_points=["recognizer_loop:utterance"],
122+
source_message=message,
123+
expected_messages=[
124+
message,
125+
Message("mycroft.audio.play_sound", {"uri": "snd/error.mp3"}),
126+
Message("complete_intent_failure", {"lang": session.lang}),
127+
Message("ovos.utterance.handled", {}),
128+
]
129+
)
130+
131+
test.execute()

0 commit comments

Comments
 (0)