Finding words in long IPA strings
For the magic system I need to find individual words in the ever accumulating string of IPA characters. To do that I hold a list of valid words in memory. When there is some text, I iterate over the dictionary, calculating a levenshtein distance between each word and start of the string. Then the word with the lowest score is taken and if the score is under a set threshold, the word is considered a match, and the matching part is remved from the buffer. To cover more cases different lengths of the string are checked. If no match is found, one character is removed from the string, and the process repeats, until all words are found.
Fun fact: When comparing IPA and other non-ascii strings you need to make sure you are considering graphemes and not characters, because in UTF-8 what we perceive as one symbol, can be encoded as many characters in memory.
So far this algorithm works quite well, though in the future it might need some performance optimization, or tuning, if it starts yelding too many false positives.
Other misc changes:
- Migrated main fn to use
clapfor parsing arguments - Added flake.nix with dev env so I can run this code on my NixOS laptop
- Added time information to the pipeline - Together with the audio from the mic, a timestamp is captured, and passed through entire pipeline up to the end, where it is used to discard text older then 200ms from the buffer, so that stale data doesn’t mess up new words.
Adding VAD
I noticed that the speech to IPA model likes to output random data when no one is talking, so I decided to add VAD - Voice Activity Detection - a system that detects if someone is talking, and only then passes the audio on. I estimated this to take me around 2 hours, and oh, how wrong I was?
It took 10x that, which is why this devlog documents 3x the normal amount of time. And it still isn’t fully ready! So let me tell you what I did and why it took so long.
In the Burn (ML/AI lib for Rust) discord I noticed a solution just dropped out of the sky: Merely few days before a Silero VAD model implementation was added to a library named bunsen. ML models take input as Tensors - multidimensional arrays, which do not say in what form or shape the data is expected. So I had to figure that out myself. Fortunetely it was just 512 elements long chunks of 16khz audio with no additional pre-processing. Knowing that I fed the microphone input to the model and began to watch it output complete nonsense unrelated to whether I was speaking or not. So I started debugging - checked my code, wrote an equivalent in python using the official silero implementation to compare results - completely different. So I cloned the bunsen library, wrote an unit test there and ran off a Opencode agent. I also contacted the library author. He found one major issue, and the agent identified a second problem and wrote a solutuion - Silero VAD expects 64 samples of the previous chunk to be passed along the 512 to work correctly. I cleaned up the code, which was quite a pain because rust-analyzer was super slow on that repository, and made a PR, but it wasn’t merged because the author went with his own way of implementing it.
The big refactor
Coming back to my project I realized that my code strucutre makes it very hard to integrate VAD into it. So I spent the next 6 hours of so refactoring almost every file in the codebase into a new pipeline system. It lets you compose a pipeline out of a producer, many processors, and a consumer. It makes use of async, which saves on the amount of threads used. Static type checking ensures you can’t compose parts that wouldn’t work together, and all elements can also define contracts on the size of data chunks they produce and consume.
I ran out of the character limit, so the rest of VAD story will be in next devlog.