2 Commits

Author SHA1 Message Date
javis-bot
3d620dc4c7 fix: concise Korean weather reply (current conditions, one sentence)
getWeather returned a verbose multi-section English forecast that the 3B
re-synthesised into long, CJK/°F-leaking answers. Hand it a ready-to-speak
Korean one-liner (지금 <곳> 날씨는 <상태>, 기온 N도(체감 M도)입니다) and drop the
hourly/7-day firehose from the default voice reply.
2026-06-14 21:49:53 +09:00
javis-bot
09afc21283 feat: split recording vs STT-processing time in turn logs
Log the captured speech-clip duration (녹음/음성) separately from the Whisper
transcription time (STT처리) so it's clear whether a slow turn is the
listening/recording or the transcription, per the user's request.
2026-06-14 21:45:50 +09:00
2 changed files with 58 additions and 65 deletions

View File

@@ -459,14 +459,31 @@ def http_converse_stream():
# own Date.now() capture timestamps (same host, same clock).
return int(time.time() * 1000)
# Length of the captured speech clip (16-bit mono PCM). This is the
# "음성 인식(녹음)" portion — how long the user actually spoke (+ the
# bot's trailing silence cutoff) — as opposed to "STT 처리", the Whisper
# transcription time below. Splitting them shows whether a slow turn is
# the listening/recording or the transcription.
try:
_frames, _sr = _read_wav_pcm(raw)
audio_sec = (len(_frames) / 2) / _sr if _sr else 0.0
except Exception:
audio_sec = 0.0
t0 = time.monotonic()
stt = transcribe(raw)
t_stt = time.monotonic()
transcript = stt.get("text", "")
if not transcript:
print(
f"[bridge] ⏱️ turn 녹음(음성)={audio_sec:.1f}s STT처리(whisper)={t_stt - t0:.1f}s "
f"→ 인식 결과 없음 ({stt.get('note', '빈 결과')})",
flush=True,
)
yield json.dumps({"type": "meta", "transcript": "", "language": stt.get("language"),
"reply": "", "error": stt.get("error"),
"note": stt.get("note", "빈 결과"),
"audio_sec": round(audio_sec, 1),
"stt_sec": round(t_stt - t0, 1), "broadcast_action": None}) + "\n"
yield json.dumps({"type": "end"}) + "\n"
return
@@ -482,6 +499,7 @@ def http_converse_stream():
"reply": reply,
"error": result.get("error"),
"note": "ok" if reply.strip() else "답변 없음",
"audio_sec": round(audio_sec, 1),
"stt_sec": round(t_stt - t0, 1),
"think_sec": round(t_think - t_stt, 1),
# Wall-clock LLM window (epoch ms) for the transcript-channel timing
@@ -516,8 +534,9 @@ def http_converse_stream():
"tts_end_ms": tts_end_ms,
}) + "\n"
print(
f"[bridge] ⏱️ turn stt={t_stt - t0:.1f}s think(LLM)={t_think - t_stt:.1f}s "
f"tts={tts_total:.1f}s total={time.monotonic() - t0:.1f}s replylen={len(reply)} "
f"[bridge] ⏱️ turn 녹음(음성)={audio_sec:.1f}s STT처리(whisper)={t_stt - t0:.1f}s "
f"think(LLM)={t_think - t_stt:.1f}s tts={tts_total:.1f}s "
f"total(STT~TTS)={time.monotonic() - t0:.1f}s replylen={len(reply)} "
f"transcript={transcript[:40]!r}",
flush=True,
)

View File

