Compare commits
2 Commits
11a72cb296
...
3d620dc4c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d620dc4c7 | ||
|
|
09afc21283 |
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user