Files
momentry_core/scripts/weather_sound_detector.py
Warren e75c4d6f07 cleanup: remove dead code and duplicate docs
- Remove session-ses_2f27.md (161KB raw session log)
- Remove 49 ROOT_* duplicate files across REFERENCE/
- Remove 14 duplicate files between REFERENCE/ root and history/
- Remove asr_legacy.rs (dead code, replaced by asr.rs)
- Remove src/core/worker/ (duplicate JobWorker)
- Remove src/core/layers/ (empty directory)
- Remove 4 .bak files in src/
- Remove 7 dead private methods in worker/processor.rs
- Remove backup directory from git tracking
2026-05-04 01:31:21 +08:00

140 lines
4.6 KiB
Python

#!/opt/homebrew/bin/python3.11
"""
Weather Sound Detector (Rain & Thunder)
職責:使用聲學特徵 (Librosa) 辨識雨聲 (Rain) 與雷聲 (Thunder)。
"""
import librosa
import numpy as np
import os
import json
# 設定
UUID = os.getenv("UUID", "384b0ff44aaaa1f1")
OUTPUT_DIR = os.getenv("MOMENTRY_OUTPUT_DIR", "./output")
AUDIO_PATH = os.path.join(OUTPUT_DIR, UUID, f"{UUID}.wav")
OUTPUT_JSON = os.path.join(OUTPUT_DIR, UUID, f"{UUID}.weather_events.json")
def detect_weather_sounds(audio_path):
print(f"🔍 Loading audio: {audio_path}")
# 使用 16kHz 取樣
y, sr = librosa.load(audio_path, sr=16000, mono=True)
total_dur = len(y) / sr
# 分析視窗:每 10 秒一幀
hop_length = int(10.0 * sr)
frame_length = int(10.0 * sr)
print("📊 Analyzing spectral features...")
# 1. 計算聲學特徵
# RMS: 能量 (響度) - shape (1, frames) -> take [0] to get (frames,)
rms = librosa.feature.rms(y=y, frame_length=frame_length, hop_length=hop_length)[0]
# Spectral Flatness: 頻譜平坦度 - shape (1, frames) -> take [0]
flatness = librosa.feature.spectral_flatness(
y=y, n_fft=frame_length, hop_length=hop_length
)[0]
# Spectral Centroid: 頻譜質心 - shape (1, frames) -> take [0]
centroid = librosa.feature.spectral_centroid(
y=y, sr=sr, n_fft=frame_length, hop_length=hop_length
)[0]
# Low Frequency Energy (LFE): 低頻能量 (計算 < 200Hz 的能量比例)
L = 200
n_bins = int(L * frame_length / sr)
stft = np.abs(librosa.stft(y, n_fft=frame_length, hop_length=hop_length))
lfe = np.sum(stft[:n_bins, :], axis=0) / (np.sum(stft, axis=0) + 1e-10)
print("🕵️‍♂️ Scanning for patterns...")
weather_events = []
# 滑動檢查
for i in range(len(rms)):
t = i * hop_length / sr
t_end = t + 10.0
r = rms[i]
f = flatness[i]
c = centroid[i]
l = lfe[i] if i < len(lfe) else 0
event_type = None
reason = ""
# 1. 雷聲偵測 (Thunder)
# 特徵:高能量 (響) + 低頻能量極高 (轟鳴)
if r > 0.08 and l > 0.4:
# 必須是低頻為主,且夠響
event_type = "Thunder"
reason = f"High LFE ({l:.2f}) & Loud"
# 2. 雨聲偵測 (Rain)
# 特徵:高平坦度 (噪音) + 持續能量 + 中頻質心
elif f > 0.30 and r > 0.015:
# 排除純靜音 (r 很低時 flatness 不準)
# 排除極低頻 (可能是風聲或空轉)
if 800 < c < 3000:
event_type = "Rain"
reason = f"High Flatness ({f:.2f}) & Mid Centroid"
if event_type:
weather_events.append(
{
"start": round(t, 1),
"end": round(t_end, 1),
"type": event_type,
"confidence": round(r + l + f, 4), # 簡單的綜合信心分數
"reason": reason,
}
)
return weather_events
if __name__ == "__main__":
if not os.path.exists(AUDIO_PATH):
print(f"❌ No audio found at {AUDIO_PATH}")
exit()
print(f"🌦️ Starting Weather Sound Analysis for {UUID}...")
events = detect_weather_sounds(AUDIO_PATH)
# 合併連續片段 (例如連續 3 個雨聲 -> 1 個大雨聲)
merged_events = []
for ev in events:
if not merged_events:
merged_events.append(ev)
continue
last = merged_events[-1]
# 如果同類型且時間重疊/相鄰 (間隔 < 5秒)
if ev["type"] == last["type"] and (ev["start"] - last["end"]) < 5.0:
last["end"] = ev["end"]
last["confidence"] = max(last["confidence"], ev["confidence"])
else:
merged_events.append(ev)
print("\n🎉 Analysis Complete!")
print(f"✅ Found {len(merged_events)} weather segments.")
# 統計
rain_count = sum(1 for e in merged_events if e["type"] == "Rain")
thunder_count = sum(1 for e in merged_events if e["type"] == "Thunder")
print(f" 🌧️ Rain events: {rain_count}")
print(f" ⚡ Thunder events: {thunder_count}")
# 儲存
with open(OUTPUT_JSON, "w") as f:
json.dump({"weather_events": merged_events}, f, indent=2)
# 顯示 Top 20
print("\n🔥 Top Weather Moments (Sorted by Confidence):")
sorted_ev = sorted(merged_events, key=lambda x: x["confidence"], reverse=True)
for i, ev in enumerate(sorted_ev[:20]):
m, s = divmod(ev["start"], 60)
print(f" {i + 1:02d}. [{int(m):02d}:{s:05.2f}] {ev['type']} ({ev['reason']})")