Skip to content
Open
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
46 changes: 43 additions & 3 deletions astrbot/core/pipeline/preprocess_stage/stage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import random
import time
import traceback
from collections.abc import AsyncGenerator
from pathlib import Path
Expand All @@ -13,6 +14,7 @@
ensure_jpeg,
ensure_wav,
file_uri_to_path,
get_media_duration,
is_file_uri,
)

Expand Down Expand Up @@ -182,20 +184,29 @@ async def process(
async def _stt_record(record_comp: Record, is_reply: bool = False):
"""对单个 Record 组件执行语音转文本,成功返回 Plain,失败返回 None。"""
prefix = "referenced " if is_reply else ""
suffix = " (referenced message)" if is_reply else ""
try:
path = await record_comp.convert_to_file_path()
except Exception as e:
logger.warning(f"Failed to resolve the {prefix}voice path: {e}")
return None

# 探测音频时长(毫秒),用于计算实时率 RTF;失败时为 None
try:
audio_duration_ms = await get_media_duration(path)
except Exception:
audio_duration_ms = None

stt_start = time.time()
status = "failed"
retry = 5
for i in range(retry):
try:
Comment on lines +200 to 204
result = await stt_provider.get_text(audio_url=path)
if result:
suffix = " (referenced message)" if is_reply else ""
status = "success"
logger.info(f"Speech-to-text{suffix} result: " + result)
return Plain(result)
break
break
except FileNotFoundError:
# napcat workaround: file may not be ready immediately
Expand All @@ -206,9 +217,38 @@ async def _stt_record(record_comp: Record, is_reply: bool = False):
continue
except BaseException as e:
logger.error(traceback.format_exc())
suffix = " (referenced message)" if is_reply else ""
logger.error(f"Speech-to-text{suffix} failed: {e}")
break

stt_duration = time.time() - stt_start
rtf = (
stt_duration / (audio_duration_ms / 1000.0)
if audio_duration_ms
else None
)

provider_type = getattr(stt_provider.meta(), "type", "")
provider_model = stt_provider.get_model()
logger.info(
f"Speech-to-text{suffix} stats: provider={provider_type} "
f"model={provider_model} duration={stt_duration * 1000:.0f}ms "
f"audio={audio_duration_ms if audio_duration_ms is not None else 'N/A'}ms "
f"rtf={rtf if rtf is not None else 'N/A'} status={status}"
)
event.trace.record(
"stt",
provider=provider_type,
model=provider_model,
status=status,
referenced=is_reply,
audio_duration_ms=audio_duration_ms,
stt_duration=stt_duration,
rtf=rtf,
audio=Path(path).name,
)

if status == "success":
return Plain(result)
return None

message_chain = event.get_messages()
Expand Down
30 changes: 30 additions & 0 deletions astrbot/dashboard/services/live_chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from astrbot.core.platform.sources.webchat.webchat_queue_mgr import webchat_queue_mgr
from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_temp_path
from astrbot.core.utils.datetime_utils import generate_timestamp_id, to_utc_isoformat
from astrbot.core.utils.media_utils import get_media_duration
from astrbot.dashboard.services.chat_service import (
BotMessageAccumulator,
build_bot_history_content,
Expand Down Expand Up @@ -884,7 +885,36 @@ async def process_audio(

await send_json({"t": "metrics", "data": {"stt": stt_provider.meta().type}})

stt_start = time.time()
user_text = await stt_provider.get_text(audio_path)
stt_duration = time.time() - stt_start
Comment on lines +888 to +890

# 探测音频时长(毫秒)并计算实时率(RTF),探测失败时优雅降级
try:
audio_duration_ms = await get_media_duration(audio_path)
except Exception:
audio_duration_ms = None
audio_duration_sec = (
audio_duration_ms / 1000.0 if audio_duration_ms else None
)
rtf = stt_duration / audio_duration_sec if audio_duration_sec else None
logger.info(
f"[Live Chat] STT stats: provider={stt_provider.meta().type} "
f"model={stt_provider.get_model()} duration={stt_duration * 1000:.0f}ms "
f"audio={audio_duration_ms if audio_duration_ms is not None else 'N/A'}ms "
f"rtf={rtf if rtf is not None else 'N/A'}"
)
await send_json(
{
"t": "metrics",
"data": {
"stt_total_time": stt_duration,
"stt_audio_duration": audio_duration_sec,
"stt_rtf": rtf,
},
}
)

if not user_text:
logger.warning("[Live Chat] STT 识别结果为空")
return
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/components/chat/LiveMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
1000).toFixed(0) }}ms</span>
<span v-if="metrics.wav_to_tts_total_time">Speak -> End: {{ (metrics.wav_to_tts_total_time *
1000).toFixed(0) }}ms</span>
<span v-if="metrics.stt_total_time">STT Total Latency: {{ (metrics.stt_total_time *
1000).toFixed(0) }}ms</span>
<span v-if="metrics.stt_audio_duration">STT Audio Duration: {{ (metrics.stt_audio_duration *
1000).toFixed(0) }}ms</span>
<span v-if="metrics.stt_rtf">STT RTF: {{ metrics.stt_rtf.toFixed(2) }}</span>
Comment on lines +47 to +51
<span v-if="metrics.stt">STT Provider: {{ metrics.stt }}</span>
<span v-if="metrics.tts">TTS Provider: {{ metrics.tts }}</span>
<span v-if="metrics.chat_model">Chat Model: {{ metrics.chat_model }}</span>
Expand Down Expand Up @@ -108,6 +113,9 @@ interface LiveMetrics {
tts_first_frame_time?: number;
tts_total_time?: number;
wav_to_tts_total_time?: number;
stt_total_time?: number;
stt_audio_duration?: number;
stt_rtf?: number;
stt?: string;
tts?: string;
chat_model?: string;
Expand Down