Fixed STT
This commit is contained in:
parent
56d0ef01fa
commit
74074093dc
@ -379,16 +379,13 @@ textToLeetAdvanced(text) {
|
|||||||
this.textarea.setAttribute("rows", "2");
|
this.textarea.setAttribute("rows", "2");
|
||||||
|
|
||||||
this.appendChild(this.textarea);
|
this.appendChild(this.textarea);
|
||||||
this.ttsButton = document.createElement("stt-button");
|
|
||||||
|
|
||||||
this.snekSpeaker = document.createElement("snek-speaker");
|
this.snekSpeaker = document.createElement("snek-speaker");
|
||||||
this.appendChild(this.snekSpeaker);
|
this.appendChild(this.snekSpeaker);
|
||||||
|
|
||||||
this.ttsButton.addEventListener("click", (e) => {
|
this.sttButton = document.createElement("stt-button");
|
||||||
this.snekSpeaker.enable()
|
this.appendChild(this.sttButton);
|
||||||
});
|
|
||||||
|
|
||||||
this.appendChild(this.ttsButton);
|
|
||||||
this.uploadButton = document.createElement("upload-button");
|
this.uploadButton = document.createElement("upload-button");
|
||||||
this.uploadButton.setAttribute("channel", this.channelUid);
|
this.uploadButton.setAttribute("channel", this.channelUid);
|
||||||
this.uploadButton.addEventListener("upload", (e) => {
|
this.uploadButton.addEventListener("upload", (e) => {
|
||||||
|
|||||||
@ -1,183 +1,299 @@
|
|||||||
// retoor <retoor@molodetz.nl>
|
// retoor <retoor@molodetz.nl>
|
||||||
|
|
||||||
class STTButton extends HTMLElement {
|
class STTButton extends HTMLElement {
|
||||||
static get observedAttributes() { return ['target']; }
|
|
||||||
|
|
||||||
simulateTypingWithEvents(element, text, delay = 100) {
|
|
||||||
let resolver = null;
|
|
||||||
const promise = new Promise((resolve) => resolver = resolve);
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
const triggerEvent = (type, key) => {
|
|
||||||
const event = new KeyboardEvent(type, {
|
|
||||||
key: key,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
});
|
|
||||||
element.dispatchEvent(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (index < text.length) {
|
|
||||||
const char = text.charAt(index);
|
|
||||||
|
|
||||||
triggerEvent('keydown', char);
|
|
||||||
|
|
||||||
if (element.isContentEditable) {
|
|
||||||
document.execCommand('insertText', false, char);
|
|
||||||
} else {
|
|
||||||
element.value += char;
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerEvent('keypress', char);
|
|
||||||
triggerEvent('keyup', char);
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
clearInterval(interval);
|
|
||||||
resolver();
|
|
||||||
}
|
|
||||||
}, delay);
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: 'open' });
|
this.attachShadow({ mode: 'open' });
|
||||||
|
this._isListening = false;
|
||||||
const btn = document.createElement('button');
|
this._recognition = null;
|
||||||
btn.setAttribute('part', 'button');
|
this._finalTranscript = '';
|
||||||
btn.setAttribute('aria-label', 'Start voice dictation');
|
this._interimTranscript = '';
|
||||||
btn.innerHTML = '🎤';
|
|
||||||
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
:host { display:inline-block; }
|
:host {
|
||||||
button { all:unset; cursor:pointer; }
|
display: inline-block;
|
||||||
button:hover { background:#e8e8e8; }
|
}
|
||||||
:host([listening]) button {
|
.stt-container {
|
||||||
background:#d32f2f; color:#fff;
|
position: relative;
|
||||||
animation:pulse 1.2s ease-in-out infinite; }
|
}
|
||||||
|
.stt-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin-left: 10px;
|
||||||
|
background-color: #000000;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||||
|
}
|
||||||
|
.stt-button:hover {
|
||||||
|
background-color: #222222;
|
||||||
|
}
|
||||||
|
.stt-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
:host([listening]) .stt-button {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%,100% { transform:scale(1); box-shadow:0 0 0 0 rgba(211,47,47,.6);}
|
0%, 100% {
|
||||||
50% { transform:scale(1.12);box-shadow:0 0 0 12px rgba(211,47,47,0);}
|
box-shadow: 0 0 0 0 rgba(211, 47, 47, 0.6);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(211, 47, 47, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stt-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
this.shadowRoot.append(style, btn);
|
|
||||||
|
|
||||||
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
const container = document.createElement('div');
|
||||||
if (!SR) {
|
container.className = 'stt-container';
|
||||||
console.warn('Web Speech API not supported in this browser.');
|
|
||||||
|
this._button = document.createElement('button');
|
||||||
|
this._button.className = 'stt-button';
|
||||||
|
this._button.setAttribute('aria-label', 'Voice input');
|
||||||
|
this._button.innerHTML = '🎤';
|
||||||
|
this._button.addEventListener('click', () => this._toggle());
|
||||||
|
|
||||||
|
container.appendChild(this._button);
|
||||||
|
this.shadowRoot.append(style, container);
|
||||||
|
|
||||||
|
this._initRecognition();
|
||||||
|
}
|
||||||
|
|
||||||
|
_initRecognition() {
|
||||||
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
|
||||||
|
if (!SpeechRecognition) {
|
||||||
|
this._button.disabled = true;
|
||||||
|
this._button.title = 'Speech recognition not supported';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recog = new SR();
|
this._recognition = new SpeechRecognition();
|
||||||
this.recog.lang = 'en-US';
|
this._recognition.continuous = true;
|
||||||
this.recog.continuous = true;
|
this._recognition.interimResults = true;
|
||||||
this.recog.interimResults = true;
|
this._recognition.lang = navigator.language || 'en-US';
|
||||||
|
|
||||||
|
this._recognition.onstart = () => {
|
||||||
|
this._isListening = true;
|
||||||
|
this.setAttribute('listening', '');
|
||||||
|
this._finalTranscript = '';
|
||||||
|
this._interimTranscript = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
this._recognition.onend = () => {
|
||||||
|
if (this._isListening) {
|
||||||
|
try {
|
||||||
|
this._recognition.start();
|
||||||
|
} catch (e) {
|
||||||
|
this._stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._recognition.onerror = (event) => {
|
||||||
|
if (event.error === 'no-speech') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.error === 'aborted') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.warn('Speech recognition error:', event.error);
|
||||||
|
if (event.error === 'not-allowed') {
|
||||||
|
this._button.disabled = true;
|
||||||
|
this._button.title = 'Microphone access denied';
|
||||||
|
}
|
||||||
|
this._stop();
|
||||||
|
};
|
||||||
|
|
||||||
|
this._recognition.onresult = (event) => {
|
||||||
let interim = '';
|
let interim = '';
|
||||||
let committed = '';
|
|
||||||
let previousInterim = '';
|
|
||||||
|
|
||||||
this.recog.onresult = (e) => {
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||||
interim = '';
|
const result = event.results[i];
|
||||||
for (let i = e.resultIndex; i < e.results.length; i++) {
|
const transcript = result[0].transcript;
|
||||||
const res = e.results[i];
|
|
||||||
const alt = res[0];
|
|
||||||
const txt = alt.transcript.trim();
|
|
||||||
|
|
||||||
if (res.isFinal) {
|
if (result.isFinal) {
|
||||||
const sentence = txt.charAt(0).toUpperCase() + txt.slice(1);
|
let text = transcript.trim();
|
||||||
let punctuated = /[.!?]$/.test(sentence) ? sentence : sentence + '.';
|
text = text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
if (!/[.!?]$/.test(text)) {
|
||||||
|
text += '.';
|
||||||
|
}
|
||||||
|
this._finalTranscript += text + ' ';
|
||||||
|
this._updateTextarea(this._finalTranscript.trim());
|
||||||
|
this._finalize();
|
||||||
|
this._finalTranscript = '';
|
||||||
|
this._interimTranscript = '';
|
||||||
|
} else {
|
||||||
|
interim += transcript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
committed += punctuated + ' ';
|
this._interimTranscript = interim;
|
||||||
if (this.targetEl) {
|
if (interim) {
|
||||||
this.targetEl.focus();
|
this._updateTextarea(this._finalTranscript + interim);
|
||||||
punctuated = punctuated.replace(/\./g, ".
|
}
|
||||||
")
|
};
|
||||||
.replace(/\?/g, "?
|
}
|
||||||
")
|
|
||||||
.replace(/\!/g, "!
|
|
||||||
");
|
|
||||||
|
|
||||||
this.targetEl.value = punctuated; // punctuated;
|
_getTextarea() {
|
||||||
this.simulateTypingWithEvents(this.targetEl, ' ', 0).then(() => {
|
|
||||||
const chatInput = document.querySelector('chat-input');
|
const chatInput = document.querySelector('chat-input');
|
||||||
|
return chatInput ? chatInput.textarea : document.querySelector('textarea');
|
||||||
|
}
|
||||||
|
|
||||||
|
_getChatInput() {
|
||||||
|
return document.querySelector('chat-input');
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearTextarea() {
|
||||||
|
const textarea = this._getTextarea();
|
||||||
|
if (textarea) {
|
||||||
|
textarea.value = '';
|
||||||
|
}
|
||||||
|
const chatInput = this._getChatInput();
|
||||||
|
if (chatInput) {
|
||||||
|
chatInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateTextarea(text) {
|
||||||
|
const textarea = this._getTextarea();
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
textarea.focus();
|
||||||
|
textarea.value = text;
|
||||||
|
|
||||||
|
const chatInput = this._getChatInput();
|
||||||
|
if (chatInput) {
|
||||||
|
chatInput.value = text;
|
||||||
|
chatInput.updateFromInput(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_finalize() {
|
||||||
|
const chatInput = this._getChatInput();
|
||||||
|
if (chatInput) {
|
||||||
chatInput.finalizeMessage();
|
chatInput.finalizeMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_simulateEnterKey() {
|
||||||
|
const textarea = this._getTextarea();
|
||||||
|
if (!textarea) return;
|
||||||
|
|
||||||
|
textarea.focus();
|
||||||
|
|
||||||
|
const keydownEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
keyCode: 13,
|
||||||
|
which: 13,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
});
|
});
|
||||||
|
textarea.dispatchEvent(keydownEvent);
|
||||||
|
|
||||||
|
const keyupEvent = new KeyboardEvent('keyup', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
keyCode: 13,
|
||||||
|
which: 13,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
textarea.dispatchEvent(keyupEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousInterim = '';
|
_sendMessage() {
|
||||||
|
const chatInput = this._getChatInput();
|
||||||
|
if (chatInput && this._finalTranscript.trim()) {
|
||||||
|
chatInput.value = this._finalTranscript.trim();
|
||||||
|
chatInput.textarea.value = this._finalTranscript.trim();
|
||||||
|
|
||||||
|
this._simulateEnterKey();
|
||||||
|
|
||||||
|
this._finalTranscript = '';
|
||||||
|
this._interimTranscript = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_toggle() {
|
||||||
|
if (this._isListening) {
|
||||||
|
this._stopAndSend();
|
||||||
} else {
|
} else {
|
||||||
if (alt.confidence >= 0.85) {
|
this._start();
|
||||||
interim += txt + ' ';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interim && this.targetEl) {
|
_start() {
|
||||||
const el = this.targetEl;
|
if (!this._recognition) return;
|
||||||
el.focus();
|
|
||||||
|
|
||||||
if (el.isContentEditable) {
|
|
||||||
el.innerText = el.innerText.replace(new RegExp(previousInterim + '$'), '');
|
|
||||||
} else {
|
|
||||||
el.value = interim
|
|
||||||
// el.value = interim
|
|
||||||
//el.value = el.value.replace(new RegExp(previousInterim + '$'), interim);
|
|
||||||
}
|
|
||||||
document.querySelector('chat-input').sendMessage(interim);
|
|
||||||
this.simulateTypingWithEvents(el, ' ', 0).then(() => {
|
|
||||||
|
|
||||||
})
|
|
||||||
// this.simulateTypingWithEvents(el, interim, 0).then(() => {
|
|
||||||
// previousInterim = interim;
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.recog.onend = () => {
|
|
||||||
if (this.listening) {
|
|
||||||
try {
|
try {
|
||||||
this.recog.start();
|
this._recognition.start();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to restart speech recognition:', e);
|
if (e.name === 'InvalidStateError') {
|
||||||
}
|
this._recognition.stop();
|
||||||
}
|
setTimeout(() => {
|
||||||
};
|
|
||||||
|
|
||||||
btn.addEventListener('click', () => this.toggle());
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(name, _, newVal) {}
|
|
||||||
|
|
||||||
get targetEl() {
|
|
||||||
return document.querySelector("textarea") || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {}
|
|
||||||
|
|
||||||
get listening() {
|
|
||||||
return this.hasAttribute('listening');
|
|
||||||
}
|
|
||||||
set listening(val) {
|
|
||||||
if (val) this.setAttribute('listening', '');
|
|
||||||
else this.removeAttribute('listening');
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle() {
|
|
||||||
if (!this.recog) return;
|
|
||||||
if (this.listening) {
|
|
||||||
this.recog.stop();
|
|
||||||
this.listening = false;
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
this.recog.start();
|
this._recognition.start();
|
||||||
this.listening = true;
|
} catch (err) {
|
||||||
} catch (e) {
|
console.warn('Failed to start recognition:', err);
|
||||||
console.warn('Error starting recognition:', e);
|
|
||||||
}
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_stop() {
|
||||||
|
this._isListening = false;
|
||||||
|
this.removeAttribute('listening');
|
||||||
|
if (this._recognition) {
|
||||||
|
try {
|
||||||
|
this._recognition.stop();
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopAndSend() {
|
||||||
|
if (this._finalTranscript.trim() || this._interimTranscript.trim()) {
|
||||||
|
if (this._interimTranscript.trim()) {
|
||||||
|
let text = this._interimTranscript.trim();
|
||||||
|
text = text.charAt(0).toUpperCase() + text.slice(1);
|
||||||
|
if (!/[.!?]$/.test(text)) {
|
||||||
|
text += '.';
|
||||||
|
}
|
||||||
|
this._finalTranscript += text + ' ';
|
||||||
|
}
|
||||||
|
this._updateTextarea(this._finalTranscript.trim());
|
||||||
|
this._finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._stop();
|
||||||
|
this._finalTranscript = '';
|
||||||
|
this._interimTranscript = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
const textarea = this._getTextarea();
|
||||||
|
if (textarea) {
|
||||||
|
textarea.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey && this._isListening) {
|
||||||
|
this._stopAndSend();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user