@@ -175,6 +175,20 @@ WMO_CODES = {
99: "Thunderstorm with heavy hail",
}
# Korean conditions for the concise spoken reply.
WMO_CODES_KO = {
0: "맑음", 1: "대체로 맑음", 2: "구름 조금", 3: "흐림",
45: "안개", 48: "서리 안개",
51: "약한 이슬비", 53: "이슬비", 55: "강한 이슬비",
56: "약한 어는 이슬비", 57: "강한 어는 이슬비",
61: "약한 비", 63: "", 65: "강한 비",
66: "약한 어는 비", 67: "강한 어는 비",
71: "약한 눈", 73: "", 75: "강한 눈", 77: "싸락눈",
80: "약한 소나기", 81: "소나기", 82: "강한 소나기",
85: "약한 눈소나기", 86: "강한 눈소나기",
95: "천둥번개", 96: "우박 동반 천둥번개", 99: "강한 우박 천둥번개",
}
class WeatherTool(Tool):
"""Tool for getting current weather using Open-Meteo API."""
@@ -412,71 +426,31 @@ class WeatherTool(Tool):
# Get weather description
weather_desc = WMO_CODES.get(weather_code, "Unknown conditions")
# Build response text — current conditions
lines = [
f"Current weather in {location_display}:",
f"",
f"Conditions: {weather_desc}",
]
# Concise, ready-to-speak Korean one-liner for the voice path. The
# tool result is normally re-synthesised by the LLM, but a small
# model rambles and leaks °F / CJK fragments, so we hand it a clean
# Korean sentence it can echo verbatim (one-sentence system rule).
_ko = WMO_CODES_KO.get(weather_code, weather_desc)
_short_loc = location_display.split(",")[0].strip() or location_display
_ko_parts = [f"지금 {_short_loc} 날씨는 {_ko}"]
if temp_c is not None:
lines.append(f"Temperature: {temp_c}°C ({temp_f}°F)")
_t = f"기온 {round(temp_c)}"
if feels_like_c is not None and round(feels_like_c) != round(temp_c):
_t += f"(체감 {round(feels_like_c)}도)"
_ko_parts.append(_t)
ko_sentence = ", ".join(_ko_parts) + "입니다."
if feels_like_c is not None and feels_like_c != temp_c:
lines.append(f"Feels like: {feels_like_c}°C ({feels_like_f}°F)")
if humidity is not None:
lines.append(f"Humidity: {humidity}%")
if wind_speed is not None:
wind_info = f"Wind: {wind_speed} km/h"
if wind_gusts and wind_gusts > wind_speed:
wind_info += f" (gusts up to {wind_gusts} km/h)"
lines.append(wind_info)
# Append today's hourly forecast (remaining hours)
hourly = weather_data.get("hourly", {})
hourly_times = hourly.get("time", [])
hourly_temps = hourly.get("temperature_2m", [])
hourly_codes = hourly.get("weather_code", [])
if hourly_times and hourly_temps:
# Get current hour from the current time field
current_time = current.get("time", "")
current_hour_str = current_time[11:13] if len(current_time) >= 13 else ""
current_hour = int(current_hour_str) if current_hour_str.isdigit() else 0
today_prefix = current_time[:10] if len(current_time) >= 10 else ""
hourly_lines = []
for i, t in enumerate(hourly_times):
if not t.startswith(today_prefix):
continue
hour_str = t[11:13] if len(t) >= 13 else ""
hour = int(hour_str) if hour_str.isdigit() else -1
# Show every 3 hours from now onwards
if hour > current_hour and hour % 3 == 0 and i < len(hourly_temps) and i < len(hourly_codes):
desc = WMO_CODES.get(hourly_codes[i], "")
hourly_lines.append(f" {hour:02d}:00 — {hourly_temps[i]}°C, {desc}")
if hourly_lines:
lines.append("")
lines.append("Today's forecast (upcoming hours):")
lines.extend(hourly_lines)
# Append daily forecast
daily = weather_data.get("daily", {})
daily_dates = daily.get("time", [])
daily_codes = daily.get("weather_code", [])
daily_max = daily.get("temperature_2m_max", [])
daily_min = daily.get("temperature_2m_min", [])
if daily_dates and daily_max and daily_min:
lines.append("")
lines.append("7-day forecast:")
for i, date_str in enumerate(daily_dates):
if i < len(daily_max) and i < len(daily_min) and i < len(daily_codes):
desc = WMO_CODES.get(daily_codes[i], "")
lines.append(f" {date_str}: {daily_min[i]}{daily_max[i]}°C, {desc}")
# Build response text — concise current conditions (Korean sentence
# first so the model echoes it; English detail kept for any follow-up
# reasoning but the forecast firehose is dropped to curb rambling).
lines = [
f"한국어로 정확히 이 한 문장만 답하세요: {ko_sentence}",
f"(참고 데이터 — 답변에 추가하지 말 것: {location_display}, {weather_desc}, "
f"{temp_c}°C feels {feels_like_c}°C, humidity {humidity}%, wind {wind_speed}km/h)",
]
# Forecast (hourly / 7-day) is intentionally omitted from the default
# voice reply to keep it to one spoken sentence; current conditions
# are what "날씨 알려줘" asks for.
reply_text = "\n".join(lines)