Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

202 lines
6.5 KiB
Python

import json
import re
import uuid
from typing import Dict, Optional
LIVE_STT_SESSION_PREFIX = "stt_live_session:"
LIVE_STT_SESSION_TTL_SECONDS = 15 * 60
LIVE_STT_MUTABLE_TAIL_WORDS = 8
LIVE_STT_SILENCE_MUTABLE_TAIL_WORDS = 2
LIVE_STT_MIN_COMMITTED_OVERLAP_WORDS = 2
def normalize_transcript_text(text: str) -> str:
return " ".join((text or "").split()).strip()
def join_transcript_parts(*parts: str) -> str:
return " ".join(part for part in map(normalize_transcript_text, parts) if part)
def _normalize_word(word: str) -> str:
normalized = re.sub(r"[^\w]+", "", word.casefold(), flags=re.UNICODE)
return normalized or word.casefold()
def _split_words(text: str) -> list[str]:
normalized = normalize_transcript_text(text)
return normalized.split() if normalized else []
def _common_prefix_length(left_words: list[str], right_words: list[str]) -> int:
max_index = min(len(left_words), len(right_words))
prefix_length = 0
for index in range(max_index):
if _normalize_word(left_words[index]) != _normalize_word(right_words[index]):
break
prefix_length += 1
return prefix_length
def _find_suffix_prefix_overlap(
left_words: list[str], right_words: list[str], min_overlap: int
) -> int:
max_overlap = min(len(left_words), len(right_words))
if max_overlap < min_overlap:
return 0
left_keys = [_normalize_word(word) for word in left_words]
right_keys = [_normalize_word(word) for word in right_words]
for overlap_size in range(max_overlap, min_overlap - 1, -1):
if left_keys[-overlap_size:] == right_keys[:overlap_size]:
return overlap_size
return 0
def strip_committed_prefix(committed_text: str, hypothesis_text: str) -> str:
committed_words = _split_words(committed_text)
hypothesis_words = _split_words(hypothesis_text)
if not committed_words or not hypothesis_words:
return normalize_transcript_text(hypothesis_text)
full_prefix_length = _common_prefix_length(committed_words, hypothesis_words)
if full_prefix_length == len(committed_words):
return " ".join(hypothesis_words[full_prefix_length:])
overlap_size = _find_suffix_prefix_overlap(
committed_words,
hypothesis_words,
LIVE_STT_MIN_COMMITTED_OVERLAP_WORDS,
)
if overlap_size:
return " ".join(hypothesis_words[overlap_size:])
return " ".join(hypothesis_words)
def _calculate_commit_count(
previous_hypothesis: str, current_hypothesis: str, is_silence: bool
) -> int:
previous_words = _split_words(previous_hypothesis)
current_words = _split_words(current_hypothesis)
if not current_words:
return 0
if not previous_words:
if is_silence:
return max(0, len(current_words) - LIVE_STT_SILENCE_MUTABLE_TAIL_WORDS)
return 0
stable_prefix_length = _common_prefix_length(previous_words, current_words)
if not stable_prefix_length:
return 0
mutable_tail_words = (
LIVE_STT_SILENCE_MUTABLE_TAIL_WORDS
if is_silence
else LIVE_STT_MUTABLE_TAIL_WORDS
)
max_committable_by_tail = max(0, len(current_words) - mutable_tail_words)
return min(stable_prefix_length, max_committable_by_tail)
def create_live_stt_session(
user: str, language: Optional[str] = None
) -> Dict[str, object]:
return {
"session_id": str(uuid.uuid4()),
"user": user,
"language": language,
"committed_text": "",
"mutable_text": "",
"previous_hypothesis": "",
"latest_hypothesis": "",
"last_chunk_index": -1,
}
def get_live_stt_session_key(session_id: str) -> str:
return f"{LIVE_STT_SESSION_PREFIX}{session_id}"
def save_live_stt_session(redis_client, session_state: Dict[str, object]) -> None:
redis_client.setex(
get_live_stt_session_key(str(session_state["session_id"])),
LIVE_STT_SESSION_TTL_SECONDS,
json.dumps(session_state),
)
def load_live_stt_session(redis_client, session_id: str) -> Optional[Dict[str, object]]:
raw_session = redis_client.get(get_live_stt_session_key(session_id))
if not raw_session:
return None
if isinstance(raw_session, bytes):
raw_session = raw_session.decode("utf-8")
return json.loads(raw_session)
def delete_live_stt_session(redis_client, session_id: str) -> None:
redis_client.delete(get_live_stt_session_key(session_id))
def apply_live_stt_hypothesis(
session_state: Dict[str, object],
hypothesis_text: str,
chunk_index: int,
is_silence: bool = False,
) -> Dict[str, object]:
last_chunk_index = int(session_state.get("last_chunk_index", -1))
if chunk_index < 0:
raise ValueError("chunk_index must be non-negative")
if chunk_index < last_chunk_index:
raise ValueError("chunk_index is older than the last processed chunk")
if chunk_index == last_chunk_index:
return session_state
committed_text = normalize_transcript_text(str(session_state.get("committed_text", "")))
previous_hypothesis = normalize_transcript_text(
str(session_state.get("latest_hypothesis", ""))
)
current_hypothesis = strip_committed_prefix(committed_text, hypothesis_text)
if not current_hypothesis and is_silence and previous_hypothesis:
committed_text = join_transcript_parts(committed_text, previous_hypothesis)
previous_hypothesis = ""
commit_count = _calculate_commit_count(
previous_hypothesis,
current_hypothesis,
is_silence=is_silence,
)
current_words = _split_words(current_hypothesis)
if commit_count:
committed_text = join_transcript_parts(
committed_text,
" ".join(current_words[:commit_count]),
)
current_hypothesis = " ".join(current_words[commit_count:])
session_state["committed_text"] = committed_text
session_state["mutable_text"] = normalize_transcript_text(current_hypothesis)
session_state["previous_hypothesis"] = previous_hypothesis
session_state["latest_hypothesis"] = normalize_transcript_text(current_hypothesis)
session_state["last_chunk_index"] = chunk_index
return session_state
def get_live_stt_transcript_text(session_state: Dict[str, object]) -> str:
return join_transcript_parts(
str(session_state.get("committed_text", "")),
str(session_state.get("mutable_text", "")),
)
def finalize_live_stt_session(session_state: Dict[str, object]) -> str:
return join_transcript_parts(
str(session_state.get("committed_text", "")),
str(session_state.get("latest_hypothesis", "")),
)