Skip to content

Commit 97f4a7f

Browse files
authored
examples : add Vim plugin (#1131)
* Initial proof of concept Vim plugin At present, this is likely only slightly better than feature parity with the existing whisper.nvim Known issues: Trailing whitespace Up to an existing length(5 seconds) of speech may be processed when listening is enabled CPU cycles are spent processing speech even when not listening. Fixing these issues is likely dependent upon future efforts to create a dedicated library instead of wrapping examples/stream * Support $WHISPER_CPP_HOME environment variable A minor misunderstanding of the whisper.nvim implementation resulted in a plugin that was functional, but not a drop in replacement as it should be now.
1 parent 3998465 commit 97f4a7f

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed

examples/whisper.nvim/whisper.vim

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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

Comments
 (0)