Fixed STT
This commit is contained in:
parent
56d0ef01fa
commit
74074093dc
@ -379,16 +379,13 @@ textToLeetAdvanced(text) {
|
||||
this.textarea.setAttribute("rows", "2");
|
||||
|
||||
this.appendChild(this.textarea);
|
||||
this.ttsButton = document.createElement("stt-button");
|
||||
|
||||
this.snekSpeaker = document.createElement("snek-speaker");
|
||||
this.appendChild(this.snekSpeaker);
|
||||
|
||||
this.ttsButton.addEventListener("click", (e) => {
|
||||
this.snekSpeaker.enable()
|
||||
});
|
||||
this.sttButton = document.createElement("stt-button");
|
||||
this.appendChild(this.sttButton);
|
||||
|
||||
this.appendChild(this.ttsButton);
|
||||
this.uploadButton = document.createElement("upload-button");
|
||||
this.uploadButton.setAttribute("channel", this.channelUid);
|
||||
this.uploadButton.addEventListener("upload", (e) => {
|
||||
|
||||
@ -1,183 +1,299 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
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() {
|
||||
super();
|
||||
this.attachShadow({ mode: 'open' });
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.setAttribute('part', 'button');
|
||||
btn.setAttribute('aria-label', 'Start voice dictation');
|
||||
btn.innerHTML = '🎤';
|
||||
this._isListening = false;
|
||||
this._recognition = null;
|
||||
this._finalTranscript = '';
|
||||
this._interimTranscript = '';
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
:host { display:inline-block; }
|
||||
button { all:unset; cursor:pointer; }
|
||||
button:hover { background:#e8e8e8; }
|
||||
:host([listening]) button {
|
||||
background:#d32f2f; color:#fff;
|
||||
animation:pulse 1.2s ease-in-out infinite; }
|
||||
:host {
|
||||
display: inline-block;
|
||||
}
|
||||
.stt-container {
|
||||
position: relative;
|
||||
}
|
||||
.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 {
|
||||
0%,100% { transform:scale(1); box-shadow:0 0 0 0 rgba(211,47,47,.6);}
|
||||
50% { transform:scale(1.12);box-shadow:0 0 0 12px rgba(211,47,47,0);}
|
||||
0%, 100% {
|
||||
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;
|
||||
if (!SR) {
|
||||
console.warn('Web Speech API not supported in this browser.');
|
||||
const container = document.createElement('div');
|
||||
container.className = 'stt-container';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
this.recog = new SR();
|
||||
this.recog.lang = 'en-US';
|
||||
this.recog.continuous = true;
|
||||
this.recog.interimResults = true;
|
||||
this._recognition = new SpeechRecognition();
|
||||
this._recognition.continuous = true;
|
||||
this._recognition.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 committed = '';
|
||||
let previousInterim = '';
|
||||
|
||||
this.recog.onresult = (e) => {
|
||||
interim = '';
|
||||
for (let i = e.resultIndex; i < e.results.length; i++) {
|
||||
const res = e.results[i];
|
||||
const alt = res[0];
|
||||
const txt = alt.transcript.trim();
|
||||
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||
const result = event.results[i];
|
||||
const transcript = result[0].transcript;
|
||||
|
||||
if (res.isFinal) {
|
||||
const sentence = txt.charAt(0).toUpperCase() + txt.slice(1);
|
||||
let punctuated = /[.!?]$/.test(sentence) ? sentence : sentence + '.';
|
||||
if (result.isFinal) {
|
||||
let text = transcript.trim();
|
||||
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 + ' ';
|
||||
if (this.targetEl) {
|
||||
this.targetEl.focus();
|
||||
punctuated = punctuated.replace(/\./g, ".
|
||||
")
|
||||
.replace(/\?/g, "?
|
||||
")
|
||||
.replace(/\!/g, "!
|
||||
");
|
||||
this._interimTranscript = interim;
|
||||
if (interim) {
|
||||
this._updateTextarea(this._finalTranscript + interim);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.targetEl.value = punctuated; // punctuated;
|
||||
this.simulateTypingWithEvents(this.targetEl, ' ', 0).then(() => {
|
||||
_getTextarea() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
_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 {
|
||||
if (alt.confidence >= 0.85) {
|
||||
interim += txt + ' ';
|
||||
}
|
||||
this._start();
|
||||
}
|
||||
}
|
||||
|
||||
if (interim && this.targetEl) {
|
||||
const el = this.targetEl;
|
||||
el.focus();
|
||||
_start() {
|
||||
if (!this._recognition) return;
|
||||
|
||||
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 {
|
||||
this.recog.start();
|
||||
this._recognition.start();
|
||||
} catch (e) {
|
||||
console.warn('Failed to restart speech recognition:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
if (e.name === 'InvalidStateError') {
|
||||
this._recognition.stop();
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.recog.start();
|
||||
this.listening = true;
|
||||
} catch (e) {
|
||||
console.warn('Error starting recognition:', e);
|
||||
this._recognition.start();
|
||||
} catch (err) {
|
||||
console.warn('Failed to start recognition:', err);
|
||||
}
|
||||
}, 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