This commit is contained in:
Magzhan Zhumabayev
2026-05-01 18:08:46 +05:00
parent 1f07c710e1
commit 5dda1e10a3
5 changed files with 169 additions and 29 deletions
+65
View File
@@ -557,6 +557,13 @@ def _sanitize_voice_text(text: str) -> str:
DEFAULT_NAME_RETRY_TEXT = "Подскажите, пожалуйста, как я могу к вам обращаться?"
DEFAULT_PERSONALIZED_GREETING_TEMPLATE = '{name}, я работаю на основе искусственного интеллекта и постараюсь максимально внимательно разобраться с вашим вопросом. Чем я могу помочь?'
_NAME_CAPTURE_CONFUSION_ALIASES = {
"парень": "Арнур",
"арнер": "Арнур",
"ар нур": "Арнур",
"шомарт": "Жомарт",
"шумарт": "Жомарт",
}
def _voice_text_key(text: str | None) -> str:
@@ -564,6 +571,9 @@ def _voice_text_key(text: str | None) -> str:
return re.sub(r"\s+", " ", compact).strip()
_KAZAKH_NAME_BY_KEY = {_voice_text_key(name): name for name in TOP_KAZAKH_NAMES}
def _voice_letters_key(text: str | None) -> str:
return "".join(re.findall(r"[^\W\d_]+", str(text or "").lower(), flags=re.UNICODE))
@@ -582,6 +592,39 @@ def _looks_like_supported_name_text(text: str) -> bool:
)
def _correct_name_capture_candidate(name: str | None) -> str | None:
candidate = _canonical_name(str(name or "").strip())
if not candidate:
return None
key = _voice_text_key(candidate)
exact_match = _KAZAKH_NAME_BY_KEY.get(key)
if exact_match:
return exact_match
alias_match = _NAME_CAPTURE_CONFUSION_ALIASES.get(key)
if alias_match:
return alias_match
best_name = None
best_ratio = 0.0
for gazetteer_key, gazetteer_name in _KAZAKH_NAME_BY_KEY.items():
ratio = difflib.SequenceMatcher(None, key, gazetteer_key).ratio()
if ratio > best_ratio:
best_ratio = ratio
best_name = gazetteer_name
if best_name is None:
return candidate
threshold = 0.82 if len(key) >= 5 else 0.9
if best_ratio >= threshold:
LOGGER.info(
"name capture candidate corrected via Kazakh names: candidate=%r corrected=%r ratio=%.3f",
candidate,
best_name,
best_ratio,
)
return best_name
return candidate
def _normalize_name_candidate(text: str | None) -> str | None:
raw = str(text or "").strip(" \t\r\n,.;:!?\"'()[]{}")
if not raw or any(ch.isdigit() for ch in raw):
@@ -798,6 +841,16 @@ def _voice_start_name_outcome(text: str | None) -> tuple[str, str | None]:
if not raw:
return "name_not_obtained", None
candidate, needs_followup = _extract_name_candidate(raw)
if candidate:
corrected_candidate = _correct_name_capture_candidate(candidate) or candidate
if corrected_candidate != candidate:
LOGGER.info(
"name capture candidate normalized: candidate=%r corrected=%r transcript=%r",
candidate,
corrected_candidate,
_preview_text(raw),
)
candidate = corrected_candidate
if candidate and not needs_followup:
return "name_obtained", candidate
if candidate:
@@ -1493,6 +1546,8 @@ class CallSession:
await self._stt.transcribe(
audio_bytes,
keyterms=self._name_capture_keyterms() if self._awaiting_customer_name else None,
language_code=self._name_capture_language_code() if self._awaiting_customer_name else None,
force_batch=self._awaiting_customer_name,
)
).strip()
except Exception:
@@ -1513,6 +1568,14 @@ class CallSession:
def _name_capture_keyterms(self) -> tuple[str, ...]:
return TOP_KAZAKH_NAMES
def _name_capture_language_code(self) -> str | None:
raw = (
os.getenv("STT_NAME_CAPTURE_LANGUAGE_CODE", "").strip()
or os.getenv("STT_NAME_CAPTURE_LANGUAGE", "").strip()
or "kz"
)
return raw or None
def _build_name_collection_response(self, transcript: str) -> str | None:
if not self._awaiting_customer_name:
return None
@@ -2074,6 +2137,8 @@ class CallSession:
return await self._stt.transcribe(
utterance_audio,
keyterms=self._name_capture_keyterms() if self._awaiting_customer_name else None,
language_code=self._name_capture_language_code() if self._awaiting_customer_name else None,
force_batch=self._awaiting_customer_name,
)
def _maybe_dump_audio(self, audio_bytes: bytes) -> None: