Automatic Speech Recognition
Transformers
Safetensors
Japanese
moss_transcribe_diarize
text2text-generation
asr
japanese
custom_code
Instructions to use rose3/moss-anime with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use rose3/moss-anime with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("automatic-speech-recognition", model="rose3/moss-anime", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("rose3/moss-anime", trust_remote_code=True) model = AutoModelForMultimodalLM.from_pretrained("rose3/moss-anime", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download code/transcribe.py from rose3/moss-anime: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/rose3/moss-anime/resolve/main/code/transcribe.py
- Command line
-
hf download hf://rose3/moss-anime/code/transcribe.py
-
curl -L -o transcribe.py https://huggingface.co/rose3/moss-anime/resolve/main/code/transcribe.py
10.3 kB
| """Japanese anime / game voice ASR with speaker labels and timestamps (MOSS-Transcribe-Diarize fine-tuned on | |
| galgame voices). | |
| python transcribe.py input.wav # prints [start-end] Sxx: text | |
| python transcribe.py voices\\ --out result.jsonl # a folder, one JSON line per file | |
| python transcribe.py ep01.wav --srt ep01.srt | |
| python transcribe.py input.wav --num_beams 4 --punct_bias 1.0 | |
| Decoding safeguards (all on by default, see README): | |
| * loop guard bans a token that would extend a 1-4 token cycle repeated 8 times (ぇぇぇ…, ーーー…) | |
| * length cap max_new_tokens = 20 per second of audio + 64 | |
| * temperature fallback re-decode at T=0.2..0.8 while a segment is too repetitive (gzip ratio > 1.8) | |
| * silence gate no output for audio quieter than -55 dBFS; segments over such audio are dropped | |
| Long input is cut at quiet points into windows of at most --window seconds (speaker labels restart per window). | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import contextlib | |
| import copy | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import tempfile | |
| import zlib | |
| from pathlib import Path | |
| import numpy as np | |
| import soundfile as sf | |
| import soxr | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoProcessor, LogitsProcessor, LogitsProcessorList | |
| HERE = Path(__file__).resolve().parent | |
| sys.path.insert(0, str(HERE)) # bundled inference_utils | |
| from inference_utils import build_transcription_messages, prepare_inputs # noqa: E402 | |
| SR = 16000 | |
| SEG = re.compile(r"\[(\d+(?:\.\d+)?)\]\[(S\d+)\](.*?)\[(\d+(?:\.\d+)?)\]", re.S) | |
| AUDIO_EXT = {".wav", ".flac", ".ogg", ".opus", ".mp3", ".m4a"} | |
| def load(path) -> np.ndarray: | |
| a, sr = sf.read(str(path), dtype="float32", always_2d=True) | |
| a = a.mean(axis=1) | |
| return soxr.resample(a, sr, SR) if sr != SR else a | |
| def parse(text: str) -> list[tuple[float, float, str, str]]: | |
| return [(float(s), float(e), spk, txt) for s, spk, txt, e in SEG.findall(text)] | |
| def frame_db(a: np.ndarray, hop: float = 0.05) -> np.ndarray: | |
| n = int(hop * SR) | |
| if len(a) < n: | |
| a = np.pad(a, (0, n - len(a))) | |
| f = a[: len(a) // n * n].reshape(-1, n) | |
| return 10 * np.log10((f ** 2).mean(axis=1) + 1e-12) | |
| def compression_ratio(text: str) -> float: | |
| parts = [s[3] for s in parse(text)] or [re.sub(r"\[[^\]]*\]", "", text)] | |
| def cr(t: str) -> float: | |
| b = t.encode("utf-8") | |
| return len(b) / max(len(zlib.compress(b)), 1) if b else 0.0 | |
| return max(cr(p) for p in parts) | |
| class LoopGuard(LogitsProcessor): | |
| def __init__(self, reps: int = 8, max_period: int = 4): | |
| self.reps, self.max_period = reps, max_period | |
| def __call__(self, input_ids, scores): | |
| for b in range(input_ids.shape[0]): | |
| tail = input_ids[b, -self.reps * self.max_period:].tolist() | |
| for p in range(1, self.max_period + 1): | |
| need = p * self.reps | |
| if len(tail) >= need and tail[-need:] == tail[-p:] * self.reps: | |
| scores[b, tail[-p]] = -float("inf") | |
| break | |
| return scores | |
| class TokenBias(LogitsProcessor): | |
| def __init__(self, ids: list[int], bias: float): | |
| self.ids, self.bias = ids, bias | |
| def __call__(self, input_ids, scores): | |
| scores[:, self.ids] += self.bias | |
| return scores | |
| class Transcriber: | |
| def __init__(self, model_dir: str, device: str = "cuda", num_beams: int = 1, punct_bias: float = 0.0, | |
| fallback: bool = True, max_cr: float = 1.8, gate_db: float | None = -55.0): | |
| self.dev = device | |
| self.dtype = torch.bfloat16 if device == "cuda" else torch.float32 | |
| self.model = AutoModelForCausalLM.from_pretrained(model_dir, trust_remote_code=True, dtype=self.dtype, | |
| attn_implementation="sdpa").to(device).eval() | |
| self.proc = AutoProcessor.from_pretrained(model_dir, trust_remote_code=True) | |
| self.num_beams, self.punct_bias, self.fallback, self.max_cr, self.gate_db = num_beams, punct_bias, fallback, max_cr, gate_db | |
| tok = self.proc.tokenizer | |
| self.punct_ids = sorted({ids[0] for s in ("!", "?", "!", "?", "っ", "ッ", "っ!", "!!", "!?", "ッ!") | |
| if len(ids := tok.encode(s, add_special_tokens=False)) == 1}) | |
| def _amp(self): | |
| return torch.autocast("cuda", dtype=torch.bfloat16) if self.dev == "cuda" else contextlib.nullcontext() | |
| def _generate(self, inputs, dur: float, temp: float) -> str: | |
| gc = copy.deepcopy(self.model.generation_config) | |
| gc.do_sample = temp > 0 | |
| if temp > 0: | |
| gc.temperature, gc.top_p, gc.top_k = temp, 1.0, 0 | |
| elif self.num_beams > 1: | |
| gc.num_beams = self.num_beams | |
| gc.max_new_tokens = min(4096, int(dur * 20) + 64) | |
| procs = [LoopGuard()] + ([TokenBias(self.punct_ids, self.punct_bias)] if self.punct_bias else []) | |
| with torch.inference_mode(), self._amp(): | |
| out = self.model.generate(input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"], | |
| input_features=inputs["input_features"], | |
| audio_feature_lengths=inputs["audio_feature_lengths"], | |
| audio_chunk_mapping=inputs["audio_chunk_mapping"], | |
| generation_config=gc, logits_processor=LogitsProcessorList(procs)) | |
| prompt_len = int(inputs["attention_mask"][0].sum().item()) | |
| return self.proc.tokenizer.decode(out[0][prompt_len:], skip_special_tokens=True).strip() | |
| def window(self, audio: np.ndarray, tmp_path: Path) -> list[tuple[float, float, str, str]]: | |
| """Transcribe one window (<= ~90 s works best). Returns (start, end, speaker, text).""" | |
| frames = frame_db(audio) | |
| if self.gate_db is not None and frames.max(initial=-120.0) < self.gate_db: | |
| return [] | |
| sf.write(tmp_path, audio, SR, subtype="PCM_16") | |
| with self._amp(): | |
| inputs = prepare_inputs(self.proc, build_transcription_messages(str(tmp_path)), max_length=131072, | |
| device=torch.device(self.dev)).to(self.dev) | |
| dur = len(audio) / SR | |
| best = None | |
| for temp in (0.0,) + ((0.2, 0.4, 0.6, 0.8) if self.fallback else ()): | |
| text = self._generate(inputs, dur, temp) | |
| c = compression_ratio(text) | |
| if best is None or c < best[0]: | |
| best = (c, text) | |
| if c <= self.max_cr: | |
| break | |
| segs = parse(best[1]) | |
| if self.gate_db is not None: | |
| hop = 0.05 | |
| segs = [s for s in segs | |
| if frames[int(s[0] / hop):max(int(s[1] / hop), int(s[0] / hop) + 1)].max(initial=-120.0) >= self.gate_db] | |
| return segs | |
| def __call__(self, path: Path, window_sec: float = 90.0) -> list[dict]: | |
| audio = load(path) | |
| tmp = Path(tempfile.gettempdir()) / f"moss_asr_{os.getpid()}.wav" # the processor reads audio from a path | |
| out = [] | |
| try: | |
| for off, chunk in split_long(audio, window_sec): | |
| for s, e, spk, txt in self.window(chunk, tmp): | |
| out.append({"start": round(off + s, 2), "end": round(off + e, 2), "speaker": spk, "text": txt, | |
| "window_start": round(off, 2)}) | |
| finally: | |
| tmp.unlink(missing_ok=True) | |
| return out | |
| def split_long(audio: np.ndarray, window_sec: float): | |
| """Cut at the quietest 50 ms frame in the last third of each window.""" | |
| n, win = len(audio), int(window_sec * SR) | |
| pos = 0 | |
| while pos < n: | |
| if n - pos <= win: | |
| yield pos / SR, audio[pos:] | |
| return | |
| seg = audio[pos:pos + win] | |
| db = frame_db(seg) | |
| lo = len(db) * 2 // 3 | |
| cut = (lo + int(np.argmin(db[lo:]))) * int(0.05 * SR) | |
| yield pos / SR, audio[pos:pos + cut] | |
| pos += cut | |
| def srt_time(t: float) -> str: | |
| ms = int(round(t * 1000)) | |
| return f"{ms // 3600000:02d}:{ms // 60000 % 60:02d}:{ms // 1000 % 60:02d},{ms % 1000:03d}" | |
| def main(): | |
| ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("inputs", nargs="+", help="audio files or folders") | |
| ap.add_argument("--model", default=str(HERE.parent)) | |
| ap.add_argument("--out", help="write JSON lines (one per file) here") | |
| ap.add_argument("--srt", help="write an SRT subtitle file (single input only)") | |
| ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu") | |
| ap.add_argument("--num_beams", type=int, default=1) | |
| ap.add_argument("--punct_bias", type=float, default=0.0, help="logit bonus for !?っ (sentence-final marks)") | |
| ap.add_argument("--no_fallback", action="store_true") | |
| ap.add_argument("--gate_db", type=float, default=-55.0) | |
| ap.add_argument("--no_gate", action="store_true") | |
| ap.add_argument("--window", type=float, default=90.0, help="max seconds per decoding window") | |
| args = ap.parse_args() | |
| files = [] | |
| for p in map(Path, args.inputs): | |
| files += sorted(f for f in p.rglob("*") if f.suffix.lower() in AUDIO_EXT) if p.is_dir() else [p] | |
| tr = Transcriber(args.model, args.device, args.num_beams, args.punct_bias, not args.no_fallback, | |
| gate_db=None if args.no_gate else args.gate_db) | |
| fout = open(args.out, "w", encoding="utf-8") if args.out else None | |
| for f in files: | |
| segs = tr(f, args.window) | |
| if fout: | |
| fout.write(json.dumps({"audio": str(f), "text": "".join(s["text"] for s in segs), "segments": segs}, | |
| ensure_ascii=False) + "\n") | |
| fout.flush() | |
| print(f"== {f}") | |
| for s in segs: | |
| print(f"[{s['start']:7.2f}-{s['end']:7.2f}] {s['speaker']}: {s['text']}") | |
| if args.srt: | |
| with open(args.srt, "w", encoding="utf-8") as fs: | |
| for i, s in enumerate(segs, 1): | |
| fs.write(f"{i}\n{srt_time(s['start'])} --> {srt_time(s['end'])}\n{s['speaker']}: {s['text']}\n\n") | |
| if fout: | |
| fout.close() | |
| if __name__ == "__main__": | |
| main() | |