music-lens 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/analyze.py +52 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "music-lens",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Let a text-only model read a song: lyrics + timestamps, loudness, brightness, key, tempo, sections. Local, no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@ Usage:
16
16
  import argparse
17
17
  import json
18
18
  import os
19
+ import re
19
20
  import sys
20
21
  import warnings
21
22
  from pathlib import Path
@@ -47,6 +48,35 @@ MINOR = np.array([6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.
47
48
  NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
48
49
  NOTES_ZH = ["C", "升C", "D", "升D", "E", "F", "升F", "G", "升G", "A", "升A", "B"]
49
50
 
51
+ # Whisper famously fabricates credits/subtitles when the audio carries no speech
52
+ # (intros, outros, instrumental breaks). These are stock phrases from its training
53
+ # data -- e.g. "作词 李宗盛" is a near-universal Chinese lyric-video credit line.
54
+ HALLUCINATION_PATTERNS = [
55
+ r"作\s*[詞词]\s*[::]?\s*\S{2,4}\s*作曲", # "作词 X 作曲 Y"
56
+ r"作词曲", # "作词曲 李宗盛"
57
+ r"[詞词曲]\s*[::]\s*李宗盛",
58
+ r"李宗盛",
59
+ r"字幕\s*(by|BY|由|组|組|提供|制作|製作)",
60
+ r"中文字幕",
61
+ r"字幕组",
62
+ r"优优独播剧场",
63
+ r"YoYo Television",
64
+ r"Zither Harp",
65
+ r"请不吝点赞",
66
+ r"点赞.{0,4}订阅",
67
+ r"明镜与点点",
68
+ r"^\s*谢谢(观看|收看|收听)",
69
+ r"^\s*Thanks? for watching",
70
+ r"^\s*Subtitles?\s*by",
71
+ r"^\s*Amara\.org",
72
+ ]
73
+
74
+ _HALLUCINATION_RE = re.compile("|".join(HALLUCINATION_PATTERNS), re.IGNORECASE)
75
+
76
+
77
+ def is_hallucination(text: str) -> bool:
78
+ return bool(_HALLUCINATION_RE.search(text))
79
+
50
80
 
51
81
  def estimate_key(chroma_mean):
52
82
  best = (-2.0, None)
@@ -68,6 +98,8 @@ def main():
68
98
  ap.add_argument("--device", default="cpu")
69
99
  ap.add_argument("--skip-whisper", action="store_true")
70
100
  ap.add_argument("--vad", action="store_true", help="enable VAD (good for speech, bad for singing)")
101
+ ap.add_argument("--keep-hallucinations", action="store_true",
102
+ help="keep fabricated credit/subtitle lines instead of dropping them")
71
103
  args = ap.parse_args()
72
104
 
73
105
  src = Path(args.audio).expanduser().resolve()
@@ -131,6 +163,7 @@ def main():
131
163
  # ------------------------------------------------------------- transcription
132
164
  segments = []
133
165
  info = {}
166
+ dropped = []
134
167
  if not args.skip_whisper:
135
168
  print(f"[2/6] transcribing with faster-whisper '{args.model}' (lang={args.lang}) ...")
136
169
  from faster_whisper import WhisperModel
@@ -170,6 +203,21 @@ def main():
170
203
  if len(deduped) != len(segments):
171
204
  print(f" collapsed duplicates: {len(segments)} -> {len(deduped)} lines")
172
205
  segments = deduped
206
+
207
+ # drop fabricated credits / subtitles (whisper filler on instrumental parts)
208
+ dropped = []
209
+ if not args.keep_hallucinations:
210
+ kept = []
211
+ for seg in segments:
212
+ if is_hallucination(seg["text"]):
213
+ dropped.append(seg)
214
+ print(f" dropped hallucination @{seg['start']:6.1f}s {seg['text']}")
215
+ else:
216
+ kept.append(seg)
217
+ if kept:
218
+ segments = kept
219
+ elif dropped:
220
+ print(" (all segments looked like hallucinations -- keeping them)")
173
221
  else:
174
222
  print("[2/6] whisper skipped")
175
223
 
@@ -253,6 +301,7 @@ def main():
253
301
  "vocal_time_ratio": round(sing_ratio, 2),
254
302
  "sections": sections,
255
303
  "segments_count": len(segments),
304
+ "dropped_hallucinations": len(dropped),
256
305
  }
257
306
  with open(out / "05_summary.json", "w", encoding="utf-8") as f:
258
307
  json.dump(summary, f, ensure_ascii=False, indent=2)
@@ -269,6 +318,9 @@ def main():
269
318
  L.append(f"- 动态范围:{lo_d} dB ~ {hi_d} dB(相对最响处)")
270
319
  L.append(f"- 人声占比:约 **{sing_ratio*100:.0f}%** 时长检测到人声")
271
320
  L.append(f"- 歌词字数:约 {total_lyrics} 字,{len(segments)} 句\n")
321
+ if dropped:
322
+ L.append(f"> 已过滤 {len(dropped)} 条疑似幻觉(片头/署名/字幕类)。"
323
+ f"想保留请加 `--keep-hallucinations`。\n")
272
324
 
273
325
  L.append("## 段落结构(按响度自动切分)\n")
274
326
  for s in sections: