yum-slop/TaSTT
Free self-hosted STT for VRChat.
git clone https://git.yummers.dev/yum-slop/TaSTT
bce0853
master
1import io 2import joblib 3from logger import log ,log_err 4import numpy as np 5import pandas as pd 6from pathlib import Path 7import pronouncing 8import re 9import sys 10 11def count_syllables (word ): 12"""Count syllables in a word using pronouncing library with regex fallback.""" 13phones = pronouncing .phones_for_word (word .lower ()) 14if len (phones )== 0 : 15return 0 16return pronouncing .syllable_count (phones [0 ]) 17 18def text_syllable_count (text ): 19"""Count total syllables in text.""" 20words = re .findall (r'\b\w+\b' ,text ) 21return sum (count_syllables (word )for word in words ) 22 23class HallucinationFilter : 24"""Filter for detecting hallucinated segments in speech-to-text output.""" 25def __init__ (self ,cfg ,model_path :Path = None ): 26""" 27Initialize the hallucination filter. 28Args: 29model_path: Optional path to the model file. If not provided, 30uses the default path. 31""" 32self .cfg = cfg 33self .model = None 34self .threshold = None 35self .features = None 36# Get the project root directory 37app_root = Path (__file__ ).resolve ().parent 38project_root = app_root .parent 39model_path = project_root / "Models" / "thankyou_filter_gb.pkl" 40# Try to load the model 41log (f"Loading hallucination filter" ) 42bundle = joblib .load (model_path ) 43self .model = bundle ["model" ] 44self .threshold = bundle ["threshold" ] 45self .features = bundle ["features" ] 46log (f"Loaded hallucination filter model from { model_path } " ) 47def is_hallucination (self ,segment )-> bool : 48""" 49Check if a segment is likely a hallucination. 50Returns False if model is not available. 51Args: 52segment: A segment object with attributes avg_logprob, audio_len_s, 53no_speech_prob, compression_ratio, text, start, and end. 54Returns: 55bool: True if the segment is likely a hallucination, False otherwise. 56""" 57s = segment # Brevity 58 59if s .no_speech_prob == 0 : 60# no_speech is not available. Use fancy classifier trained on my 61# speech data. 62text = s .transcript 63duration = s .audio_len_s 64raw_duration = s .end_ts - s .start_ts 65n_syllables = text_syllable_count (text ) 66sps = n_syllables / duration 67raw_sps = n_syllables / raw_duration 68duration_ratio = raw_duration / duration 69X = pd .DataFrame ([[ 70s .avg_logprob , 71s .no_speech_prob , 72s .compression_ratio , 73np .log1p (duration ), 74np .log1p (sps ), 75np .log1p (raw_duration ), 76np .log1p (raw_sps ), 77duration_ratio , 78s .avg_logprob * duration 79 ]],columns = self .features ) 80# Get probability 81prob = self .model .predict_proba (X )[0 ,1 ] 82return prob >= self .threshold 83 84# If no_speech is set, use simpler filter. 85if s .no_speech_prob > 0.6 and s .avg_logprob < - 0.5 : 86if self .cfg ["enable_debug_mode" ]: 87f"Drop probable hallucination (case 1) " + 88f"(text=' { s . text } ', " + 89f"no_speech_prob= { s . no_speech_prob } , " + 90f"avg_logprob= { s . avg_logprob } )" ,file = sys .stderr ) 91return True 92# Another touchup targeted at the vexatious "thanks for watching!" 93# hallucination. This triggers a lot when listening to 94# instrumental/electronic music. 95if s .no_speech_prob > 0.15 and s .avg_logprob < - 0.7 : 96if self .cfg ["enable_debug_mode" ]: 97f"Drop probable hallucination (case 2) " + 98f"(text=' { s . text } ', " + 99f"no_speech_prob= { s . no_speech_prob } , " + 100f"avg_logprob= { s . avg_logprob } )" ,file = sys .stderr ) 101return True 102return False 103