Views
No views yet
pip install -U "funasr>=1.2.7" torch1#!/usr/bin/env python3
2from pathlib import Path
3import os
4import argparse
5
6from huggingface_hub import snapshot_download
7from funasr import AutoModel
8from funasr.utils.postprocess_utils import rich_transcription_postprocess
9
10HF_REPO_ID = "AeiROBOT/SenseVoice-Small-ko" # 업로드한 HF 리포 ID
11LOCAL_DIR = "/home/khw/Workspace/SenseVoice/hf_models/SenseVoice-Small-ko"
12
13# ----- SenseVoice 토큰 파서 -----
14LANG_TOKENS = {"<|zh|>", "<|en|>", "<|yue|>", "<|ja|>", "<|ko|>", "<|nospeech|>"}
15EMO_TOKENS = {"<|HAPPY|>", "<|SAD|>", "<|ANGRY|>", "<|NEUTRAL|>", "<|FEARFUL|>", "<|DISGUSTED|>", "<|SURPRISED|>"}
16EVENT_TOKENS = {"<|BGM|>", "<|Speech|>", "<|Applause|>", "<|Laughter|>", "<|Cry|>", "<|Sneeze|>", "<|Breath|>", "<|Cough|>"}
17WITH_ITN_TOKENS = {"<|withitn|>", "<|woitn|>"}
18
19
20def _consume(prefixes, text: str):
21 for p in prefixes:
22 if text.startswith(p):
23 return p, text[len(p):]
24 return None, text
25
26
27def parse_sensevoice_text(raw: str):
28 """SenseVoice 출력 문자열에서 (lang, emo, event, with_itn, text) 분리.
29
30 예:
31 "<|ko|><|NEUTRAL|><|Speech|><|withitn|>조 금만 생각 을 하 면서 살 면 훨씬 편할 거야." ->
32 {
33 "language": "<|ko|>",
34 "emo": "<|NEUTRAL|>",
35 "event": "<|Speech|>",
36 "with_itn": "<|withitn|>",
37 "text": "조 금만 생각 을 하 면서 살 면 훨씬 편할 거야."
38 }
39 """
40 if not raw:
41 return {"language": None, "emo": None, "event": None, "with_itn": None, "text": ""}
42
43 rest = raw.strip()
44 lang, rest = _consume(LANG_TOKENS, rest)
45 emo, rest = _consume(EMO_TOKENS, rest)
46 event, rest = _consume(EVENT_TOKENS, rest)
47 with_itn, rest = _consume(WITH_ITN_TOKENS, rest)
48
49 clean_text = rest.strip()
50 return {
51 "language": lang,
52 "emo": emo,
53 "event": event,
54 "with_itn": with_itn,
55 "text": clean_text,
56 }
57
58
59def parse_args():
60 p = argparse.ArgumentParser()
61 p.add_argument("--wav_file", default="dataset/wav_dataset/DISGUSTED/test_2025_12_12_040201.wav", help="pretrained 모델 이름 또는 로컬 디렉터리")
62 return p.parse_args()
63
64def get_model():
65 local_path = snapshot_download(
66 repo_id=HF_REPO_ID,
67 repo_type="model",
68 local_dir=LOCAL_DIR,
69 local_dir_use_symlinks=False,
70 token=os.environ.get("HUGGINGFACE_HUB_TOKEN"), # private 이므로 필요
71 )
72 print("다운로드 경로:", local_path)
73
74 # 2) AutoModel에 로컬 경로를 넘겨서 사용
75 model_dir = local_path # 또는 LOCAL_DIR
76
77 model = AutoModel(
78 model=model_dir,
79 trust_remote_code=True,
80 remote_code=str(Path(model_dir) / "model.py"), # HF 리포에 있는 model.py 사용
81 vad_model="fsmn-vad",
82 vad_kwargs={"max_single_segment_time": 30000},
83 device="cuda:0",
84 )
85
86 return model
87
88def main():
89 args = parse_args()
90 wav_path = args.wav_file
91
92 model = get_model()
93
94 res = model.generate(
95 input=wav_path,
96 cache={},
97 language="auto", # 또는 "ko"
98 use_itn=True,
99 batch_size_s=60,
100 merge_vad=True,
101 merge_length_s=15,
102 )
103
104 raw_text = res[0]["text"]
105 parsed = parse_sensevoice_text(raw_text)
106
107 # ITN 후처리
108 pretty_text = rich_transcription_postprocess(parsed["text"]) if parsed["text"] else ""
109
110 print("=== Raw ===")
111 print(raw_text)
112 print("=== Parsed ===")
113 print("lang :", parsed["language"])
114 print("emo :", parsed["emo"])
115 print("event :", parsed["event"])
116 print("withitn:", parsed["with_itn"])
117 print("text :", pretty_text)
118
119
120if __name__ == "__main__":
121 main()1#!/usr/bin/env python3
2import os
3import json
4import argparse
5import unicodedata
6from pathlib import Path
7from typing import List, Dict, Tuple, Optional
8
9import torch
10from funasr import AutoModel
11from funasr.utils.postprocess_utils import rich_transcription_postprocess
12
13
14# =======================
15# SenseVoice 토큰 파서
16# =======================
17LANG_TOKENS = {"<|zh|>", "<|en|>", "<|yue|>", "<|ja|>", "<|ko|>", "<|nospeech|>"}
18EMO_TOKENS = {"<|HAPPY|>", "<|SAD|>", "<|ANGRY|>", "<|NEUTRAL|>", "<|FEARFUL|>", "<|DISGUSTED|>", "<|SURPRISED|>"}
19EVENT_TOKENS = {"<|BGM|>", "<|Speech|>", "<|Applause|>", "<|Laughter|>", "<|Cry|>", "<|Sneeze|>", "<|Breath|>", "<|Cough|>"}
20WITH_ITN_TOKENS = {"<|withitn|>", "<|woitn|>"}
21
22
23def _consume(prefixes, text: str):
24 for p in prefixes:
25 if text.startswith(p):
26 return p, text[len(p):]
27 return None, text
28
29
30def parse_sensevoice_text(raw: str) -> Dict[str, Optional[str]]:
31 if not raw:
32 return {"language": None, "emo": None, "event": None, "with_itn": None, "text": ""}
33
34 rest = raw.strip()
35 lang, rest = _consume(LANG_TOKENS, rest)
36 emo, rest = _consume(EMO_TOKENS, rest)
37 event, rest = _consume(EVENT_TOKENS, rest)
38 with_itn, rest = _consume(WITH_ITN_TOKENS, rest)
39
40 clean_text = rest.strip()
41 return {
42 "language": lang,
43 "emo": emo,
44 "event": event,
45 "with_itn": with_itn,
46 "text": clean_text,
47 }
48
49
50# =======================
51# 텍스트 정규화 & 지표
52# =======================
53
54def normalize_text(s: str, lower: bool, strip_punct: bool, strip_spaces: bool) -> str:
55 if s is None:
56 return ""
57 t = s
58 if lower:
59 t = t.lower()
60 if strip_punct:
61 t = "".join(ch for ch in t if not unicodedata.category(ch).startswith("P"))
62 if strip_spaces:
63 t = "".join(t.split())
64 return t
65
66
67def _levenshtein(a: List[str], b: List[str]) -> int:
68 n, m = len(a), len(b)
69 if n == 0:
70 return m
71 if m == 0:
72 return n
73 prev = list(range(m + 1))
74 for i in range(1, n + 1):
75 curr = [i] + [0] * m
76 ai = a[i - 1]
77 for j in range(1, m + 1):
78 cost = 0 if ai == b[j - 1] else 1
79 curr[j] = min(
80 prev[j] + 1,
81 curr[j - 1] + 1,
82 prev[j - 1] + cost,
83 )
84 prev = curr
85 return prev[m]
86
87
88def cer(ref: str, hyp: str) -> float:
89 r = list(ref)
90 h = list(hyp)
91 dist = _levenshtein(r, h)
92 return dist / max(1, len(r))
93
94
95def wer(ref: str, hyp: str) -> float:
96 r = ref.split()
97 h = hyp.split()
98 dist = _levenshtein(r, h)
99 return dist / max(1, len(r))
100
101
102def norm_emo(label: Optional[str]) -> str:
103 if not label:
104 return ""
105 t = label.strip()
106 if t.startswith("<|") and t.endswith("|>"):
107 t = t[2:-2]
108 return t.upper()
109
110
111# =======================
112# IO & argparse
113# =======================
114
115def parse_args():
116 p = argparse.ArgumentParser()
117 p.add_argument("--model-dir", default="/home/khw/Workspace/SenseVoice/outputs", help="finetune 산출물 디렉터리")
118 p.add_argument("--jsonl", default="/home/khw/Workspace/SenseVoice/data/train.jsonl", help="입력 JSONL 경로")
119 p.add_argument("--base-audio-dir", default="/home/khw/Workspace/SenseVoice", help="source 상대경로의 기준 디렉터리")
120 p.add_argument("--remote-code", default="/home/khw/Workspace/SenseVoice/model.py", help="SenseVoice 모델 구현 경로")
121 p.add_argument("--device", default=None, help="cuda:0 / cpu (미지정 시 자동 결정)")
122 p.add_argument("--batch-size", type=int, default=64, help="배치 크기(짧은 음원 다수 가정)")
123 p.add_argument("--use-best-ckpt", action="store_true", help="model.pt.best를 model.pt로 심볼릭 링크 생성")
124 p.add_argument("--lang", default="ko", choices=["auto", "zh", "en", "yue", "ja", "ko", "nospeech"], help="언어 강제 설정. 기본 ko")
125 p.add_argument("--lower", action="store_true", help="정밀도 계산 시 소문자화")
126 p.add_argument("--strip-punct", action="store_true", help="정밀도 계산 시 문장부호 제거")
127 p.add_argument("--strip-spaces", action="store_true", help="정밀도 계산 시 모든 공백 제거")
128 p.add_argument("--out", default="/home/khw/Workspace/SenseVoice/results/preds_train.jsonl", help="추론 결과 JSONL")
129 return p.parse_args()
130
131
132def _find_latest_epoch_ckpt(model_dir: Path) -> Optional[Path]:
133 """model.pt.ep* 중에서 가장 큰 epoch 번호를 가진 체크포인트를 찾는다."""
134 candidates = []
135 for p in model_dir.glob("model.pt.ep*"):
136 name = p.name
137 try:
138 # 이름에서 숫자 부분만 파싱: model.pt.ep50 -> 50
139 ep_str = name.split("model.pt.ep", 1)[1]
140 ep = int(ep_str)
141 candidates.append((ep, p))
142 except (IndexError, ValueError):
143 # 패턴이 안 맞으면 무시
144 continue
145
146 if not candidates:
147 return None
148
149 candidates.sort(key=lambda x: x[0]) # epoch 오름차순 정렬
150 return candidates[-1][1] # 가장 큰 epoch
151
152
153def prepare_checkpoint(model_dir: Path) -> Path:
154 """주어진 model_dir 안에서 사용할 체크포인트를 선택하고, model.pt를 준비한다.
155
156 우선순위:
157 1) model.pt.best
158 2) model.pt.ep* 중 가장 큰 epoch
159 3) model.pt (기존 파일)
160
161 셋 다 없으면 SystemExit으로 종료.
162
163 선택된 파일이 model.pt가 아니라면, model.pt를 해당 파일을 가리키는
164 심볼릭 링크(또는 복사본)으로 만든다.
165 """
166 best = model_dir / "model.pt.best"
167 target = model_dir / "model.pt" # AutoModel이 최종적으로 보게 될 파일
168
169 chosen: Optional[Path] = None
170
171 # 1) model.pt.best 최우선
172 if best.exists():
173 chosen = best
174 reason = "model.pt.best"
175 else:
176 # 2) 가장 마지막 epoch의 model.pt.ep*
177 latest_ep = _find_latest_epoch_ckpt(model_dir)
178 if latest_ep is not None:
179 chosen = latest_ep
180 reason = latest_ep.name
181 # 3) 기존 model.pt
182 elif target.exists():
183 chosen = target
184 reason = "existing model.pt"
185 else:
186 reason = "(none)"
187
188 if chosen is None:
189 raise SystemExit(
190 f"[fatal] No checkpoint found in {model_dir}. "
191 f"Expected one of: model.pt.best, model.pt.ep*, model.pt. Program will exit."
192 )
193
194 # 선택된 체크포인트를 model.pt로 맞춰준다 (링크 또는 복사)
195 if chosen != target:
196 if target.exists() or target.is_symlink():
197 try:
198 target.unlink()
199 except Exception as e:
200 print(f"[warn] failed to remove existing {target}: {e}")
201
202 try:
203 # 상대 이름으로 심볼릭 링크 생성
204 target.symlink_to(chosen.name)
205 print(f"[info] using checkpoint: {chosen.name} (linked as model.pt)")
206 except Exception as e:
207 # 일부 파일시스템/권한 환경에서 symlink가 안 될 수 있으므로, 복사로 폴백
208 print(f"[warn] symlink failed ({e}), will try to copy instead.")
209 import shutil
210 try:
211 shutil.copy2(str(chosen), str(target))
212 print(f"[info] using checkpoint: {chosen.name} (copied to model.pt)")
213 except Exception as e2:
214 raise SystemExit(
215 f"[fatal] failed to prepare checkpoint at {target}: {e2}. Program will exit."
216 )
217 else:
218 print(f"[info] using checkpoint: {reason}")
219
220 return chosen
221
222
223def load_items(jsonl_path: Path) -> List[Dict]:
224 items = []
225 with jsonl_path.open("r", encoding="utf-8") as f:
226 for line in f:
227 line = line.strip()
228 if not line:
229 continue
230 try:
231 obj = json.loads(line)
232 items.append(obj)
233 except Exception as e:
234 print(f"[warn] skip bad line: {e}")
235 return items
236
237
238def to_abs_paths(items: List[Dict], base_audio_dir: Path) -> Tuple[List[Dict], int]:
239 missing = 0
240 for it in items:
241 src = it.get("source")
242 if src:
243 p = (base_audio_dir / src).resolve()
244 if not p.exists():
245 missing += 1
246 it["abs_source"] = str(p)
247 else:
248 it["abs_source"] = None
249 missing += 1
250 return items, missing
251
252
253def batched(iterable, n: int):
254 batch = []
255 for x in iterable:
256 batch.append(x)
257 if len(batch) == n:
258 yield batch
259 batch = []
260 if batch:
261 yield batch
262
263
264# =======================
265# main
266# =======================
267
268def main():
269 args = parse_args()
270
271 model_dir = Path(args.model_dir)
272 jsonl_path = Path(args.jsonl)
273 base_audio_dir = Path(args.base_audio_dir)
274
275 # 체크포인트 우선순위 적용: model.pt.best > model.pt.ep* (최대 epoch) > model.pt
276 ckpt = prepare_checkpoint(model_dir)
277 print(f"[info] final checkpoint file: {ckpt}")
278
279 device = args.device or ("cuda:0" if torch.cuda.is_available() else "cpu")
280
281 # model.py(remote_code)는 반드시 존재해야 한다. 없으면 바로 종료.
282 remote_code_path = Path(args.remote_code)
283 if not remote_code_path.exists():
284 raise SystemExit(
285 f"[fatal] remote_code not found at {remote_code_path}. "
286 f"Expected model.py for SenseVoice. Program will exit."
287 )
288
289 trust_remote = True
290
291 model = AutoModel(
292 model=str(model_dir), # 로컬 디렉터리만 사용
293 trust_remote_code=trust_remote,
294 remote_code=str(remote_code_path),
295 device=device,
296 vad_model=None,
297 )
298
299 items = load_items(jsonl_path)
300 items, _ = to_abs_paths(items, base_audio_dir)
301
302 valid_items = [it for it in items if it.get("abs_source") and Path(it["abs_source"]).exists()]
303 missing = len(items) - len(valid_items)
304 if missing:
305 print(f"[warn] {missing} items skipped due to missing files")
306
307 out_path = Path(args.out)
308 out_path.parent.mkdir(parents=True, exist_ok=True)
309
310 total = len(valid_items)
311 print(f"[info] total inputs used: {total}, device: {device}, language: {args.lang}")
312 if total == 0:
313 print("[exit] No valid audio found. Check --base-audio-dir or 'source' paths.")
314 with out_path.open("w", encoding="utf-8") as wf:
315 pass
316 return
317
318 # 지표 누적
319 exact_matches = 0
320 cer_sum = 0.0
321 wer_sum = 0.0
322 text_pairs = 0
323
324 emo_correct = 0
325 emo_total = 0
326
327 written = 0
328 with out_path.open("w", encoding="utf-8") as wf:
329 for batch in batched(valid_items, args.batch_size):
330 wav_list = [b["abs_source"] for b in batch]
331
332 try:
333 res = model.generate(
334 input=wav_list,
335 cache={},
336 language=args.lang,
337 use_itn=True,
338 batch_size=len(wav_list),
339 )
340 except Exception as e:
341 print(f"[error] inference failed on batch starting key={batch[0].get('key')}: {e}")
342 continue
343
344 for it, r in zip(batch, res):
345 raw_text = r.get("text", "") or ""
346 parsed = parse_sensevoice_text(raw_text)
347 pretty_text = rich_transcription_postprocess(parsed["text"]) if parsed["text"] else ""
348
349 ref_text = it.get("target") or ""
350
351 # 텍스트 지표
352 if ref_text:
353 nt_ref = normalize_text(ref_text, args.lower, args.strip_punct, args.strip_spaces)
354 nt_hyp = normalize_text(pretty_text, args.lower, args.strip_punct, args.strip_spaces)
355
356 if nt_ref == nt_hyp:
357 exact_matches += 1
358 cer_sum += cer(nt_ref, nt_hyp)
359 wer_sum += wer(nt_ref, nt_hyp)
360 text_pairs += 1
361
362 # 감정 지표
363 tgt_emo_n = norm_emo(it.get("emo_target"))
364 pred_emo_n = norm_emo(parsed["emo"])
365 if tgt_emo_n:
366 emo_total += 1
367 if pred_emo_n == tgt_emo_n:
368 emo_correct += 1
369
370 out_obj = {
371 "key": it.get("key"),
372 "audio": it.get("abs_source"),
373 "pred_raw": raw_text,
374 "pred_text": pretty_text,
375 "ref_text": ref_text,
376 "pred_language": parsed["language"],
377 "pred_emo": pred_emo_n or parsed["emo"] or "",
378 "ref_emo": tgt_emo_n or it.get("emo_target") or "",
379 "pred_event": parsed["event"] or "",
380 "with_itn": parsed["with_itn"] or "",
381 }
382 wf.write(json.dumps(out_obj, ensure_ascii=False) + "\n")
383
384 # ===== 사람이 보기 좋은 per-sample 출력 =====
385 idx = written + 1
386 print("\n[{}] key={}".format(idx, it.get("key")))
387 print("REF_TEXT :", ref_text)
388 print("REF_EMO :", tgt_emo_n or it.get("emo_target"))
389 print("PRED_TEXT:", pretty_text)
390 print("PRED_EMO :", pred_emo_n or parsed["emo"]) # 토큰 그대로 보여줘도 됨
391 print("PRED_EVT :", parsed["event"]) # 이벤트도 같이 확인
392 print("-" * 80)
393
394 written += 1
395
396 # 요약 출력
397 print("\n===== Summary =====")
398 print(f"Samples inferred: {written}")
399 if text_pairs > 0:
400 exact_acc = exact_matches / text_pairs * 100.0
401 avg_cer = cer_sum / text_pairs
402 avg_wer = wer_sum / text_pairs
403 print(f"Text pairs (with ref): {text_pairs}")
404 print(f"- Exact match accuracy: {exact_acc:.2f}%")
405 print(f"- Avg CER: {avg_cer:.4f}")
406 print(f"- Avg WER: {avg_wer:.4f}")
407 else:
408 print("No text references found; text metrics skipped.")
409
410 if emo_total > 0:
411 emo_acc = emo_correct / emo_total * 100.0
412 print(f"Emotion pairs: {emo_total}")
413 print(f"- Emotion accuracy: {emo_acc:.2f}%")
414 else:
415 print("No emotion references found; emotion metrics skipped.")
416
417 print(f"Results saved to: {out_path}")
418
419
420if __name__ == "__main__":
421 main()
422
4231
2#!/usr/bin/env python3
3import os
4from pathlib import Path
5
6from huggingface_hub import HfApi, create_repo, upload_folder
7
8# ===== 사용자 설정 =====
9# 실제로 만들 Hugging Face 모델 repo ID (예시)
10REPO_ID = "AeiROBOT/SenseVoice-Small-ko" # <-- 원하는 이름으로 수정
11
12# 업로드할 로컬 폴더 (학습 결과)
13MODEL_DIR = Path("/home/khw/Workspace/SenseVoice/outputs")
14
15# 로컬에 있는 model.py를 함께 올리고 싶으면 (FunASR/SenseVoice용)
16# outputs 안에 이미 복사해 두었으면 생략 가능
17EXTRA_FILES = [
18 Path("/home/khw/Workspace/SenseVoice/model.py"), # 없으면 주석 처리
19]
20
21
22def main():
23 # 1) 토큰 가져오기 (환경변수 사용 권장)
24 # 미리 export HUGGINGFACE_HUB_TOKEN=hf_xxx 하기
25 token = os.environ.get("HUGGINGFACE_HUB_TOKEN")
26 if token is None:
27 raise RuntimeError(
28 "HUGGINGFACE_HUB_TOKEN 환경변수가 설정되어 있지 않습니다. "
29 "https://huggingface.co/settings/tokens 에서 토큰을 만들고,\n"
30 "export HUGGINGFACE_HUB_TOKEN=hf_xxx 로 설정한 뒤 다시 실행하세요."
31 )
32
33 api = HfApi()
34
35 # 2) 리포지터리 생성 (이미 있으면 exist_ok=True 로 그냥 통과)
36 create_repo(
37 repo_id=REPO_ID,
38 token=token,
39 private=True, # 비공개로 올리려면 True
40 exist_ok=True,
41 repo_type="model",
42 )
43
44 # 3) 추가로 올릴 파일(model.py 등)을 outputs 안으로 복사 (선택)
45 # -> HF 리포 root에 README.md, model.pt, config.yaml, configuration.json, model.py 등이 같이 있도록 추천
46 for extra in EXTRA_FILES:
47 if extra.is_file():
48 target = MODEL_DIR / extra.name
49 if not target.exists():
50 print(f"[info] copy {extra} -> {target}")
51 target.write_bytes(extra.read_bytes())
52 else:
53 print(f"[warn] extra file not found: {extra}")
54
55 # 3-1) 모델 카드(README) 업로드: 실행 위치(CWD)의 README_huggingface.md를 outputs/README.md로 복사
56 # - HF 모델 허브는 repo 루트의 README.md를 모델 카드로 인식합니다.
57 readme_src = Path.cwd() / "README_huggingface.md"
58 readme_dst = MODEL_DIR / "README.md"
59 if readme_src.is_file():
60 print(f"[info] copy {readme_src} -> {readme_dst}")
61 readme_dst.write_text(readme_src.read_text(encoding="utf-8"), encoding="utf-8")
62 else:
63 print(f"[warn] README_huggingface.md not found in CWD: {Path.cwd()}")
64
65 # 4) 폴더 통째로 업로드
66 print(f"[info] uploading folder: {MODEL_DIR} -> {REPO_ID}")
67 upload_folder(
68 repo_id=REPO_ID,
69 folder_path=str(MODEL_DIR),
70 path_in_repo=".", # 리포 루트에 그대로 올리기
71 token=token,
72 repo_type="model",
73 ignore_patterns=[
74 "model.pt.ep*", # 체크포인트들 제외
75 "*.pt.ep*", # 혹시 다른 파일명도 비슷하게 찍히면 같이 제외
76 ],
77 )
78
79 print("[done] uploaded to:", f"https://huggingface.co/{REPO_ID}")
80
81
82if __name__ == "__main__":
83 main()
84
85