音声入力するUserScript
PC chromeのみ動作確認
やったこと
音声入力中であることを分かりやすくした
カーソルのアイコンとメニュー
ショートカットでon off
ctrl-sに対応させたが、もっといいところ無いかな
入力中も候補テキストがでるようにした
喋ってる最中にEnterを押すと確定
todo
放置してると割と早く音声入力が終了する
再起動しまくるか
ボタンを押してる間だけ、録音みたいな感じで入力できると更に良さそう?
参考
script.jsconst 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()
})