| 
 | 1 | +" The current whisper ecosystem shows mighty powerful potential, but seems to  | 
 | 2 | +" lack the required structure to make a speech to text plugin frictionless.  | 
 | 3 | +" The most direct path forward will be to have a standalone library interfaced  | 
 | 4 | +" with vim's libcall  | 
 | 5 | +" Libcall only allows for a single argument(a string or number) and a single  | 
 | 6 | +" output (always a string).  | 
 | 7 | +" This... honestly fits well for common interactions as follows  | 
 | 8 | +"  init(modelname) -> (null string for success or error message)  | 
 | 9 | +"  unload(ignored) -> (null string for success or error message)  | 
 | 10 | +"   likely never needed  | 
 | 11 | +"  processCommand(newline separated commands string) -> commands  | 
 | 12 | +"   Should have support for consecutive commands  | 
 | 13 | +"  stream(maybe sentinel?) -> processed text.  | 
 | 14 | +"  | 
 | 15 | +" Support for streaming responses is desired, but care is needed to support  | 
 | 16 | +" backtracking when more refined output is available.  | 
 | 17 | +" Perhaps the greatest element of difficulty, speech input should be buffered  | 
 | 18 | +" and it should be possible to 'rewind' input to mask latency and pivot off  | 
 | 19 | +" modal changes. (If a command sends the editor to insert mode, stt should no  | 
 | 20 | +" longer be limited by the command syntax)  | 
 | 21 | +"  | 
 | 22 | +" For now though, a simple proof of concept shall suffice.  | 
 | 23 | +if !exists("g:whisper_dir")  | 
 | 24 | +   let g:whisper_dir = expand($WHISPER_CPP_HOME)  | 
 | 25 | +   if g:whisper_dir == ""  | 
 | 26 | +      echoerr "Please provide a path to the whisper.cpp repo in either the $WHISPER_CPP_HOME environment variable, or g:whisper_dir"  | 
 | 27 | +   endif  | 
 | 28 | +endif  | 
 | 29 | +if !exists("g:whisper_stream_path")  | 
 | 30 | +   if executable("stream")  | 
 | 31 | +      " A version of stream already exists in the path and should be used  | 
 | 32 | +      let g:whisper_stream_path = "stream"  | 
 | 33 | +   else  | 
 | 34 | +      let g:whisper_stream_path = g:whisper_dir .. "stream"  | 
 | 35 | +      if !filereadable(g:whisper_stream_path)  | 
 | 36 | +         echoerr "Was not able to locate a stream executable at: " .. g:whisper_stream_path  | 
 | 37 | +         throw "Executable not found"  | 
 | 38 | +      endif  | 
 | 39 | +   endif  | 
 | 40 | +endif  | 
 | 41 | +if !exists("g:whisper_model_path")  | 
 | 42 | +   " TODO: allow paths relative the repo dir  | 
 | 43 | +   let g:whisper_model_path = g:whisper_dir .. "models/ggml-base.en.bin"  | 
 | 44 | +   if !filereadable(g:whisper_model_path)  | 
 | 45 | +      echoerr "Could not find model at: " .. g:whisper_model_path  | 
 | 46 | +      throw "Model not found"  | 
 | 47 | +   endif  | 
 | 48 | +endif  | 
 | 49 | +let s:streaming_command = [g:whisper_stream_path,"-m",g:whisper_model_path,"-t","8","--step","0","--length","5000","-vth","0.6"]  | 
 | 50 | + | 
 | 51 | +let s:listening = v:false  | 
 | 52 | +let s:cursor_pos = getpos(".")  | 
 | 53 | +let s:cursor_pos[0] = bufnr("%")  | 
 | 54 | +let s:loaded = v:false  | 
 | 55 | +func s:callbackHandler(channel, msg)  | 
 | 56 | +   " Large risk of breaking if msg isn't line buffered  | 
 | 57 | +   " TODO: investigate sound_playfile as an indicator that listening has started?  | 
 | 58 | +   if a:msg == "[Start speaking]"  | 
 | 59 | +      let s:loaded = v:true  | 
 | 60 | +      if s:listening  | 
 | 61 | +         echo "Loading complete. Now listening"  | 
 | 62 | +      else  | 
 | 63 | +         echo "Loading complete. Listening has not been started"  | 
 | 64 | +      endif  | 
 | 65 | +   endif  | 
 | 66 | +   if s:listening  | 
 | 67 | +      let l:msg_lines = split(a:msg,"\n")  | 
 | 68 | +      let l:new_text = ""  | 
 | 69 | +      for l:line in l:msg_lines  | 
 | 70 | +         " This is sloppy, but will suffice until library is written  | 
 | 71 | +         if l:line[0] == '['  | 
 | 72 | +            let l:new_text = l:new_text .. l:line[28:-1] .. ' '  | 
 | 73 | +         endif  | 
 | 74 | +      endfor  | 
 | 75 | +         let l:buffer_line = getbufoneline(s:cursor_pos[0],s:cursor_pos[1])  | 
 | 76 | +      if len(l:buffer_line) == 0  | 
 | 77 | +         " As a special case, an empty line is instead set to the text  | 
 | 78 | +         let l:new_line = l:new_text  | 
 | 79 | +         let s:cursor_pos[2] = len(l:new_text)  | 
 | 80 | +      else  | 
 | 81 | +         " Append text after the cursor  | 
 | 82 | +         let l:new_line = strpart(l:buffer_line,0,s:cursor_pos[2]) .. l:new_text  | 
 | 83 | +         let l:new_line = l:new_line .. strpart(l:buffer_line,s:cursor_pos[2])  | 
 | 84 | +         let s:cursor_pos[2] = s:cursor_pos[2]+len(l:new_text)  | 
 | 85 | +      endif  | 
 | 86 | +      call setbufline(s:cursor_pos[0],s:cursor_pos[1],l:new_line)  | 
 | 87 | +   endif  | 
 | 88 | +endfunction  | 
 | 89 | + | 
 | 90 | +function! whisper#startListening()  | 
 | 91 | +   let s:cursor_pos = getpos(".")  | 
 | 92 | +   let s:cursor_pos[0] = bufnr("%")  | 
 | 93 | +   let s:listening = v:true  | 
 | 94 | +endfunction  | 
 | 95 | +function! whisper#stopListening()  | 
 | 96 | +   let s:listening = v:false  | 
 | 97 | +endfunction  | 
 | 98 | +function! whisper#toggleListening()  | 
 | 99 | +   let s:cursor_pos = getpos(".")  | 
 | 100 | +   let s:cursor_pos[0] = bufnr("%")  | 
 | 101 | +   let s:listening = !s:listening  | 
 | 102 | +   if s:loaded  | 
 | 103 | +      if s:listening  | 
 | 104 | +         echo "Now listening"  | 
 | 105 | +      else  | 
 | 106 | +         echo "No longer listening"  | 
 | 107 | +      endif  | 
 | 108 | +   endif  | 
 | 109 | +endfunction  | 
 | 110 | + | 
 | 111 | +" Note this includes stderr at present. It's still filtered and helps debugging  | 
 | 112 | +let s:whisper_job = job_start(s:streaming_command, {"callback": "s:callbackHandler"})  | 
 | 113 | +" TODO: Check lifetime. If the script is resourced, is the existing  | 
 | 114 | +" s:whisper_job dropped and therefore killed?  | 
 | 115 | +if job_status(s:whisper_job) == "fail"  | 
 | 116 | +   echoerr "Failed to start whisper job"  | 
 | 117 | +endif  | 
0 commit comments