Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gateway/platforms/feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -3616,7 +3616,7 @@ def _resolve_normalized_message_type(
if preferred == "photo":
return self._resolve_media_message_type(media_types[0] if media_types else "", default=MessageType.PHOTO)
if preferred == "audio":
return self._resolve_media_message_type(media_types[0] if media_types else "", default=MessageType.AUDIO)
return MessageType.VOICE

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please retain a discriminator here rather than classifying every Feishu audio event as VOICE: current gateway routing deliberately keeps MessageType.AUDIO attachments out of STT (gateway/run.py:10414-10422). This needs coverage for both a native voice note and an ordinary audio-file attachment.

if preferred == "document":
return self._resolve_media_message_type(media_types[0] if media_types else "", default=MessageType.DOCUMENT)
return MessageType.TEXT
Expand Down
67 changes: 42 additions & 25 deletions tools/transcription_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ def _safe_find_spec(module_name: str) -> bool:
XAI_STT_BASE_URL = os.getenv("XAI_STT_BASE_URL", "https://api.x.ai/v1")
ELEVENLABS_STT_BASE_URL = os.getenv("ELEVENLABS_STT_BASE_URL", "https://api.elevenlabs.io/v1")

SUPPORTED_FORMATS = {".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm", ".ogg", ".aac", ".flac"}
LOCAL_NATIVE_AUDIO_FORMATS = {".wav", ".aiff", ".aif"}
SUPPORTED_FORMATS = {".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm", ".ogg", ".aac", ".flac", ".silk"}
LOCAL_NATIVE_AUDIO_FORMATS = {".wav", ".aiff", ".aif", ".ogg"}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding .silk to this global validation set affects cloud-provider dispatch too, but the new conversion exists only in _transcribe_local. Please split this unrelated capability or normalize it consistently for every provider that can receive an accepted .silk file.

MAX_FILE_SIZE = 25 * 1024 * 1024 # 25 MB

# Known model sets for auto-correction
Expand Down Expand Up @@ -1132,29 +1132,35 @@ def _transcribe_local(file_path: str, model_name: str) -> Dict[str, Any]:
if _forced_lang:
transcribe_kwargs["language"] = _forced_lang

try:
segments, info = _local_model.transcribe(file_path, **transcribe_kwargs)
transcript = " ".join(segment.text.strip() for segment in segments)
except Exception as exc:
# CUDA runtime libs sometimes only fail at dlopen-on-first-use,
# AFTER the model loaded successfully. Evict the broken cached
# model, reload on CPU, retry once. Without this the module-
# global `_local_model` is poisoned and every subsequent voice
# message on this process fails identically until restart.
if not _looks_like_cuda_lib_error(exc):
raise
logger.warning(
"faster-whisper CUDA runtime failed mid-transcribe (%s) — "
"evicting cached model and retrying on CPU (int8).",
exc,
)
_local_model = None
_local_model_name = None
from faster_whisper import WhisperModel
_local_model = WhisperModel(model_name, device="cpu", compute_type="int8")
_local_model_name = model_name
segments, info = _local_model.transcribe(file_path, **transcribe_kwargs)
transcript = " ".join(segment.text.strip() for segment in segments)
# Convert non-native formats (e.g. WeChat .silk) before passing to Whisper
with tempfile.TemporaryDirectory(prefix="hermes-stt-") as work_dir:
prepared_input, prep_error = _prepare_local_audio(file_path, work_dir)
if prep_error:
return {"success": False, "transcript": "", "error": prep_error}

try:
segments, info = _local_model.transcribe(prepared_input, **transcribe_kwargs)
transcript = " ".join(segment.text.strip() for segment in segments)
except Exception as exc:
# CUDA runtime libs sometimes only fail at dlopen-on-first-use,
# AFTER the model loaded successfully. Evict the broken cached
# model, reload on CPU, retry once. Without this the module-
# global `_local_model` is poisoned and every subsequent voice
# message on this process fails identically until restart.
if not _looks_like_cuda_lib_error(exc):
raise
logger.warning(
"faster-whisper CUDA runtime failed mid-transcribe (%s) — "
"evicting cached model and retrying on CPU (int8).",
exc,
)
_local_model = None
_local_model_name = None
from faster_whisper import WhisperModel
_local_model = WhisperModel(model_name, device="cpu", compute_type="int8")
_local_model_name = model_name
segments, info = _local_model.transcribe(prepared_input, **transcribe_kwargs)
transcript = " ".join(segment.text.strip() for segment in segments)

logger.info(
"Transcribed %s via local whisper (%s, lang=%s, %.1fs audio)",
Expand All @@ -1174,6 +1180,17 @@ def _prepare_local_audio(file_path: str, work_dir: str) -> tuple[Optional[str],
if audio_path.suffix.lower() in LOCAL_NATIVE_AUDIO_FORMATS:
return file_path, None

# Handle WeChat .silk format via pilk (pure Python SILK decoder, no ffmpeg needed)
if audio_path.suffix.lower() == ".silk":
try:
import pilk
converted_path = os.path.join(work_dir, f"{audio_path.stem}.wav")
pilk.silk_to_wav(file_path, converted_path, rate=24000)
return converted_path, None
except Exception as e:
logger.error("pilk .silk decode failed for %s: %s", file_path, e)
return None, f"Failed to decode .silk audio: {e}"

ffmpeg = _find_ffmpeg_binary()
if not ffmpeg:
return None, "Local STT fallback requires ffmpeg for non-WAV inputs, but ffmpeg was not found"
Expand Down