if !exists("g:whisper_dir") let g:whisper_dir = expand($WHISPER_CPP_HOME) if g:whisper_dir == "" echoerr "Please provide a path to the whisper.cpp repo in either the $WHISPER_CPP_HOME environment variable, or g:whisper_dir" endif endif if !exists("g:whisper_lsp_path") let g:whisper_lsp_path = g:whisper_dir .. "lsp" if !filereadable(g:whisper_lsp_path) echoerr "Was not able to locate a lsp executable at: " .. g:whisper_lsp_path throw "Executable not found" endif endif if !exists("g:whisper_model_path") " TODO: allow custom paths relative to the repo dir let g:whisper_model_path = g:whisper_dir .. "models/ggml-base.en.bin" if !filereadable(g:whisper_model_path) echoerr "Could not find model at: " .. g:whisper_model_path throw "Model not found" endif endif let s:output_buffer = bufnr("whisper_log", v:true) call setbufvar(s:output_buffer,"&buftype","nofile") let s:lsp_command = [g:whisper_lsp_path,"-m",g:whisper_model_path] " For faster execution. TODO: server load multiple models/run multiple servers? " let s:lsp_command = [g:whisper_lsp_path, "-m", g:whisper_dir .. "models/ggml-tiny.en.bin", "-ac", "128"] " requestCommands([params_dict]) func whisper#requestCommands(...) let l:req = {"method": "guided", "params": {"commandset_index": 0}} if a:0 > 0 call extend(l:req.params, a:1) endif let resp = ch_sendexpr(g:lsp_job, l:req, {"callback": function("s:commandCallback", [l:req.params, 0])}) endfunction " doTranscription([params_dict]) func whisper#doTranscription(...) let l:req = {"method": "unguided", "params": {}} if a:0 > 0 call extend(l:req.params, a:1) endif let resp = ch_sendexpr(g:lsp_job, l:req, {"callback": function("s:transcriptionCallback", [function("s:insertText"),function("s:endTranscription")])}) endfunction " For testing func whisper#uppertest(cha) echo tr(a:cha, s:c_lowerkeys, s:c_upperkeys) endfunction " (upper, exit, count, motion, command, insert/append, save run) "base" " (upper, exit, count, motion, command, inside/around) "motion/visual" " (upper, exit, count, motion, line, inside/around) "command already entered" " (upper, exit, key, ) "from/till" " upper and lower keys is used to translate between cases with tr " Must be sunchronized let s:c_lowerkeys = "1234567890-=qwertyuiop[]\\asdfghjkl;'zxcvbnm,./\"" let s:c_upperkeys = "!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:\"ZXCVBNM<>?'" let s:c_count = split("1234567890\"",'\zs') let s:c_command = split("ryuogpdxcv.iam", '\zs') let s:c_motion = split("wetf'hjklnb$^)",'\zs') " object words: Word, Sentence, Paragraph, [, (, <, Tag, {. ", ' let s:c_area = split("wsp])>t}\"'",'\zs') "Special commands. let s:c_special_always = ["exit", "upper"] let s:c_special_normal = ["save", "run", "space"] " If not in dict, key is spoken word, " If key resolves to string, value is used for normal/motion, but key for chars " If key resolves to dict, {0: "normal",1: "motion",2:"single char",3: "area"} " Missing entries fall back as follows {0: "required", 1: 0, 2: "key", 3: 0} let s:spoken_dict = {"w": "word", "e": "end", "r": "replace", "t": {0: "till", 3: "tag"}, "y": "yank", "u": "undo", "i": {0: "insert", 1: "inside"}, "o": "open", "p": {0: "paste", 3: "paragraph"}, "a": {0: "append", 1: "around"}, "s": {0: "substitute", 3: "sentence"}, "d": "delete", "f": "from", "g": "go", "h": "left", "j": "down", "k": "up", "l": "right", "c": "change", "v": "visual", "b": "back", "n": "next", "m": "mark", ".": {0: "repeat", 2: "period"}, "]": {0: "bracket", 2: "bracket"}, "'": {0: "jump", 2: "apostrophe", 3: "apostrophe"}, '"': {0: 'register', 2: "quotation", 3: "quotation"}, "-": {0: "minus", 2: "minus"}, "$": {0: "dollar", 2: "dollar"}, "^": {0: "carrot", 2: "carrot"}, ")": {0: "sentence", 2: "parenthesis", 3: "parenthesis"}, "}": {0: "paragraph", 2: "brace", 3: "brace"}, ">": {0: "indent", 2: "angle", 3: "angle"}} " Give this another pass. This seems overly hacky even if it's functional let s:sub_tran_msg = "" func s:subTranProg(msg) if s:sub_tran_msg != "" let s:sub_tran_msg = s:sub_tran_msg .. a:msg if mode() !=? 'v' exe "normal" "u" .. s:sub_tran_msg endif else if s:command_backlog == "" " this should not occur call s:logCallback(0, "Warning: Encountered sub transcription without prior command") let s:command_backlog = "a" endif if a:msg[0] == ' ' let s:sub_tran_msg = s:command_backlog .. a:msg[1:-1] else let s:sub_tran_msg = s:command_backlog .. a:msg endif if mode() !=? 'v' exe "normal" s:sub_tran_msg endif endif call appendbufline(s:output_buffer, "$", s:sub_tran_msg .. ":" .. string(a:msg )) endfunction func s:subTranFinish(params, timestamp) let s:repeat_command = s:sub_tran_msg " Visual selection is lot if used with streaming, so streaming of partial " transcriptions is disabled in visual mode if mode() ==? 'v' exe "normal" s:sub_tran_msg endif let s:sub_tran_msg = "" let s:command_backlog = "" exe "normal a\u" let l:params = a:params let l:params.timestamp = a:timestamp if exists("l:params.commandset_index") unlet l:params.commandset_index endif call whisper#requestCommands(a:params) endfunction func s:logCallback(channel, msg) call appendbufline(s:output_buffer,"$",a:msg) endfunction func s:transcriptionCallback(progressCallback, finishedCallback, channel, msg) let l:tr = a:msg.result.transcription let l:ex_ind = match(tolower(l:tr),"exit", len(l:tr)-6) " The worst case I've observed so far is " Exit.", which is 6 characters if l:ex_ind != -1 call a:progressCallback(strpart(l:tr,0,l:ex_ind-1)) call a:finishedCallback(a:msg.result.timestamp) else call a:progressCallback(l:tr) let req = {"method": "unguided", "params": {"timestamp": a:msg.result.timestamp, "no_context": v:true}} let resp = ch_sendexpr(g:lsp_job, req, {"callback": function("s:transcriptionCallback", [a:progressCallback, a:finishedCallback])}) endif endfunc func s:insertText(msg) exe "normal a" .. a:msg endfunction func s:endTranscription(timestamp) call appendbufline(s:output_buffer, "$", "Ending unguided transcription") endfunction " If a command does not include a whole actionable step, attempting to execute " it discards the remainder of things. There is likely a simpler solution, " but it can be made functional now by storing a backbuffer until actionable let s:command_backlog = "" let s:repeat_command = "" let s:preceeding_upper = v:false func s:commandCallback(params, commandset_index, channel, msg) let l:command_index = a:msg.result.command_index let l:do_execute = v:false let l:next_mode = a:commandset_index let l:command = s:commandset_list[a:commandset_index][l:command_index] call s:logCallback(0, string(a:msg) .. " " .. a:commandset_index .. " " .. l:command) if l:command_index == 0 "exit "if s:command_backlog == "" call s:logCallback(0,"Stopping command mode") echo "No longer listening" let s:command_backlog = "" return "else " Legacy code to clear an existing buffer with exit. " Was found to be rarely desired and is better introduced as a " standalone command (clear?) " call s:logCallback(0,"Clearing command_backlog" .. s:command_backlog) " let s:command_backlog = "" " let s:preceeding_upper = v:false " endif elseif l:command_index == 1 " upper let s:preceeding_upper = !s:preceeding_upper elseif l:command == "save" " save and run can only happen in commandset 0, exe "w" elseif l:command == "run" exe "make run" elseif l:command == "space" exe "normal i \l" elseif has_key(s:c_user, l:command) let Userfunc = s:c_user[l:command] if type(Userfunc) == v:t_string let Userfunc = function(Userfunc) endif call Userfunc() else if s:preceeding_upper " Upper should keep commandset let s:preceeding_upper = v:false let l:visual_command = tr(l:command, s:c_lowerkeys, s:c_upperkeys) else let l:visual_command = l:command endif echo s:command_backlog .. " - " .. l:visual_command let s:command_backlog = s:command_backlog .. l:visual_command if a:commandset_index == 2 || a:commandset_index == 3 " single key, either completes motion, replace, or register " Should move to execute unless part of a register " Change will be caught at execute if s:command_backlog[-2:-2] !=# '"' call s:logCallback(0,"not register") let l:do_execute = v:true end let l:next_mode = 0 " commandset index only matters for a/i elseif (l:command == "a" || l:command == "i") && a:commandset_index == 1 " inside/around. Is commandset 3 let l:next_mode = 3 elseif l:command ==# '"' let l:next_mode = 2 elseif index(s:c_count, l:command) != -1 let l:next_mode = a:commandset_index elseif index(s:c_motion, l:command) != -1 if l:command == 't' || l:command == 'f' || l:command == "'" " prompt single key let l:next_mode = 2 else let l:do_execute = v:true let l:next_mode = 0 endif elseif index(s:c_command, l:command) != -1 if index(["y","g","d","c"], s:command_backlog[-1:-1]) != -1 && s:command_backlog[-1:-1] != s:command_backlog[-2:-2] && mode() !=? 'v' " need motion or repeated command " Potential for bad state here if disparaging command keys are " entered (i.e. yd), but vim can handle checks for this at exe " And checking for cases like y123d would complicate things let l:next_mode = 1 elseif index(["i","a","c", "o", "s"], l:command) != -1 || s:command_backlog[-1:-1] ==# 'R' "'Insert' mode, do general transcription let l:req = {"method": "unguided", "params": a:params} let l:req.params.timestamp = a:msg.result.timestamp let l:req.params.no_context = v:true let resp = ch_sendexpr(g:lsp_job, req, {"callback": function("s:transcriptionCallback", [function("s:subTranProg"), function("s:subTranFinish", [a:params])])}) return elseif l:command == 'r' || l:command == 'm' let l:next_mode = 2 elseif l:command == '.' let l:next_mode = 0 let l:do_execute = v:true let s:command_backlog = s:command_backlog[0:-2] .. s:repeat_command else if l:command ==? 'v' let l:next_mode = 1 else let l:next_mode = 0 endif let l:do_execute = v:true endif else throw "Invalid command state: " .. l:command .. " " .. a:commandset_index .. " " .. s:command_backlog endif endif if l:do_execute if mode() ==?'v' && l:next_mode == 0 let l:next_mode = 1 elseif match(s:command_backlog, 'c') != -1 let l:req = {"method": "unguided", "params": a:params} let l:req.params.timestamp = a:msg.result.timestamp let l:req.params.no_context = v:true let resp = ch_sendexpr(g:lsp_job, req, {"callback": function("s:transcriptionCallback", [function("s:subTranProg"), function("s:subTranFinish", [a:params])])}) return endif exe "normal" s:command_backlog if index(s:c_motion + ["u"],l:command) == -1 exe "normal a\u" let s:repeat_command = s:command_backlog call s:logCallback(0, s:command_backlog) endif let s:command_backlog = "" endif let l:req = {"method": "guided", "params": a:params} let l:req.params.timestamp = a:msg.result.timestamp let l:req.params.commandset_index = l:next_mode let resp = ch_sendexpr(g:lsp_job, l:req, {"callback": function("s:commandCallback",[a:params, l:next_mode])}) endfunction func s:loadedCallback(channel, msg) echo "Loading complete" call s:logCallback(a:channel, a:msg) endfunction func s:registerCommandset(commandlist, is_final) let req = {"method": "registerCommandset"} let req.params = a:commandlist call s:logCallback(0, join(a:commandlist)) call add(g:whisper_commandlist_spoken, a:commandlist) if a:is_final let resp = ch_sendexpr(g:lsp_job, req, {"callback": "s:loadedCallback"}) else let resp = ch_sendexpr(g:lsp_job, req, {"callback": "s:logCallback"}) endif endfunction func s:registerAllCommands() let l:normal = s:c_special_always + s:c_special_normal + s:c_count + s:c_command + s:c_motion + keys(s:c_user) let l:visual = s:c_special_always + s:c_count + s:c_command + s:c_motion " Currently the same as visual. " let l:post_command = s:c_special_always + s:c_count + s:c_command + s:c_motion let l:single_key = s:c_special_always + split(s:c_lowerkeys, '\zs') let l:area = s:c_special_always + s:c_area " Used only for compatibility with the testing script let g:whisper_commandlist_spoken = [] let s:commandset_list = [l:normal, l:visual, l:single_key, l:area] call s:registerCommandset(s:commandsetToSpoken(l:normal, 0), v:false) call s:registerCommandset(s:commandsetToSpoken(l:visual, 1), v:false) call s:registerCommandset(s:commandsetToSpoken(l:single_key, 2), v:false) call s:registerCommandset(s:commandsetToSpoken(l:area, 3), v:true) endfunction func s:commandsetToSpoken(commandset, spoken_index) let l:spoken_list = [] for l:command in a:commandset if has_key(s:spoken_dict, l:command) let l:spoken_value = s:spoken_dict[l:command] if type(l:spoken_value) == v:t_dict if has_key(l:spoken_value, a:spoken_index) let l:spoken_value = l:spoken_value[a:spoken_index] else if a:spoken_index == 2 let l:spoken_value = l:command else let l:spoken_value = l:spoken_value[0] endif endif else if a:spoken_index == 2 let l:spoken_value = l:command endif endif else let l:spoken_value = l:command endif call add(l:spoken_list, l:spoken_value) endfor return l:spoken_list endfunction " TODO: Check lifetime. If the script is resourced, is the existing " s:lsp_job dropped and therefore killed? " This seems to not be the case and I've had to deal with zombie processes " that survive exiting vim, even though said behavior conflicts with my " understanding of the provided documentation let s:lsp_opts = {"in_mode": "lsp", "out_mode": "lsp", "err_mode": "nl", "err_io": "buffer", "err_buf": s:output_buffer} if !exists("g:lsp_job") if exists("g:whisper_user_commands") let s:c_user = g:whisper_user_commands else let s:c_user = {} endif let g:lsp_job = job_start(s:lsp_command, s:lsp_opts) if job_status(g:lsp_job) == "fail" echoerr "Failed to start whisper job" endif call s:registerAllCommands() endif