generated at
音声入力するUserScript
PC chromeのみ動作確認

やったこと
音声入力中であることを分かりやすくした
カーソルのアイコンとメニュー
ショートカットでon off
ctrl-sに対応させたが、もっといいところ無いかな
入力中も候補テキストがでるようにした
喋ってる最中にEnterを押すと確定

todo
放置してると割と早く音声入力が終了する
再起動しまくるか
ボタンを押してる間だけ、録音みたいな感じで入力できると更に良さそう?

参考


script.js
const isMobile = () => /mobile/i.test(navigator.userAgent) const writeText = text => { console.log('writeText', text) document.querySelector('.text-input').focus() document.execCommand('insertText', null, text) } const icons = { on: 'https://i.gyazo.com/0562c6a405a29661f18d0fdf8840065d.png', off: 'https://img.icons8.com/ios/4096/microphone.png', }; const pageMenuId = 'speech input' const setIcon = name => document.getElementById(pageMenuId).firstElementChild.src = icons[name] const textInput = document.querySelector('.text-input') function createViewer() { const wrapper = document.createElement("div") textInput.parentElement.append(wrapper) wrapper.style.position = 'absolute' wrapper.style.zIndex = 10000 const viewer = document.createElement("span") wrapper.append(viewer) viewer.style.color = '#777' const img = document.createElement("img") wrapper.append(img) img.src = icons.off img.style.width = '18px' img.style.height = '18px' const input = document.createElement('input') wrapper.append(input) input.style.opacity = '0' input.onkeydown = e => { if(e.key === 'Enter') { commit() ignoreNextCommitUntilFinish = true } } const getInputStyle = () => { const cursor = document.querySelector('.cursor') const {top,left,height} = cursor.style return {top,left,height} } function update(text) { viewer.hidden = false Object.assign(wrapper.style, getInputStyle()) } function updateText(text) { if(ignoreNextCommitUntilFinish) return input.focus() viewer.textContent = text } function clear() { update() updateText() input.blur() textInput.focus() } const move = (e) => update() document.addEventListener('keyup', move) document.addEventListener('pointerup', move) function unmount() { wrapper.remove() document.removeEventListener('keyup', move) document.removeEventListener('pointerup', move) textInput.focus() } move() scrapbox.on('lines:changed', move) let ignoreNextCommitUntilFinish; function commit(text) { if(ignoreNextCommitUntilFinish) { if(text) ignoreNextCommitUntilFinish = false clear() return } console.log('commit', text) writeText(text ?? viewer.textContent) clear() } return { updateText, commit, unmount } } function startRec() { const { updateText, commit, unmount } = createViewer() const rec = new webkitSpeechRecognition() rec.continuous = true rec.interimResults = true rec.onstart = () => { console.log('on start'); setIcon("on") } rec.onaudiostart = () => { console.log('on audio start') } rec.onsoundstart = () => { console.log('on sound start') } rec.onspeechstart = () => { console.log('on speech start')} rec.onspeechend = () => { console.log('on speech end') } rec.onsoundend = () => { console.log('on sound end') } rec.onaudioend = () => { console.log('on audio end') } rec.onend = () => { console.log('on end'); unmount(); setIcon("off") } rec.onerror = (e) => {console.log("error",e) } rec.onresult = (event) => { const { results } = event; for (let i = event.resultIndex; i<results.length; i++){ console.log(results[i]) const t = results[i][0].transcript if(results[i].isFinal) { console.log(t) commit(t+"\n") //喋るごとに改行を入れる仕様 } else { console.log("[途中経過]", t) updateText(t) }    } } return rec } let rec; function start() { rec = startRec() rec.start() document.addEventListener('keyup', e => { if (e.key == 'Escape') stop() }) } function stop() { rec.stop() rec = null } function toggle() { if (!rec) start() else stop() } scrapbox.PageMenu.addMenu({ title: pageMenuId, image: icons.off, onClick: () => { if(isMobile()) return toggle() } }) document.addEventListener('keydown', e => { if (e.key == 's' && e.ctrlKey) toggle() })