[{"data":1,"prerenderedAt":205},["ShallowReactive",2],{"navigation":3,"\u002Fblog\u002Fwhisper":46,"\u002Fblog\u002Fwhisper-surround":201},[4],{"title":5,"path":6,"stem":7,"children":8,"page":45},"Blog","\u002Fblog","blog",[9,13,17,21,25,29,33,37,41],{"title":10,"path":11,"stem":12},"Audio Augmentation Techniques for Deep Learning","\u002Fblog\u002Faudio-augmentation","blog\u002Faudio-augmentation",{"title":14,"path":15,"stem":16},"Speed Up Whisper with CTranslate2","\u002Fblog\u002Fc-translate","blog\u002Fc-translate",{"title":18,"path":19,"stem":20},"Calling C++ and Go from Dart using FFI","\u002Fblog\u002Fdart-ffi-native-bridge","blog\u002Fdart-ffi-native-bridge",{"title":22,"path":23,"stem":24},"Russey Keo High School Memories","\u002Fblog\u002Fmemory","blog\u002Fmemory",{"title":26,"path":27,"stem":28},"My Dev Setup: Same Keys Everywhere","\u002Fblog\u002Fmy-dev-setup","blog\u002Fmy-dev-setup",{"title":30,"path":31,"stem":32},"Data Extraction with LangChain and Ollama Locally","\u002Fblog\u002Fpydantic-llm","blog\u002Fpydantic-llm",{"title":34,"path":35,"stem":36},"7-Segment LED Display with Proteus Simulation","\u002Fblog\u002Fseven-segment","blog\u002Fseven-segment",{"title":38,"path":39,"stem":40},"Understanding JWT Refresh Token Rotation & Revocation","\u002Fblog\u002Funderstand-refresh-token","blog\u002Funderstand-refresh-token",{"title":42,"path":43,"stem":44},"Khmer Speech Recognition","\u002Fblog\u002Fwhisper","blog\u002Fwhisper",false,{"id":47,"title":42,"author":48,"body":52,"date":190,"description":191,"extension":192,"image":193,"meta":194,"minRead":184,"navigation":198,"path":43,"seo":199,"stem":44,"__hash__":200},"blog\u002Fblog\u002Fwhisper.md",{"name":49,"avatar":50},"Phanith LIM",{"src":51,"alt":49},"\u002Fmyprofile.jpg",{"type":53,"value":54,"toc":182},"minimark",[55,59,62,67,70,73,77,80,83,89,108,113,124,127,130,134,137,140,149,152,157],[56,57,58],"p",{},"Whisper transcribes English well. On Khmer it mostly guesses.",[56,60,61],{},"So I fine-tuned it: custom scripts, a dataset I put together, and a training pipeline that runs end to end. Almost all of the effort went into the data, and almost none into the model.",[63,64,66],"h3",{"id":65},"why-khmer-is-awkward","Why Khmer is awkward",[56,68,69],{},"Khmer runs words together. No spaces between them, and almost no punctuation. A sentence arrives as one unbroken string of characters.",[56,71,72],{},"That breaks an assumption most speech pipelines quietly make, which is that you can line words up with sounds because you can see where the words are. Here you cannot, so the normalization step below carries weight that it would not carry in English.",[63,74,76],{"id":75},"cutting-audio-into-training-pairs","Cutting audio into training pairs",[56,78,79],{},"Whisper learns from short clips paired with the exact text spoken in them. My recordings were long, with a single transcript covering the whole file, so something has to decide where each word falls in time. That is forced alignment.",[56,81,82],{},"Two paths run in parallel and meet at the aligner.",[56,84,85],{},[86,87,88],"strong",{},"The transcript side",[90,91,92,96,99,102,105],"ol",{},[93,94,95],"li",{},"Start with the raw transcript.",[93,97,98],{},"Normalize it, fixing punctuation, casing and formatting.",[93,100,101],{},"Romanize the Khmer script into Latin characters.",[93,103,104],{},"Run uroman normalization so the text matches what the model expects.",[93,106,107],{},"Tokenize it into a token sequence.",[56,109,110],{},[86,111,112],{},"The audio side",[90,114,115,118,121],{},[93,116,117],{},"Start with the waveform.",[93,119,120],{},"Run it through the acoustic model.",[93,122,123],{},"Read off the emission probabilities, one distribution over tokens per frame.",[56,125,126],{},"The aligner takes the token sequence and those probabilities and works out which token was being spoken in which frame. What comes back is every token with a timestamp and a confidence score.",[56,128,129],{},"The timestamps give the cut points. The confidence scores are the useful part: a clip the aligner is unsure about is usually a clip where the transcript and the audio disagree, and those are worth dropping before they teach the model something wrong.",[63,131,133],{"id":132},"trying-it-out","Trying it out",[56,135,136],{},"The fine-tuned model runs in a small demo that takes a microphone or an uploaded file and returns the transcription. It is the screenshot at the top of this post.",[56,138,139],{},"The video walks through the results:",[56,141,142],{},[143,144,148],"a",{"href":145,"rel":146},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=kjtTuh6uOUA",[147],"nofollow","Watch the video on YouTube",[56,150,151],{},"Khmer speech recognition is still far behind English, and a small model trained on a small dataset is not going to close that. What this does show is that the gap is mostly a data problem, and data problems are the kind you can work at.",[153,154,156],"h4",{"id":155},"resources","Resources",[158,159,160,168,175],"ul",{},[93,161,162,167],{},[143,163,166],{"href":164,"rel":165},"https:\u002F\u002Fgithub.com\u002Fphanithlim\u002Ffine-tuning-whisper",[147],"phanithlim\u002Ffine-tuning-whisper",", the scripts and the pipeline",[93,169,170],{},[143,171,174],{"href":172,"rel":173},"https:\u002F\u002Fhuggingface.co\u002Fcollections\u002FPhanithLIM\u002Fkhmer-automation-speech-recognition-67d7f77f52ba357dc72abd4e",[147],"Models and datasets on Hugging Face",[93,176,177],{},[143,178,181],{"href":179,"rel":180},"https:\u002F\u002Fdrive.google.com\u002Fdrive\u002Ffolders\u002F1bApbUyeVRhIDEy0SzwvSQR8lka-mV_LM?usp=sharing",[147],"Report & Slide",{"title":183,"searchDepth":184,"depth":184,"links":185},"",2,[186,188,189],{"id":65,"depth":187,"text":66},3,{"id":75,"depth":187,"text":76},{"id":132,"depth":187,"text":133},"2025-02-27","Fine-tuning Whisper for Khmer, and why the data was the hard part.","md","\u002Fblog\u002Fwhisper\u002Fimage1.png",{"tags":195},[196,197],"Machine Learning","Audio",true,{"title":42,"description":191},"SMfWWO50jkoF4h7XkLR5XjDg7zT3HAFLJZtu6Hho_vU",[202,204],{"title":38,"path":39,"stem":40,"description":203,"children":-1},"Why a refresh token lives for 7 days, and what the revoke column is for.",null,1791981009390]