דילוג לתוכן
  • חוקי הפורום
  • פופולרי
  • לא נפתר
  • משתמשים
  • חיפוש גוגל בפורום
  • צור קשר
עיצובים
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • ברירת מחדל (ללא עיצוב (ברירת מחדל))
  • ללא עיצוב (ברירת מחדל)
כיווץ
מתמחים טופ
  1. דף הבית
  2. מחשבים וטכנולוגיה
  3. עזרה הדדית - מחשבים וטכנולוגיה
  4. שיתוף | תוסף וסקריפט לתמלול דיבור לטקסט בכל אתר - ללא API

שיתוף | תוסף וסקריפט לתמלול דיבור לטקסט בכל אתר - ללא API

מתוזמן נעוץ נעול הועבר עזרה הדדית - מחשבים וטכנולוגיה
35 פוסטים 10 כותבים 207 צפיות 8 עוקבים
  • מהישן לחדש
  • מהחדש לישן
  • הכי הרבה הצבעות
תגובה
  • תגובה כנושא
התחברו כדי לפרסם תגובה
נושא זה נמחק. רק משתמשים עם הרשאות מתאימות יוכלו לצפות בו.
  • אברהם גלסרא אברהם גלסר

    בניתי בעזרת ChatGPT כלי קטן שמוסיף כפתור מיקרופון קבוע בדפדפן, ומאפשר להכתיב טקסט ישירות לתוך שדות כתיבה באתרים.

    chrome-capture-2026-05-29 (1).gif

    מה הכלי עושה בפועל:

    מוסיף כפתור מיקרופון צף בכל אתר.
    כברירת מחדל מוצג רק כפתור התמלול עצמו, בלי תפריטים מסביב.
    יש חץ קטן לפתיחת כל האפשרויות.
    בלחיצה על החץ נפתחות האפשרויות: בחירת שפה, הגדרות, טקסט שתומלל, העתקה/הדבקה וכדומה.
    בלחיצה נוספת על החץ הכל נסגר שוב.

    בלחיצה על כפתור המיקרופון מתחיל תמלול דיבור לטקסט.
    לחיצה נוספת עוצרת את התמלול.
    הטקסט נכנס בזמן אמת לשדה הכתיבה הפעיל.
    עובד עם textarea, input ושדות כתיבה מתקדמים יותר כמו contenteditable.
    תומך בעברית, אנגלית ובכמה שפות נוספות.
    יש אפשרות לבחור שפת תמלול.
    אם אין שדה כתיבה פעיל, הטקסט מופיע בחלונית של הכלי ואפשר להעתיק אותו.

    אפשר להזיז את כפתור התמלול עם Alt + גרירה על הכפתור.
    המיקום נשמר גם אחרי רענון.
    המיקום נשמר בנפרד לכל אתר, כלומר אפשר לקבוע מיקום אחד ל-ChatGPT, מיקום אחר לפורום וכו׳.
    ככה תוכלו להתאים את המיקום שלו לכל אתר איפה שהכי מתאים לו להיות, למשל בChatGPT:

    341d49db-543d-4bc3-8371-9947e417f7b0-image.png

    אם הכפתור נעלם בגלל שהוזז החוצה, אפשר להחזיר אותו למיקום ברירת המחדל באתר הנוכחי עם:
    Alt + Shift + M
    או:
    Ctrl + Alt + M

    בנוסף, אם מוחקים ידנית תוך כדי תמלול טקסט שהכלי כתב בתוך תיבת הטקסט, הכלי מתעלם מהמחיקה ולא מחזיר את הטקסט הזה מחדש בסיום ההקלטה.

    הכלי מבוסס על מנגנון זיהוי הדיבור של הדפדפן, כך שאין צורך בשרת חיצוני או במפתח API משלכם. בפעם הראשונה הדפדפן יבקש הרשאת מיקרופון.

    יש שתי אפשרויות שימוש:

    אפשרות 1 - סקריפט Tampermonkey

    מתאים למי שכבר משתמש ב־Tampermonkey או רוצה להתקין userscript.

    התקנה:

    אפשרות א':
    תודה ל @מייבין-במקצת על הרעיון, על ידי כניסה לקישור הזה בדפדפן שבו מותקן התוסף Tampermonkey (בדרך הזו גם תמיד תהיו בגרסה הכי חדשה של הסקריפט אוטומטית).

    אפשרות ב':
    מתקינים Tampermonkey.
    יוצרים סקריפט חדש.
    מדביקים את הקוד.
    שומרים.
    מרעננים את האתר שבו רוצים להשתמש.

    הקוד לסקריפט:

    // ==UserScript==
    // @name         Universal Voice to Text - Floating Dictation Button
    // @namespace    https://chat.openai.com/
    // @version      2.6.0
    // @description  Floating draggable speech-to-text button for almost any website. Dictates into focused inputs, textareas and contenteditable editors.
    // @author       Avraham + ChatGPT
    // @match        http://*/*
    // @match        https://*/*
    // @run-at       document-idle
    // @grant        GM_getValue
    // @grant        GM_setValue
    // @grant        GM_registerMenuCommand
    // ==/UserScript==
    
    (() => {
      'use strict';
    
      const APP_ID = 'uvtt-floating-root';
      const STORAGE_PREFIX = 'uvtt_tm_';
      const INTERIM_ATTR = 'data-uvtt-interim';
    
      const DEFAULT_SETTINGS = {
        language: 'he-IL',
        continuous: true,
        autoCapitalize: true,
        addSpaceBeforeText: true,
        showStatus: true,
        showLanguageSelect: true
      };
    
      const DEFAULT_POSITION = {
        mode: 'corner',
        right: 24,
        bottom: 24,
        left: null,
        top: null
      };
    
      const LANGUAGES = [
        { value: 'he-IL', label: 'עברית' },
        { value: 'en-US', label: 'English US' },
        { value: 'en-GB', label: 'English UK' },
        { value: 'ar-SA', label: 'العربية' },
        { value: 'fr-FR', label: 'Français' },
        { value: 'es-ES', label: 'Español' },
        { value: 'ru-RU', label: 'Русский' }
      ];
    
      const INPUT_TYPES = new Set([
        '', 'text', 'search', 'email', 'url', 'tel', 'password', 'number'
      ]);
    
      const state = {
        mounted: false,
        listening: false,
        recognition: null,
        settings: { ...DEFAULT_SETTINGS },
        position: { ...DEFAULT_POSITION },
        currentEditor: null,
        lastFocusedEditor: null,
        statusText: 'מוכן',
        suppressNextEndRestart: false,
        lastTranscriptAt: 0,
        bufferText: '',
        discardUntilFinal: false,
    
        interim: {
          editor: null,
          type: null,
          node: null,
          prefix: '',
          rawText: '',
          previewText: '',
          inputStart: null,
          inputLength: 0
        },
    
        dragging: {
          active: false,
          moved: false,
          pointerId: null,
          startedOnMic: false,
          canDrag: false,
          startX: 0,
          startY: 0,
          startLeft: 0,
          startTop: 0
        }
      };
    
      let host;
      let shadow;
      let wrap;
      let micButton;
      let expandButton;
      let statusPill;
      let languageSelect;
      let settingsButton;
      let settingsPanel;
      let bufferPreview;
      let copyButton;
      let pasteButton;
      let clearButton;
      let resetPositionButton;
    
      function gmGet(key, fallback) {
        try {
          if (typeof GM_getValue === 'function') {
            return GM_getValue(`${STORAGE_PREFIX}${key}`, fallback);
          }
        } catch (_) {}
    
        try {
          const raw = localStorage.getItem(`${STORAGE_PREFIX}${key}`);
          return raw === null ? fallback : JSON.parse(raw);
        } catch (_) {
          return fallback;
        }
      }
    
      function gmSet(key, value) {
        try {
          if (typeof GM_setValue === 'function') {
            GM_setValue(`${STORAGE_PREFIX}${key}`, value);
            return;
          }
        } catch (_) {}
    
        try {
          localStorage.setItem(`${STORAGE_PREFIX}${key}`, JSON.stringify(value));
        } catch (_) {}
      }
    
      function getSiteStorageKey(name) {
        const origin = (() => {
          try {
            return window.location.origin || `${window.location.protocol}//${window.location.host}`;
          } catch (_) {
            return 'unknown-origin';
          }
        })();
    
        return `${name}:${origin}`;
      }
    
      function getPositionStorageKey() {
        return getSiteStorageKey('position');
      }
    
      function loadSettings() {
        const loaded = { ...DEFAULT_SETTINGS };
        for (const key of Object.keys(DEFAULT_SETTINGS)) {
          loaded[key] = gmGet(key, DEFAULT_SETTINGS[key]);
        }
        state.settings = loaded;
    
        const loadedPosition = gmGet(getPositionStorageKey(), DEFAULT_POSITION);
        state.position = {
          ...DEFAULT_POSITION,
          ...(loadedPosition && typeof loadedPosition === 'object' ? loadedPosition : {})
        };
      }
    
      function saveSetting(key, value) {
        state.settings[key] = value;
        gmSet(key, value);
    
        if (key === 'language' && state.recognition && state.listening) {
          setStatus('השפה תעודכן בהפעלה הבאה');
        }
    
        refreshUiVisibilitySettings();
      }
    
      function savePosition(position) {
        state.position = { ...state.position, ...position };
        gmSet(getPositionStorageKey(), state.position);
      }
    
      function supportsSpeechRecognition() {
        return Boolean(window.SpeechRecognition || window.webkitSpeechRecognition);
      }
    
      function getSpeechRecognitionCtor() {
        return window.SpeechRecognition || window.webkitSpeechRecognition;
      }
    
      function isInsideOwnUi(node) {
        if (!node) return false;
        return node === host || (host && host.contains(node));
      }
    
      function isEditableElement(element) {
        if (!element || !(element instanceof HTMLElement)) return false;
        if (isInsideOwnUi(element)) return false;
        if (element.isContentEditable) return true;
    
        const tag = element.tagName.toLowerCase();
        if (tag === 'textarea') return !element.disabled && !element.readOnly;
    
        if (tag === 'input') {
          const input = element;
          return INPUT_TYPES.has((input.getAttribute('type') || '').toLowerCase()) && !input.disabled && !input.readOnly;
        }
    
        return false;
      }
    
      function getEditorType(editor) {
        if (!editor) return null;
        const tag = editor.tagName?.toLowerCase();
        if (tag === 'textarea' || tag === 'input') return 'input';
        if (editor.isContentEditable) return 'contenteditable';
        return null;
      }
    
      function isElementVisible(el) {
        if (!el || !(el instanceof HTMLElement)) return false;
        const rect = el.getBoundingClientRect();
        const style = getComputedStyle(el);
        return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
      }
    
      function getCandidateEditors() {
        const selectors = [
          'textarea',
          'input[type="text"]',
          'input[type="search"]',
          'input[type="email"]',
          'input[type="url"]',
          'input[type="tel"]',
          'input[type="password"]',
          'input[type="number"]',
          'input:not([type])',
          '[contenteditable="true"]',
          '[contenteditable="plaintext-only"]',
          '[role="textbox"]'
        ];
    
        return Array.from(document.querySelectorAll(selectors.join(',')))
          .filter((el) => isEditableElement(el) && isElementVisible(el));
      }
    
      function getDeepActiveElement(root = document) {
        let active = root.activeElement;
        while (active && active.shadowRoot && active.shadowRoot.activeElement) {
          active = active.shadowRoot.activeElement;
        }
        return active;
      }
    
      function getBestEditor() {
        const active = getDeepActiveElement();
        if (isEditableElement(active) && isElementVisible(active)) return active;
    
        if (state.lastFocusedEditor && isEditableElement(state.lastFocusedEditor) && isElementVisible(state.lastFocusedEditor)) {
          return state.lastFocusedEditor;
        }
    
        if (state.currentEditor && isEditableElement(state.currentEditor) && isElementVisible(state.currentEditor)) {
          return state.currentEditor;
        }
    
        const selection = window.getSelection();
        if (selection && selection.anchorNode) {
          const candidates = getCandidateEditors();
          for (const editor of candidates) {
            if (editor.contains(selection.anchorNode)) return editor;
          }
        }
    
        return null;
      }
    
      function setStatus(text) {
        state.statusText = text;
        if (statusPill) statusPill.textContent = text;
      }
    
      function getLanguageLabel(value) {
        return LANGUAGES.find((lang) => lang.value === value)?.label || value;
      }
    
      function normalizeTranscript(rawText) {
        let text = (rawText || '').replace(/\s+/g, ' ').trim();
        if (!text) return '';
    
        if (state.settings.autoCapitalize && state.settings.language.startsWith('en')) {
          text = text.charAt(0).toUpperCase() + text.slice(1);
        }
    
        return text;
      }
    
      function getInputPrefix(editor, start) {
        if (!state.settings.addSpaceBeforeText) return '';
        const left = String(editor.value || '').slice(0, Math.max(0, start ?? editor.selectionStart ?? 0));
        if (!left) return '';
        return /[\s\n]$/.test(left) ? '' : ' ';
      }
    
      function editorContainsSelection(editor) {
        const selection = window.getSelection();
        if (!selection || selection.rangeCount === 0) return false;
        return !!selection.anchorNode && editor.contains(selection.anchorNode);
      }
    
      function moveCaretToEndContentEditable(editor) {
        editor.focus();
        const selection = window.getSelection();
        if (!selection) return;
    
        const range = document.createRange();
        range.selectNodeContents(editor);
        range.collapse(false);
        selection.removeAllRanges();
        selection.addRange(range);
      }
    
      function getPlainText(editor) {
        return (editor.innerText || editor.textContent || '').replace(/\u00a0/g, ' ');
      }
    
      function getContentEditablePrefix(editor) {
        if (!state.settings.addSpaceBeforeText) return '';
    
        let leftText = '';
        const selection = window.getSelection();
        if (selection && selection.rangeCount > 0 && editorContainsSelection(editor)) {
          try {
            const probe = selection.getRangeAt(0).cloneRange();
            probe.setStart(editor, 0);
            leftText = probe.toString();
          } catch (_) {
            leftText = getPlainText(editor);
          }
        } else {
          leftText = getPlainText(editor);
        }
    
        if (!leftText) return '';
        return /[\s\n]$/.test(leftText) ? '' : ' ';
      }
    
      function dispatchInput(editor, data = '') {
        try {
          editor.dispatchEvent(new InputEvent('input', {
            inputType: 'insertText',
            data,
            bubbles: true,
            composed: true
          }));
        } catch (_) {
          editor.dispatchEvent(new Event('input', { bubbles: true }));
        }
      }
    
      function dispatchContentEditableDomChange(editor) {
        // In React/ProseMirror-style editors, sending InputEvent data for every
        // interim result can make the app append each partial transcript again.
        // We already changed the DOM ourselves, so notify the editor with a
        // neutral input event instead of another "insertText" payload.
        try {
          editor.dispatchEvent(new InputEvent('input', {
            inputType: 'insertReplacementText',
            data: null,
            bubbles: true,
            composed: true
          }));
        } catch (_) {
          editor.dispatchEvent(new Event('input', { bubbles: true }));
        }
      }
    
      function setInputRangeText(editor, replacement, start, end) {
        editor.focus();
    
        try {
          editor.setRangeText(replacement, start, end, 'end');
        } catch (_) {
          const value = String(editor.value || '');
          editor.value = `${value.slice(0, start)}${replacement}${value.slice(end)}`;
          const caret = start + replacement.length;
          try { editor.setSelectionRange(caret, caret); } catch (_) {}
        }
    
        dispatchInput(editor, replacement);
      }
    
      function insertTextIntoInput(editor, rawText) {
        const normalized = normalizeTranscript(rawText);
        if (!normalized) return false;
    
        editor.focus();
        const start = editor.selectionStart ?? String(editor.value || '').length;
        const end = editor.selectionEnd ?? start;
        const text = `${getInputPrefix(editor, start)}${normalized}`;
        setInputRangeText(editor, text, start, end);
        return true;
      }
    
      function getTextPositionInContentEditable(editor, charOffset) {
        const target = Math.max(0, Number(charOffset) || 0);
        const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
        let remaining = target;
        let lastPosition = null;
        let node;
    
        while ((node = walker.nextNode())) {
          const length = node.nodeValue.length;
          if (remaining <= length) return { node, offset: remaining };
          remaining -= length;
          lastPosition = { node, offset: length };
        }
    
        return lastPosition || { node: editor, offset: 0 };
      }
    
      function selectContentEditableTextRange(editor, startOffset, endOffset) {
        const start = getTextPositionInContentEditable(editor, startOffset);
        const end = getTextPositionInContentEditable(editor, endOffset);
        if (!start || !end) return false;
    
        try {
          const range = document.createRange();
          range.setStart(start.node, start.offset);
          range.setEnd(end.node, end.offset);
          const selection = window.getSelection();
          if (!selection) return false;
          selection.removeAllRanges();
          selection.addRange(range);
          return true;
        } catch (_) {
          return false;
        }
      }
    
      function selectLastCharactersBeforeCaret(editor, charCount) {
        const length = Math.max(0, Number(charCount) || 0);
        if (!length) return true;
    
        editor.focus();
        if (!editorContainsSelection(editor)) moveCaretToEndContentEditable(editor);
    
        const selection = window.getSelection();
        if (!selection || selection.rangeCount === 0) return false;
    
        try {
          const caret = selection.getRangeAt(0).cloneRange();
          caret.collapse(false);
    
          const beforeCaret = caret.cloneRange();
          beforeCaret.selectNodeContents(editor);
          beforeCaret.setEnd(caret.endContainer, caret.endOffset);
    
          const endOffset = beforeCaret.toString().length;
          const startOffset = Math.max(0, endOffset - length);
          return selectContentEditableTextRange(editor, startOffset, endOffset);
        } catch (_) {
          return false;
        }
      }
    
      function selectLastMatchingText(editor, text) {
        const needle = String(text || '');
        if (!needle) return false;
    
        const haystack = getPlainText(editor);
        const startOffset = haystack.lastIndexOf(needle);
        if (startOffset < 0) return false;
    
        return selectContentEditableTextRange(editor, startOffset, startOffset + needle.length);
      }
    
      function replaceContentEditableSelection(editor, text) {
        editor.focus();
    
        let inserted = false;
        try {
          if (document.queryCommandSupported && document.queryCommandSupported('insertText')) {
            inserted = document.execCommand('insertText', false, text);
          }
        } catch (_) {
          inserted = false;
        }
    
        if (!inserted) {
          const selection = window.getSelection();
          if (!selection || selection.rangeCount === 0) return false;
          const range = selection.getRangeAt(0);
          range.deleteContents();
          const textNode = document.createTextNode(text);
          range.insertNode(textNode);
          range.setStartAfter(textNode);
          range.setEndAfter(textNode);
          selection.removeAllRanges();
          selection.addRange(range);
          dispatchContentEditableDomChange(editor);
        }
    
        return true;
      }
    
      function replaceTrackedContentEditableText(editor, oldLength, oldText, newText) {
        editor.focus();
    
        const previous = String(oldText || '');
        if (oldLength > 0 || previous) {
          // Never replace arbitrary text near the caret. If the live preview was
          // manually deleted/changed by the user, do not bring it back later.
          if (!previous || !selectLastMatchingText(editor, previous)) return false;
        }
    
        return replaceContentEditableSelection(editor, newText);
      }
    
      function insertTextIntoContentEditable(editor, rawText) {
        const normalized = normalizeTranscript(rawText);
        if (!normalized) return false;
    
        const text = `${getContentEditablePrefix(editor)}${normalized}`;
        editor.focus();
    
        if (!editorContainsSelection(editor)) {
          moveCaretToEndContentEditable(editor);
        }
    
        let inserted = false;
        try {
          if (document.queryCommandSupported && document.queryCommandSupported('insertText')) {
            inserted = document.execCommand('insertText', false, text);
          }
        } catch (_) {
          inserted = false;
        }
    
        if (!inserted) {
          const selection = window.getSelection();
          if (!selection || selection.rangeCount === 0) return false;
          const range = selection.getRangeAt(0);
          range.deleteContents();
          const textNode = document.createTextNode(text);
          range.insertNode(textNode);
          range.setStartAfter(textNode);
          range.setEndAfter(textNode);
          selection.removeAllRanges();
          selection.addRange(range);
          dispatchContentEditableDomChange(editor);
        }
    
        return true;
      }
    
      function insertTextIntoEditor(editor, rawText) {
        const type = getEditorType(editor);
        if (type === 'input') return insertTextIntoInput(editor, rawText);
        if (type === 'contenteditable') return insertTextIntoContentEditable(editor, rawText);
        return false;
      }
    
      function resetInterimState() {
        state.interim = {
          editor: null,
          type: null,
          node: null,
          prefix: '',
          rawText: '',
          previewText: '',
          inputStart: null,
          inputLength: 0
        };
      }
    
      function clearInterim({ keepVisualText = false, dispatch = true } = {}) {
        const interim = state.interim;
        if (!interim.editor) {
          resetInterimState();
          return;
        }
    
        if (interim.type === 'contenteditable' && interim.node && document.contains(interim.node)) {
          if (keepVisualText && interim.node.textContent) {
            const textNode = document.createTextNode(interim.node.textContent);
            const parent = interim.node.parentNode;
            if (parent) {
              parent.replaceChild(textNode, interim.node);
    
              const selection = window.getSelection();
              if (selection) {
                const range = document.createRange();
                range.setStartAfter(textNode);
                range.collapse(true);
                selection.removeAllRanges();
                selection.addRange(range);
              }
            }
          } else {
            interim.node.remove();
          }
    
          if (dispatch) dispatchContentEditableDomChange(interim.editor);
        }
    
        if (interim.type === 'input' && interim.inputStart !== null && !keepVisualText) {
          const start = interim.inputStart;
          const end = start + interim.inputLength;
          setInputRangeText(interim.editor, '', start, end);
        }
    
        resetInterimState();
      }
    
      function inputPreviewIsIntact(interim = state.interim) {
        if (!interim.editor || interim.type !== 'input' || interim.inputStart === null || interim.inputLength <= 0) return true;
    
        const value = String(interim.editor.value || '');
        const expected = String(interim.previewText || '');
        if (!expected) return true;
    
        const exact = value.slice(interim.inputStart, interim.inputStart + interim.inputLength) === expected;
        if (exact) return true;
    
        const fallbackIndex = value.lastIndexOf(expected);
        if (fallbackIndex >= 0) {
          interim.inputStart = fallbackIndex;
          return true;
        }
    
        return false;
      }
    
      function contentEditablePreviewIsIntact(interim = state.interim) {
        if (!interim.editor || interim.type !== 'contenteditable' || interim.inputLength <= 0) return true;
        const expected = String(interim.previewText || '');
        if (!expected) return true;
        return getPlainText(interim.editor).includes(expected);
      }
    
      function interimPreviewIsIntact() {
        if (!state.interim.editor) return true;
        if (state.interim.type === 'input') return inputPreviewIsIntact();
        if (state.interim.type === 'contenteditable') return contentEditablePreviewIsIntact();
        return true;
      }
    
      function discardCurrentSpeechSegment() {
        state.discardUntilFinal = true;
        resetInterimState();
        updateBufferPreview();
        setStatus('הטקסט נמחק — מדלג על המקטע');
      }
    
      function renderInterimInInput(editor, rawText) {
        const normalized = normalizeTranscript(rawText);
        if (!normalized) return false;
    
        editor.focus();
    
        if (state.interim.editor !== editor || state.interim.type !== 'input') {
          clearInterim({ keepVisualText: true });
          const start = editor.selectionStart ?? String(editor.value || '').length;
          state.interim.editor = editor;
          state.interim.type = 'input';
          state.interim.inputStart = start;
          state.interim.prefix = getInputPrefix(editor, start);
          state.interim.inputLength = 0;
          state.interim.previewText = '';
        } else if (!inputPreviewIsIntact()) {
          discardCurrentSpeechSegment();
          return false;
        }
    
        const text = `${state.interim.prefix}${normalized}`;
        const start = state.interim.inputStart;
        const end = start + state.interim.inputLength;
        setInputRangeText(editor, text, start, end);
        state.interim.inputLength = text.length;
        state.interim.previewText = text;
        state.interim.rawText = rawText;
        return true;
      }
    
      function ensureInterimNode(editor) {
        if (state.interim.editor === editor && state.interim.type === 'contenteditable' && state.interim.node && document.contains(state.interim.node)) {
          return state.interim.node;
        }
    
        clearInterim({ keepVisualText: true });
    
        editor.focus();
        if (!editorContainsSelection(editor)) {
          moveCaretToEndContentEditable(editor);
        }
    
        const selection = window.getSelection();
        if (!selection || selection.rangeCount === 0) return null;
    
        const span = document.createElement('span');
        span.setAttribute(INTERIM_ATTR, '1');
        span.style.opacity = '0.72';
        span.style.whiteSpace = 'pre-wrap';
        span.style.borderBottom = '1px dotted currentColor';
    
        state.interim.editor = editor;
        state.interim.type = 'contenteditable';
        state.interim.node = span;
        state.interim.prefix = getContentEditablePrefix(editor);
        state.interim.rawText = '';
    
        span.textContent = state.interim.prefix;
    
        const range = selection.getRangeAt(0);
        range.deleteContents();
        range.insertNode(span);
    
        const after = document.createRange();
        after.setStartAfter(span);
        after.collapse(true);
        selection.removeAllRanges();
        selection.addRange(after);
    
        return span;
      }
    
      function renderInterimInContentEditable(editor, rawText) {
        const normalized = normalizeTranscript(rawText);
        if (!normalized) return false;
    
        if (state.interim.editor !== editor || state.interim.type !== 'contenteditable') {
          clearInterim({ keepVisualText: true });
          editor.focus();
          if (!editorContainsSelection(editor)) moveCaretToEndContentEditable(editor);
          state.interim.editor = editor;
          state.interim.type = 'contenteditable';
          state.interim.node = null;
          state.interim.prefix = getContentEditablePrefix(editor);
          state.interim.inputLength = 0;
          state.interim.previewText = '';
          state.interim.rawText = '';
        }
    
        const text = `${state.interim.prefix}${normalized}`;
        const ok = replaceTrackedContentEditableText(
          editor,
          state.interim.inputLength,
          state.interim.previewText,
          text
        );
    
        if (!ok) {
          discardCurrentSpeechSegment();
          return false;
        }
    
        state.interim.inputLength = text.length;
        state.interim.previewText = text;
        state.interim.rawText = rawText;
        return true;
      }
    
      function renderInterimTranscript(editor, rawText) {
        const type = getEditorType(editor);
        if (type === 'input') return renderInterimInInput(editor, rawText);
        if (type === 'contenteditable') return renderInterimInContentEditable(editor, rawText);
        return false;
      }
    
      function commitFinalToInput(editor, rawText) {
        const normalized = normalizeTranscript(rawText);
        if (!normalized) return false;
    
        if (state.interim.editor === editor && state.interim.type === 'input' && state.interim.inputStart !== null) {
          if (!inputPreviewIsIntact()) {
            resetInterimState();
            setStatus('דילג על הטקסט שנמחק');
            return false;
          }
    
          const text = `${state.interim.prefix}${normalized}`;
          const start = state.interim.inputStart;
          const end = start + state.interim.inputLength;
          setInputRangeText(editor, text, start, end);
          resetInterimState();
          return true;
        }
    
        return insertTextIntoInput(editor, normalized);
      }
    
      function commitFinalToContentEditable(editor, rawText) {
        const normalized = normalizeTranscript(rawText);
        if (!normalized) return false;
    
        if (state.interim.editor === editor && state.interim.type === 'contenteditable') {
          const finalText = `${state.interim.prefix}${normalized}`;
          const ok = replaceTrackedContentEditableText(
            editor,
            state.interim.inputLength,
            state.interim.previewText,
            finalText
          );
          resetInterimState();
          if (!ok) setStatus('דילג על הטקסט שנמחק');
          return ok;
        }
    
        return insertTextIntoContentEditable(editor, normalized);
      }
    
      function commitFinalTranscript(editor, rawText) {
        const type = getEditorType(editor);
        if (type === 'input') return commitFinalToInput(editor, rawText);
        if (type === 'contenteditable') return commitFinalToContentEditable(editor, rawText);
        return false;
      }
    
      function appendToBuffer(rawText) {
        const normalized = normalizeTranscript(rawText);
        if (!normalized) return;
    
        const prefix = state.bufferText && !/[\s\n]$/.test(state.bufferText) ? ' ' : '';
        state.bufferText = `${state.bufferText}${prefix}${normalized}`;
        updateBufferPreview();
      }
    
      function updateBufferPreview(interimText = '') {
        if (!bufferPreview) return;
        const interim = normalizeTranscript(interimText);
        const full = `${state.bufferText}${interim ? `${state.bufferText ? ' ' : ''}${interim}` : ''}`.trim();
        bufferPreview.textContent = full || 'אין טקסט שמור עדיין.';
        bufferPreview.classList.toggle('empty', !full);
      }
    
      async function copyBufferToClipboard() {
        const text = state.bufferText.trim();
        if (!text) {
          setStatus('אין טקסט להעתקה');
          return;
        }
    
        try {
          await navigator.clipboard.writeText(text);
          setStatus('הטקסט הועתק');
        } catch (_) {
          setStatus('לא ניתן להעתיק אוטומטית');
        }
      }
    
      function pasteBufferToActiveEditor() {
        const text = state.bufferText.trim();
        if (!text) {
          setStatus('אין טקסט להדבקה');
          return;
        }
    
        const editor = getBestEditor();
        if (!editor) {
          setStatus('בחר קודם שדה טקסט בדף');
          return;
        }
    
        if (insertTextIntoEditor(editor, text)) {
          state.bufferText = '';
          updateBufferPreview();
          setStatus('הטקסט הודבק לשדה הפעיל');
        }
      }
    
      function updateButtonState() {
        if (!micButton) return;
    
        const supported = supportsSpeechRecognition();
        micButton.classList.toggle('recording', state.listening);
        micButton.classList.toggle('unsupported', !supported);
        micButton.title = supported
          ? (state.listening ? 'עצור תמלול · Alt+גרירה להזזה' : 'התחל תמלול · Alt+גרירה להזזה')
          : 'הדפדפן לא תומך בתמלול קולי';
        micButton.setAttribute('aria-pressed', state.listening ? 'true' : 'false');
    
        if (!supported) {
          setStatus('לא נתמך בדפדפן הזה');
        }
      }
    
      function refreshUiVisibilitySettings() {
        if (!wrap) return;
        wrap.classList.toggle('hide-status', !state.settings.showStatus);
        wrap.classList.toggle('hide-lang', !state.settings.showLanguageSelect);
      }
    
      function applyPosition() {
        if (!wrap) return;
    
        if (state.position.mode === 'free' && Number.isFinite(state.position.left) && Number.isFinite(state.position.top)) {
          // Intentionally do not clamp to the viewport. Users can move the button
          // partially or fully outside the visible page, and restore it with
          // Alt+Shift+M or the reset button when the panel is visible.
          wrap.style.left = `${state.position.left}px`;
          wrap.style.top = `${state.position.top}px`;
          wrap.style.right = 'auto';
          wrap.style.bottom = 'auto';
          return;
        }
    
        wrap.style.left = 'auto';
        wrap.style.top = 'auto';
        wrap.style.right = `${state.position.right ?? 24}px`;
        wrap.style.bottom = `${state.position.bottom ?? 24}px`;
      }
    
      function resetPosition() {
        savePosition({ ...DEFAULT_POSITION });
        requestAnimationFrame(applyPosition);
        setStatus('המיקום אופס לאתר הזה');
      }
    
      function ensureUi() {
        if (state.mounted) return;
    
        host = document.createElement('div');
        host.id = APP_ID;
        host.setAttribute('aria-hidden', 'false');
        document.documentElement.appendChild(host);
    
        shadow = host.attachShadow({ mode: 'open' });
        shadow.innerHTML = `
          <style>
            :host { all: initial; }
    
            .wrap {
              position: fixed;
              z-index: 2147483647;
              display: inline-flex;
              align-items: center;
              gap: 8px;
              direction: rtl;
              font-family: Arial, sans-serif;
              user-select: none;
              touch-action: none;
            }
    
            .options {
              display: inline-flex;
              align-items: center;
              gap: 8px;
            }
    
            .wrap.collapsed .options,
            .wrap.collapsed .panel { display: none !important; }
    
            .status,
            .lang,
            .settings-toggle,
            .expand-toggle {
              background: rgba(17, 24, 39, 0.94);
              color: #fff;
              border-radius: 999px;
              box-shadow: 0 8px 20px rgba(0,0,0,0.18);
            }
    
            .status {
              padding: 7px 11px;
              font-size: 12px;
              line-height: 1;
              white-space: nowrap;
              max-width: min(280px, 40vw);
              overflow: hidden;
              text-overflow: ellipsis;
            }
    
            .hide-status .status { display: none; }
            .hide-lang .lang { display: none; }
    
            .lang {
              border: 0;
              outline: 0;
              padding: 7px 10px;
              font-size: 12px;
              cursor: pointer;
              max-width: 125px;
            }
    
            .mic-shell {
              position: relative;
              width: 54px;
              height: 54px;
              display: inline-flex;
              align-items: center;
              justify-content: center;
              flex: 0 0 auto;
            }
    
            .btn,
            .settings-toggle,
            .expand-toggle {
              border: none;
              cursor: pointer;
              display: inline-flex;
              align-items: center;
              justify-content: center;
              transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease, background 120ms ease;
            }
    
            .btn {
              width: 54px;
              height: 54px;
              border-radius: 999px;
              background: #1a73e8;
              color: #fff;
              box-shadow: 0 12px 26px rgba(26, 115, 232, 0.34);
              position: relative;
            }
    
            .btn:hover,
            .settings-toggle:hover,
            .expand-toggle:hover { transform: translateY(-1px); }
            .btn:active,
            .settings-toggle:active,
            .expand-toggle:active { transform: translateY(0); }
    
            .btn.recording {
              background: #d93025;
              box-shadow: 0 12px 26px rgba(217, 48, 37, 0.38);
              animation: pulse 1.2s infinite;
            }
    
            .btn.unsupported {
              cursor: not-allowed;
              opacity: 0.65;
              box-shadow: none;
              animation: none;
            }
    
            .wrap.dragging .btn { cursor: grabbing; }
    
            .settings-toggle {
              width: 34px;
              height: 34px;
              font-size: 16px;
            }
    
            .expand-toggle {
              position: absolute;
              left: -3px;
              bottom: -3px;
              z-index: 2;
              width: 22px;
              height: 22px;
              padding: 0;
              font-size: 12px;
              font-weight: 700;
              line-height: 1;
              box-shadow: 0 6px 14px rgba(0,0,0,0.22);
            }
    
            .wrap.expanded .expand-toggle {
              background: rgba(17, 24, 39, 0.98);
            }
    
            .icon {
              width: 26px;
              height: 26px;
              fill: currentColor;
              pointer-events: none;
            }
    
            .panel {
              position: absolute;
              right: 0;
              bottom: 66px;
              display: none;
              width: min(320px, calc(100vw - 24px));
              padding: 12px;
              border-radius: 16px;
              background: rgba(255,255,255,0.99);
              color: #202124;
              box-shadow: 0 16px 42px rgba(0,0,0,0.24);
              border: 1px solid rgba(0,0,0,0.08);
            }
    
            .panel.open { display: block; }
    
            .panel-title {
              font-weight: 700;
              margin: 0 0 10px;
              font-size: 14px;
            }
    
            .row {
              display: flex;
              align-items: center;
              justify-content: space-between;
              gap: 12px;
              margin: 9px 0;
              font-size: 13px;
            }
    
            .row label { cursor: pointer; }
    
            .panel select {
              max-width: 150px;
              border: 1px solid #dadce0;
              border-radius: 8px;
              padding: 5px 7px;
              background: #fff;
            }
    
            .buffer {
              margin-top: 10px;
              padding: 9px;
              border: 1px solid #e0e0e0;
              border-radius: 12px;
              background: #f8fafd;
              max-height: 110px;
              overflow: auto;
              white-space: pre-wrap;
              line-height: 1.45;
              font-size: 12px;
              user-select: text;
            }
    
            .buffer.empty { color: #6b7280; }
    
            .actions {
              display: flex;
              flex-wrap: wrap;
              gap: 8px;
              margin-top: 10px;
            }
    
            .panel-button {
              border: 1px solid #dadce0;
              background: #fff;
              border-radius: 999px;
              padding: 6px 10px;
              cursor: pointer;
              font-size: 12px;
            }
    
            .panel-button:hover { background: #f1f3f4; }
    
            .hint {
              margin: 10px 0 0;
              font-size: 11px;
              color: #5f6368;
              line-height: 1.45;
            }
    
            @keyframes pulse {
              0% { transform: scale(1); }
              50% { transform: scale(1.06); }
              100% { transform: scale(1); }
            }
          </style>
    
          <div class="wrap collapsed" id="wrap">
            <div class="options" id="quickOptions">
              <div class="status" id="status">מוכן</div>
              <select class="lang" id="languageSelect" title="שפת תמלול"></select>
              <button class="settings-toggle" id="settingsBtn" type="button" title="הגדרות">⚙</button>
            </div>
            <div class="mic-shell" id="micShell">
              <button class="btn" id="micBtn" type="button" title="התחל תמלול">
                <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
                  <path d="M12 15a3.75 3.75 0 0 0 3.75-3.75V6.75a3.75 3.75 0 1 0-7.5 0v4.5A3.75 3.75 0 0 0 12 15Zm6-3.75a.75.75 0 0 1 1.5 0A7.5 7.5 0 0 1 12.75 18.7V21a.75.75 0 0 1-1.5 0v-2.3A7.5 7.5 0 0 1 4.5 11.25a.75.75 0 0 1 1.5 0 6 6 0 0 0 12 0Z"></path>
                </svg>
              </button>
              <button class="expand-toggle" id="expandBtn" type="button" title="פתח אפשרויות" aria-expanded="false">▴</button>
            </div>
    
            <div class="panel" id="settingsPanel">
              <p class="panel-title">תמלול קולי גלובלי</p>
    
              <div class="row">
                <label for="panelLanguage">שפה</label>
                <select id="panelLanguage"></select>
              </div>
    
              <div class="row">
                <label for="continuousToggle">האזנה רציפה</label>
                <input id="continuousToggle" type="checkbox">
              </div>
    
              <div class="row">
                <label for="spaceToggle">להוסיף רווח לפני התמלול</label>
                <input id="spaceToggle" type="checkbox">
              </div>
    
              <div class="row">
                <label for="capitalizeToggle">Capital באנגלית</label>
                <input id="capitalizeToggle" type="checkbox">
              </div>
    
              <div class="row">
                <label for="statusToggle">להציג סטטוס</label>
                <input id="statusToggle" type="checkbox">
              </div>
    
              <div class="row">
                <label for="langToggle">להציג בחירת שפה</label>
                <input id="langToggle" type="checkbox">
              </div>
    
              <div class="buffer empty" id="bufferPreview">אין טקסט שמור עדיין.</div>
    
              <div class="actions">
                <button class="panel-button" id="pasteBtn" type="button">הדבק לשדה הפעיל</button>
                <button class="panel-button" id="copyBtn" type="button">העתק</button>
                <button class="panel-button" id="clearBtn" type="button">נקה</button>
                <button class="panel-button" id="resetPositionBtn" type="button">אפס מיקום</button>
              </div>
    
              <div class="hint">
                לחץ בתוך שדה טקסט ואז על המיקרופון. כדי להזיז את הכפתור: החזק Alt וגרור את המיקרופון. המיקום נשמר בנפרד לכל אתר. אם הכפתור יצא מהמסך, Alt+Shift+M מחזיר אותו לברירת המחדל. אם אין שדה פעיל, התמלול נשמר כאן ואפשר להעתיק/להדביק אותו אחר כך.
              </div>
            </div>
          </div>
        `;
    
        wrap = shadow.getElementById('wrap');
        micButton = shadow.getElementById('micBtn');
        expandButton = shadow.getElementById('expandBtn');
        statusPill = shadow.getElementById('status');
        languageSelect = shadow.getElementById('languageSelect');
        settingsButton = shadow.getElementById('settingsBtn');
        settingsPanel = shadow.getElementById('settingsPanel');
        bufferPreview = shadow.getElementById('bufferPreview');
        copyButton = shadow.getElementById('copyBtn');
        pasteButton = shadow.getElementById('pasteBtn');
        clearButton = shadow.getElementById('clearBtn');
        resetPositionButton = shadow.getElementById('resetPositionBtn');
    
        hydrateLanguageSelects();
        hydratePanelControls();
        bindUiEvents();
    
        state.mounted = true;
        refreshUiVisibilitySettings();
        updateBufferPreview();
        updateButtonState();
    
        requestAnimationFrame(applyPosition);
      }
    
      function setExpanded(expanded) {
        if (!wrap || !expandButton) return;
    
        wrap.classList.toggle('expanded', Boolean(expanded));
        wrap.classList.toggle('collapsed', !expanded);
        expandButton.textContent = expanded ? '▾' : '▴';
        expandButton.title = expanded ? 'סגור אפשרויות' : 'פתח אפשרויות';
        expandButton.setAttribute('aria-expanded', expanded ? 'true' : 'false');
    
        if (settingsPanel) settingsPanel.classList.toggle('open', Boolean(expanded));
        requestAnimationFrame(applyPosition);
      }
    
      function toggleExpanded() {
        setExpanded(!wrap.classList.contains('expanded'));
      }
    
      function hydrateLanguageSelects() {
        const panelLanguage = shadow.getElementById('panelLanguage');
        const optionsHtml = LANGUAGES.map((lang) => `<option value="${lang.value}">${lang.label}</option>`).join('');
    
        languageSelect.innerHTML = optionsHtml;
        panelLanguage.innerHTML = optionsHtml;
        languageSelect.value = state.settings.language;
        panelLanguage.value = state.settings.language;
    
        const onLanguageChange = (value) => {
          saveSetting('language', value);
          languageSelect.value = value;
          panelLanguage.value = value;
          setStatus(`שפה: ${getLanguageLabel(value)}`);
        };
    
        languageSelect.addEventListener('change', () => onLanguageChange(languageSelect.value));
        panelLanguage.addEventListener('change', () => onLanguageChange(panelLanguage.value));
      }
    
      function hydratePanelControls() {
        const continuousToggle = shadow.getElementById('continuousToggle');
        const spaceToggle = shadow.getElementById('spaceToggle');
        const capitalizeToggle = shadow.getElementById('capitalizeToggle');
        const statusToggle = shadow.getElementById('statusToggle');
        const langToggle = shadow.getElementById('langToggle');
    
        continuousToggle.checked = Boolean(state.settings.continuous);
        spaceToggle.checked = Boolean(state.settings.addSpaceBeforeText);
        capitalizeToggle.checked = Boolean(state.settings.autoCapitalize);
        statusToggle.checked = Boolean(state.settings.showStatus);
        langToggle.checked = Boolean(state.settings.showLanguageSelect);
    
        continuousToggle.addEventListener('change', () => saveSetting('continuous', continuousToggle.checked));
        spaceToggle.addEventListener('change', () => saveSetting('addSpaceBeforeText', spaceToggle.checked));
        capitalizeToggle.addEventListener('change', () => saveSetting('autoCapitalize', capitalizeToggle.checked));
        statusToggle.addEventListener('change', () => saveSetting('showStatus', statusToggle.checked));
        langToggle.addEventListener('change', () => saveSetting('showLanguageSelect', langToggle.checked));
      }
    
      function bindUiEvents() {
        const preventFocusLoss = (event) => {
          event.preventDefault();
          event.stopPropagation();
        };
    
        micButton.addEventListener('pointerdown', onPointerDownForDragAndClick, true);
    
        for (const el of [expandButton, settingsButton, languageSelect, copyButton, pasteButton, clearButton, resetPositionButton]) {
          el.addEventListener('pointerdown', preventFocusLoss, true);
        }
    
        expandButton.addEventListener('click', toggleExpanded);
    
        settingsButton.addEventListener('click', () => {
          setExpanded(true);
          settingsPanel.classList.toggle('open');
          requestAnimationFrame(applyPosition);
        });
    
        copyButton.addEventListener('click', copyBufferToClipboard);
        pasteButton.addEventListener('click', pasteBufferToActiveEditor);
        clearButton.addEventListener('click', () => {
          state.bufferText = '';
          updateBufferPreview();
          setStatus('הטקסט השמור נמחק');
        });
        resetPositionButton.addEventListener('click', resetPosition);
    
        document.addEventListener('click', (event) => {
          const path = event.composedPath ? event.composedPath() : [];
          if (!path.includes(host)) {
            setExpanded(false);
          }
        }, true);
      }
    
      function onPointerDownForDragAndClick(event) {
        event.preventDefault();
        event.stopPropagation();
    
        const rect = wrap.getBoundingClientRect();
        const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
        const startedOnMic = event.currentTarget === micButton || path.includes(micButton);
    
        state.dragging = {
          active: true,
          moved: false,
          pointerId: event.pointerId,
          startedOnMic,
          canDrag: Boolean(event.altKey && startedOnMic),
          startX: event.clientX,
          startY: event.clientY,
          startLeft: rect.left,
          startTop: rect.top
        };
    
        if (state.dragging.canDrag) wrap.classList.add('dragging');
    
        try { event.currentTarget.setPointerCapture(event.pointerId); } catch (_) {}
    
        window.addEventListener('pointermove', onPointerMoveDrag, true);
        window.addEventListener('pointerup', onPointerUpDrag, true);
        window.addEventListener('pointercancel', onPointerCancelDrag, true);
      }
    
      function onPointerMoveDrag(event) {
        if (!state.dragging.active || event.pointerId !== state.dragging.pointerId) return;
        if (!state.dragging.canDrag) return;
    
        const dx = event.clientX - state.dragging.startX;
        const dy = event.clientY - state.dragging.startY;
    
        if (!state.dragging.moved && Math.hypot(dx, dy) < 6) return;
        state.dragging.moved = true;
    
        const left = state.dragging.startLeft + dx;
        const top = state.dragging.startTop + dy;
    
        wrap.style.left = `${left}px`;
        wrap.style.top = `${top}px`;
        wrap.style.right = 'auto';
        wrap.style.bottom = 'auto';
      }
    
      function onPointerUpDrag(event) {
        if (!state.dragging.active || event.pointerId !== state.dragging.pointerId) return;
    
        const wasDrag = state.dragging.moved;
        const rect = wrap.getBoundingClientRect();
    
        window.removeEventListener('pointermove', onPointerMoveDrag, true);
        window.removeEventListener('pointerup', onPointerUpDrag, true);
        window.removeEventListener('pointercancel', onPointerCancelDrag, true);
        wrap.classList.remove('dragging');
    
        if (wasDrag) {
          savePosition({ mode: 'free', left: rect.left, top: rect.top, right: null, bottom: null });
          // Keep the UI quiet after dragging; the saved position is obvious visually.
          setStatus(state.listening ? 'מקשיב...' : 'מוכן');
        } else {
          const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
          const endedOnMic = event.target === micButton || micButton.contains(event.target) || path.includes(micButton);
          if (!state.dragging.canDrag && (state.dragging.startedOnMic || endedOnMic)) toggleListening();
        }
    
        state.dragging.active = false;
        state.dragging.moved = false;
        state.dragging.startedOnMic = false;
        state.dragging.canDrag = false;
      }
    
      function onPointerCancelDrag(event) {
        if (!state.dragging.active || event.pointerId !== state.dragging.pointerId) return;
        window.removeEventListener('pointermove', onPointerMoveDrag, true);
        window.removeEventListener('pointerup', onPointerUpDrag, true);
        window.removeEventListener('pointercancel', onPointerCancelDrag, true);
        wrap.classList.remove('dragging');
        state.dragging.active = false;
        state.dragging.moved = false;
        state.dragging.startedOnMic = false;
        state.dragging.canDrag = false;
      }
    
      function toggleListening() {
        if (!supportsSpeechRecognition()) {
          setStatus('הדפדפן לא תומך בתמלול קולי');
          updateButtonState();
          return;
        }
    
        if (state.listening) stopListening();
        else startListening();
      }
    
      function createRecognition() {
        const Recognition = getSpeechRecognitionCtor();
        const recognition = new Recognition();
    
        recognition.lang = state.settings.language || 'he-IL';
        recognition.interimResults = true;
        recognition.continuous = Boolean(state.settings.continuous);
        recognition.maxAlternatives = 1;
    
        recognition.onstart = () => {
          state.listening = true;
          setStatus('מקשיב...');
          updateButtonState();
        };
    
        recognition.onresult = (event) => {
          let finalText = '';
          let interimText = '';
    
          for (let i = event.resultIndex; i < event.results.length; i += 1) {
            const result = event.results[i];
            const transcript = result[0]?.transcript || '';
            if (result.isFinal) finalText += transcript;
            else interimText += transcript;
          }
    
          if (state.interim.editor && !interimPreviewIsIntact()) {
            discardCurrentSpeechSegment();
          }
    
          if (state.discardUntilFinal) {
            if (finalText.trim()) {
              state.discardUntilFinal = false;
              state.lastTranscriptAt = Date.now();
              updateBufferPreview();
              setStatus('דילג על הטקסט שנמחק');
            } else if (interimText.trim()) {
              updateBufferPreview();
              setStatus('הטקסט נמחק — מדלג על המקטע');
            }
            return;
          }
    
          const editor = getBestEditor();
          if (editor) {
            state.currentEditor = editor;
            state.lastFocusedEditor = editor;
    
            if (finalText.trim()) {
              const committed = commitFinalTranscript(editor, finalText);
              if (committed) updateBufferPreview(finalText);
              state.lastTranscriptAt = Date.now();
            }
    
            if (interimText.trim()) {
              renderInterimTranscript(editor, interimText);
              updateBufferPreview(interimText);
              setStatus(`מתמלל: ${normalizeTranscript(interimText).slice(0, 46)}`);
            } else if (finalText.trim()) {
              clearInterim({ keepVisualText: false });
              setStatus('ממשיך להאזין...');
            }
    
            return;
          }
    
          if (finalText.trim()) {
            appendToBuffer(finalText);
            state.lastTranscriptAt = Date.now();
            setStatus('נשמר בחלונית');
          }
    
          if (interimText.trim()) {
            updateBufferPreview(interimText);
            setStatus(`מתמלל ללא שדה: ${normalizeTranscript(interimText).slice(0, 38)}`);
          }
        };
    
        recognition.onerror = (event) => {
          const code = event.error || 'unknown';
          const messages = {
            'not-allowed': 'אין הרשאה למיקרופון',
            'service-not-allowed': 'שירות התמלול נחסם',
            'audio-capture': 'לא נמצא מיקרופון',
            'no-speech': 'לא זוהה דיבור',
            'network': 'שגיאת רשת בתמלול',
            'aborted': 'התמלול הופסק'
          };
    
          if (code === 'not-allowed' || code === 'service-not-allowed') {
            clearInterim({ keepVisualText: true });
            state.suppressNextEndRestart = true;
          }
    
          setStatus(messages[code] || `שגיאה: ${code}`);
        };
    
        recognition.onend = () => {
          if (state.interim.editor && !state.discardUntilFinal) {
            clearInterim({ keepVisualText: true });
          } else if (state.discardUntilFinal) {
            resetInterimState();
          }
    
          const shouldRestart = state.listening && state.settings.continuous && !state.suppressNextEndRestart;
          if (shouldRestart) {
            try {
              recognition.start();
              return;
            } catch (_) {}
          }
    
          state.listening = false;
          state.recognition = null;
          state.suppressNextEndRestart = false;
          state.discardUntilFinal = false;
    
          if (Date.now() - state.lastTranscriptAt > 1200 && ['מקשיב...', 'ממשיך להאזין...'].includes(state.statusText)) {
            setStatus('מוכן');
          }
    
          updateButtonState();
        };
    
        return recognition;
      }
    
      function startListening() {
        state.currentEditor = getBestEditor();
        if (state.currentEditor) state.lastFocusedEditor = state.currentEditor;
    
        state.suppressNextEndRestart = false;
    
        try {
          state.recognition = createRecognition();
          state.recognition.start();
        } catch (_) {
          setStatus('לא ניתן להפעיל תמלול כעת');
          state.recognition = null;
          state.listening = false;
          updateButtonState();
        }
      }
    
      function stopListening() {
        state.suppressNextEndRestart = true;
    
        if (state.interim.editor && !interimPreviewIsIntact()) {
          discardCurrentSpeechSegment();
        }
    
        // Do not preserve/clear the interim text here.
        // When SpeechRecognition.stop() is called, Chrome may still emit a final
        // result after this click. Keeping the interim state alive lets the final
        // result replace the live preview instead of being inserted a second time.
        // If no final result arrives, recognition.onend will preserve the current
        // interim text once.
        if (state.recognition) {
          try { state.recognition.stop(); } catch (_) {}
        }
    
        state.listening = false;
        setStatus('נעצר');
        updateButtonState();
      }
    
      function onFocusIn(event) {
        const target = event.target;
        if (isEditableElement(target)) {
          state.currentEditor = target;
          state.lastFocusedEditor = target;
          if (state.listening) setStatus('מקשיב לשדה הפעיל...');
        }
      }
    
      function onMouseDown(event) {
        const target = event.target;
        if (isEditableElement(target)) {
          state.currentEditor = target;
          state.lastFocusedEditor = target;
        }
      }
    
      function onSelectionChange() {
        const editor = getBestEditor();
        if (editor) {
          state.currentEditor = editor;
          state.lastFocusedEditor = editor;
        }
      }
    
      function onKeyDown(event) {
        if (event.altKey && event.shiftKey && !event.ctrlKey && !event.metaKey && event.code === 'KeyM') {
          event.preventDefault();
          resetPosition();
        }
      }
    
      function registerMenuCommands() {
        try {
          if (typeof GM_registerMenuCommand !== 'function') return;
    
          GM_registerMenuCommand('Voice to Text: הפעלה/עצירה', toggleListening);
    
          GM_registerMenuCommand('Voice to Text: החלף עברית/אנגלית', () => {
            const next = state.settings.language === 'he-IL' ? 'en-US' : 'he-IL';
            saveSetting('language', next);
            if (languageSelect) languageSelect.value = next;
            const panelLanguage = shadow?.getElementById('panelLanguage');
            if (panelLanguage) panelLanguage.value = next;
            setStatus(`שפה: ${getLanguageLabel(next)}`);
          });
    
          GM_registerMenuCommand('Voice to Text: אפס מיקום כפתור', resetPosition);
        } catch (_) {}
      }
    
      function init() {
        loadSettings();
        ensureUi();
        registerMenuCommands();
    
        document.addEventListener('focusin', onFocusIn, true);
        document.addEventListener('mousedown', onMouseDown, true);
        document.addEventListener('selectionchange', onSelectionChange, true);
        document.addEventListener('keydown', onKeyDown, true);
        window.addEventListener('resize', () => requestAnimationFrame(applyPosition), { passive: true });
    
        setStatus(supportsSpeechRecognition() ? 'מוכן' : 'לא נתמך בדפדפן הזה');
        updateButtonState();
      }
    
      function waitForBodyThenInit() {
        if (document.body && document.documentElement) {
          init();
          return;
        }
    
        const timer = window.setInterval(() => {
          if (document.body && document.documentElement) {
            window.clearInterval(timer);
            init();
          }
        }, 250);
      }
    
      waitForBodyThenInit();
    })();
    
    
    אפשרות 2 - תוסף Chrome רגיל

    יש גם גרסה כתוסף Google Chrome, שעובדת אותו דבר כמו הסקריפט, רק בלי צורך ב־Tampermonkey.

    התקנה:

    מורידים את תיקיית התוסף.
    פותחים בכרום:
    chrome://extensions/

    מפעילים Developer mode.
    לוחצים על Load unpacked.
    בוחרים את תיקיית התוסף.
    מרעננים את האתרים הפתוחים.

    קובץ ה־ZIP לתוסף:
    universal-voice-to-text-chrome-extension-v2.6.0.zip

    לא עובד בדפי מערכת של כרום כמו chrome://extensions.
    איכות התמלול תלויה במיקרופון וברעש סביבתי.

    מי שרוצה לבדוק, לשפר או להעיר על באגים - בשמחה.

    י. פל.י מנותק
    י. פל.י מנותק
    י. פל.
    כתב נערך לאחרונה על ידי
    #12

    @אברהם-גלסר
    מה רע ב:
    https://dictanote.co/voicein/

    גאה להיות חלק:
    otzaria.org

    אברהם גלסרא המלאךה 2 תגובות תגובה אחרונה
    0
    • י. פל.י י. פל.

      @אברהם-גלסר
      מה רע ב:
      https://dictanote.co/voicein/

      אברהם גלסרא מחובר
      אברהם גלסרא מחובר
      אברהם גלסר
      כתב נערך לאחרונה על ידי
      #13

      @י.-פל. לא הכרתי, ובעיניי שלי יותר נוח...

      תגובה 1 תגובה אחרונה
      0
      • י. פל.י י. פל.

        @אברהם-גלסר
        מה רע ב:
        https://dictanote.co/voicein/

        המלאךה מנותק
        המלאךה מנותק
        המלאך
        כתב נערך לאחרונה על ידי
        #14

        @י.-פל. שזה תוסף.
        וזה סקריפט.

        אברהם גלסרא תגובה 1 תגובה אחרונה
        1
        • המלאךה המלאך

          @י.-פל. שזה תוסף.
          וזה סקריפט.

          אברהם גלסרא מחובר
          אברהם גלסרא מחובר
          אברהם גלסר
          כתב נערך לאחרונה על ידי אברהם גלסר
          #15

          @המלאך גם.

          תגובה 1 תגובה אחרונה
          0
          • אברהם גלסרא אברהם גלסר

            בניתי בעזרת ChatGPT כלי קטן שמוסיף כפתור מיקרופון קבוע בדפדפן, ומאפשר להכתיב טקסט ישירות לתוך שדות כתיבה באתרים.

            chrome-capture-2026-05-29 (1).gif

            מה הכלי עושה בפועל:

            מוסיף כפתור מיקרופון צף בכל אתר.
            כברירת מחדל מוצג רק כפתור התמלול עצמו, בלי תפריטים מסביב.
            יש חץ קטן לפתיחת כל האפשרויות.
            בלחיצה על החץ נפתחות האפשרויות: בחירת שפה, הגדרות, טקסט שתומלל, העתקה/הדבקה וכדומה.
            בלחיצה נוספת על החץ הכל נסגר שוב.

            בלחיצה על כפתור המיקרופון מתחיל תמלול דיבור לטקסט.
            לחיצה נוספת עוצרת את התמלול.
            הטקסט נכנס בזמן אמת לשדה הכתיבה הפעיל.
            עובד עם textarea, input ושדות כתיבה מתקדמים יותר כמו contenteditable.
            תומך בעברית, אנגלית ובכמה שפות נוספות.
            יש אפשרות לבחור שפת תמלול.
            אם אין שדה כתיבה פעיל, הטקסט מופיע בחלונית של הכלי ואפשר להעתיק אותו.

            אפשר להזיז את כפתור התמלול עם Alt + גרירה על הכפתור.
            המיקום נשמר גם אחרי רענון.
            המיקום נשמר בנפרד לכל אתר, כלומר אפשר לקבוע מיקום אחד ל-ChatGPT, מיקום אחר לפורום וכו׳.
            ככה תוכלו להתאים את המיקום שלו לכל אתר איפה שהכי מתאים לו להיות, למשל בChatGPT:

            341d49db-543d-4bc3-8371-9947e417f7b0-image.png

            אם הכפתור נעלם בגלל שהוזז החוצה, אפשר להחזיר אותו למיקום ברירת המחדל באתר הנוכחי עם:
            Alt + Shift + M
            או:
            Ctrl + Alt + M

            בנוסף, אם מוחקים ידנית תוך כדי תמלול טקסט שהכלי כתב בתוך תיבת הטקסט, הכלי מתעלם מהמחיקה ולא מחזיר את הטקסט הזה מחדש בסיום ההקלטה.

            הכלי מבוסס על מנגנון זיהוי הדיבור של הדפדפן, כך שאין צורך בשרת חיצוני או במפתח API משלכם. בפעם הראשונה הדפדפן יבקש הרשאת מיקרופון.

            יש שתי אפשרויות שימוש:

            אפשרות 1 - סקריפט Tampermonkey

            מתאים למי שכבר משתמש ב־Tampermonkey או רוצה להתקין userscript.

            התקנה:

            אפשרות א':
            תודה ל @מייבין-במקצת על הרעיון, על ידי כניסה לקישור הזה בדפדפן שבו מותקן התוסף Tampermonkey (בדרך הזו גם תמיד תהיו בגרסה הכי חדשה של הסקריפט אוטומטית).

            אפשרות ב':
            מתקינים Tampermonkey.
            יוצרים סקריפט חדש.
            מדביקים את הקוד.
            שומרים.
            מרעננים את האתר שבו רוצים להשתמש.

            הקוד לסקריפט:

            // ==UserScript==
            // @name         Universal Voice to Text - Floating Dictation Button
            // @namespace    https://chat.openai.com/
            // @version      2.6.0
            // @description  Floating draggable speech-to-text button for almost any website. Dictates into focused inputs, textareas and contenteditable editors.
            // @author       Avraham + ChatGPT
            // @match        http://*/*
            // @match        https://*/*
            // @run-at       document-idle
            // @grant        GM_getValue
            // @grant        GM_setValue
            // @grant        GM_registerMenuCommand
            // ==/UserScript==
            
            (() => {
              'use strict';
            
              const APP_ID = 'uvtt-floating-root';
              const STORAGE_PREFIX = 'uvtt_tm_';
              const INTERIM_ATTR = 'data-uvtt-interim';
            
              const DEFAULT_SETTINGS = {
                language: 'he-IL',
                continuous: true,
                autoCapitalize: true,
                addSpaceBeforeText: true,
                showStatus: true,
                showLanguageSelect: true
              };
            
              const DEFAULT_POSITION = {
                mode: 'corner',
                right: 24,
                bottom: 24,
                left: null,
                top: null
              };
            
              const LANGUAGES = [
                { value: 'he-IL', label: 'עברית' },
                { value: 'en-US', label: 'English US' },
                { value: 'en-GB', label: 'English UK' },
                { value: 'ar-SA', label: 'العربية' },
                { value: 'fr-FR', label: 'Français' },
                { value: 'es-ES', label: 'Español' },
                { value: 'ru-RU', label: 'Русский' }
              ];
            
              const INPUT_TYPES = new Set([
                '', 'text', 'search', 'email', 'url', 'tel', 'password', 'number'
              ]);
            
              const state = {
                mounted: false,
                listening: false,
                recognition: null,
                settings: { ...DEFAULT_SETTINGS },
                position: { ...DEFAULT_POSITION },
                currentEditor: null,
                lastFocusedEditor: null,
                statusText: 'מוכן',
                suppressNextEndRestart: false,
                lastTranscriptAt: 0,
                bufferText: '',
                discardUntilFinal: false,
            
                interim: {
                  editor: null,
                  type: null,
                  node: null,
                  prefix: '',
                  rawText: '',
                  previewText: '',
                  inputStart: null,
                  inputLength: 0
                },
            
                dragging: {
                  active: false,
                  moved: false,
                  pointerId: null,
                  startedOnMic: false,
                  canDrag: false,
                  startX: 0,
                  startY: 0,
                  startLeft: 0,
                  startTop: 0
                }
              };
            
              let host;
              let shadow;
              let wrap;
              let micButton;
              let expandButton;
              let statusPill;
              let languageSelect;
              let settingsButton;
              let settingsPanel;
              let bufferPreview;
              let copyButton;
              let pasteButton;
              let clearButton;
              let resetPositionButton;
            
              function gmGet(key, fallback) {
                try {
                  if (typeof GM_getValue === 'function') {
                    return GM_getValue(`${STORAGE_PREFIX}${key}`, fallback);
                  }
                } catch (_) {}
            
                try {
                  const raw = localStorage.getItem(`${STORAGE_PREFIX}${key}`);
                  return raw === null ? fallback : JSON.parse(raw);
                } catch (_) {
                  return fallback;
                }
              }
            
              function gmSet(key, value) {
                try {
                  if (typeof GM_setValue === 'function') {
                    GM_setValue(`${STORAGE_PREFIX}${key}`, value);
                    return;
                  }
                } catch (_) {}
            
                try {
                  localStorage.setItem(`${STORAGE_PREFIX}${key}`, JSON.stringify(value));
                } catch (_) {}
              }
            
              function getSiteStorageKey(name) {
                const origin = (() => {
                  try {
                    return window.location.origin || `${window.location.protocol}//${window.location.host}`;
                  } catch (_) {
                    return 'unknown-origin';
                  }
                })();
            
                return `${name}:${origin}`;
              }
            
              function getPositionStorageKey() {
                return getSiteStorageKey('position');
              }
            
              function loadSettings() {
                const loaded = { ...DEFAULT_SETTINGS };
                for (const key of Object.keys(DEFAULT_SETTINGS)) {
                  loaded[key] = gmGet(key, DEFAULT_SETTINGS[key]);
                }
                state.settings = loaded;
            
                const loadedPosition = gmGet(getPositionStorageKey(), DEFAULT_POSITION);
                state.position = {
                  ...DEFAULT_POSITION,
                  ...(loadedPosition && typeof loadedPosition === 'object' ? loadedPosition : {})
                };
              }
            
              function saveSetting(key, value) {
                state.settings[key] = value;
                gmSet(key, value);
            
                if (key === 'language' && state.recognition && state.listening) {
                  setStatus('השפה תעודכן בהפעלה הבאה');
                }
            
                refreshUiVisibilitySettings();
              }
            
              function savePosition(position) {
                state.position = { ...state.position, ...position };
                gmSet(getPositionStorageKey(), state.position);
              }
            
              function supportsSpeechRecognition() {
                return Boolean(window.SpeechRecognition || window.webkitSpeechRecognition);
              }
            
              function getSpeechRecognitionCtor() {
                return window.SpeechRecognition || window.webkitSpeechRecognition;
              }
            
              function isInsideOwnUi(node) {
                if (!node) return false;
                return node === host || (host && host.contains(node));
              }
            
              function isEditableElement(element) {
                if (!element || !(element instanceof HTMLElement)) return false;
                if (isInsideOwnUi(element)) return false;
                if (element.isContentEditable) return true;
            
                const tag = element.tagName.toLowerCase();
                if (tag === 'textarea') return !element.disabled && !element.readOnly;
            
                if (tag === 'input') {
                  const input = element;
                  return INPUT_TYPES.has((input.getAttribute('type') || '').toLowerCase()) && !input.disabled && !input.readOnly;
                }
            
                return false;
              }
            
              function getEditorType(editor) {
                if (!editor) return null;
                const tag = editor.tagName?.toLowerCase();
                if (tag === 'textarea' || tag === 'input') return 'input';
                if (editor.isContentEditable) return 'contenteditable';
                return null;
              }
            
              function isElementVisible(el) {
                if (!el || !(el instanceof HTMLElement)) return false;
                const rect = el.getBoundingClientRect();
                const style = getComputedStyle(el);
                return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
              }
            
              function getCandidateEditors() {
                const selectors = [
                  'textarea',
                  'input[type="text"]',
                  'input[type="search"]',
                  'input[type="email"]',
                  'input[type="url"]',
                  'input[type="tel"]',
                  'input[type="password"]',
                  'input[type="number"]',
                  'input:not([type])',
                  '[contenteditable="true"]',
                  '[contenteditable="plaintext-only"]',
                  '[role="textbox"]'
                ];
            
                return Array.from(document.querySelectorAll(selectors.join(',')))
                  .filter((el) => isEditableElement(el) && isElementVisible(el));
              }
            
              function getDeepActiveElement(root = document) {
                let active = root.activeElement;
                while (active && active.shadowRoot && active.shadowRoot.activeElement) {
                  active = active.shadowRoot.activeElement;
                }
                return active;
              }
            
              function getBestEditor() {
                const active = getDeepActiveElement();
                if (isEditableElement(active) && isElementVisible(active)) return active;
            
                if (state.lastFocusedEditor && isEditableElement(state.lastFocusedEditor) && isElementVisible(state.lastFocusedEditor)) {
                  return state.lastFocusedEditor;
                }
            
                if (state.currentEditor && isEditableElement(state.currentEditor) && isElementVisible(state.currentEditor)) {
                  return state.currentEditor;
                }
            
                const selection = window.getSelection();
                if (selection && selection.anchorNode) {
                  const candidates = getCandidateEditors();
                  for (const editor of candidates) {
                    if (editor.contains(selection.anchorNode)) return editor;
                  }
                }
            
                return null;
              }
            
              function setStatus(text) {
                state.statusText = text;
                if (statusPill) statusPill.textContent = text;
              }
            
              function getLanguageLabel(value) {
                return LANGUAGES.find((lang) => lang.value === value)?.label || value;
              }
            
              function normalizeTranscript(rawText) {
                let text = (rawText || '').replace(/\s+/g, ' ').trim();
                if (!text) return '';
            
                if (state.settings.autoCapitalize && state.settings.language.startsWith('en')) {
                  text = text.charAt(0).toUpperCase() + text.slice(1);
                }
            
                return text;
              }
            
              function getInputPrefix(editor, start) {
                if (!state.settings.addSpaceBeforeText) return '';
                const left = String(editor.value || '').slice(0, Math.max(0, start ?? editor.selectionStart ?? 0));
                if (!left) return '';
                return /[\s\n]$/.test(left) ? '' : ' ';
              }
            
              function editorContainsSelection(editor) {
                const selection = window.getSelection();
                if (!selection || selection.rangeCount === 0) return false;
                return !!selection.anchorNode && editor.contains(selection.anchorNode);
              }
            
              function moveCaretToEndContentEditable(editor) {
                editor.focus();
                const selection = window.getSelection();
                if (!selection) return;
            
                const range = document.createRange();
                range.selectNodeContents(editor);
                range.collapse(false);
                selection.removeAllRanges();
                selection.addRange(range);
              }
            
              function getPlainText(editor) {
                return (editor.innerText || editor.textContent || '').replace(/\u00a0/g, ' ');
              }
            
              function getContentEditablePrefix(editor) {
                if (!state.settings.addSpaceBeforeText) return '';
            
                let leftText = '';
                const selection = window.getSelection();
                if (selection && selection.rangeCount > 0 && editorContainsSelection(editor)) {
                  try {
                    const probe = selection.getRangeAt(0).cloneRange();
                    probe.setStart(editor, 0);
                    leftText = probe.toString();
                  } catch (_) {
                    leftText = getPlainText(editor);
                  }
                } else {
                  leftText = getPlainText(editor);
                }
            
                if (!leftText) return '';
                return /[\s\n]$/.test(leftText) ? '' : ' ';
              }
            
              function dispatchInput(editor, data = '') {
                try {
                  editor.dispatchEvent(new InputEvent('input', {
                    inputType: 'insertText',
                    data,
                    bubbles: true,
                    composed: true
                  }));
                } catch (_) {
                  editor.dispatchEvent(new Event('input', { bubbles: true }));
                }
              }
            
              function dispatchContentEditableDomChange(editor) {
                // In React/ProseMirror-style editors, sending InputEvent data for every
                // interim result can make the app append each partial transcript again.
                // We already changed the DOM ourselves, so notify the editor with a
                // neutral input event instead of another "insertText" payload.
                try {
                  editor.dispatchEvent(new InputEvent('input', {
                    inputType: 'insertReplacementText',
                    data: null,
                    bubbles: true,
                    composed: true
                  }));
                } catch (_) {
                  editor.dispatchEvent(new Event('input', { bubbles: true }));
                }
              }
            
              function setInputRangeText(editor, replacement, start, end) {
                editor.focus();
            
                try {
                  editor.setRangeText(replacement, start, end, 'end');
                } catch (_) {
                  const value = String(editor.value || '');
                  editor.value = `${value.slice(0, start)}${replacement}${value.slice(end)}`;
                  const caret = start + replacement.length;
                  try { editor.setSelectionRange(caret, caret); } catch (_) {}
                }
            
                dispatchInput(editor, replacement);
              }
            
              function insertTextIntoInput(editor, rawText) {
                const normalized = normalizeTranscript(rawText);
                if (!normalized) return false;
            
                editor.focus();
                const start = editor.selectionStart ?? String(editor.value || '').length;
                const end = editor.selectionEnd ?? start;
                const text = `${getInputPrefix(editor, start)}${normalized}`;
                setInputRangeText(editor, text, start, end);
                return true;
              }
            
              function getTextPositionInContentEditable(editor, charOffset) {
                const target = Math.max(0, Number(charOffset) || 0);
                const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
                let remaining = target;
                let lastPosition = null;
                let node;
            
                while ((node = walker.nextNode())) {
                  const length = node.nodeValue.length;
                  if (remaining <= length) return { node, offset: remaining };
                  remaining -= length;
                  lastPosition = { node, offset: length };
                }
            
                return lastPosition || { node: editor, offset: 0 };
              }
            
              function selectContentEditableTextRange(editor, startOffset, endOffset) {
                const start = getTextPositionInContentEditable(editor, startOffset);
                const end = getTextPositionInContentEditable(editor, endOffset);
                if (!start || !end) return false;
            
                try {
                  const range = document.createRange();
                  range.setStart(start.node, start.offset);
                  range.setEnd(end.node, end.offset);
                  const selection = window.getSelection();
                  if (!selection) return false;
                  selection.removeAllRanges();
                  selection.addRange(range);
                  return true;
                } catch (_) {
                  return false;
                }
              }
            
              function selectLastCharactersBeforeCaret(editor, charCount) {
                const length = Math.max(0, Number(charCount) || 0);
                if (!length) return true;
            
                editor.focus();
                if (!editorContainsSelection(editor)) moveCaretToEndContentEditable(editor);
            
                const selection = window.getSelection();
                if (!selection || selection.rangeCount === 0) return false;
            
                try {
                  const caret = selection.getRangeAt(0).cloneRange();
                  caret.collapse(false);
            
                  const beforeCaret = caret.cloneRange();
                  beforeCaret.selectNodeContents(editor);
                  beforeCaret.setEnd(caret.endContainer, caret.endOffset);
            
                  const endOffset = beforeCaret.toString().length;
                  const startOffset = Math.max(0, endOffset - length);
                  return selectContentEditableTextRange(editor, startOffset, endOffset);
                } catch (_) {
                  return false;
                }
              }
            
              function selectLastMatchingText(editor, text) {
                const needle = String(text || '');
                if (!needle) return false;
            
                const haystack = getPlainText(editor);
                const startOffset = haystack.lastIndexOf(needle);
                if (startOffset < 0) return false;
            
                return selectContentEditableTextRange(editor, startOffset, startOffset + needle.length);
              }
            
              function replaceContentEditableSelection(editor, text) {
                editor.focus();
            
                let inserted = false;
                try {
                  if (document.queryCommandSupported && document.queryCommandSupported('insertText')) {
                    inserted = document.execCommand('insertText', false, text);
                  }
                } catch (_) {
                  inserted = false;
                }
            
                if (!inserted) {
                  const selection = window.getSelection();
                  if (!selection || selection.rangeCount === 0) return false;
                  const range = selection.getRangeAt(0);
                  range.deleteContents();
                  const textNode = document.createTextNode(text);
                  range.insertNode(textNode);
                  range.setStartAfter(textNode);
                  range.setEndAfter(textNode);
                  selection.removeAllRanges();
                  selection.addRange(range);
                  dispatchContentEditableDomChange(editor);
                }
            
                return true;
              }
            
              function replaceTrackedContentEditableText(editor, oldLength, oldText, newText) {
                editor.focus();
            
                const previous = String(oldText || '');
                if (oldLength > 0 || previous) {
                  // Never replace arbitrary text near the caret. If the live preview was
                  // manually deleted/changed by the user, do not bring it back later.
                  if (!previous || !selectLastMatchingText(editor, previous)) return false;
                }
            
                return replaceContentEditableSelection(editor, newText);
              }
            
              function insertTextIntoContentEditable(editor, rawText) {
                const normalized = normalizeTranscript(rawText);
                if (!normalized) return false;
            
                const text = `${getContentEditablePrefix(editor)}${normalized}`;
                editor.focus();
            
                if (!editorContainsSelection(editor)) {
                  moveCaretToEndContentEditable(editor);
                }
            
                let inserted = false;
                try {
                  if (document.queryCommandSupported && document.queryCommandSupported('insertText')) {
                    inserted = document.execCommand('insertText', false, text);
                  }
                } catch (_) {
                  inserted = false;
                }
            
                if (!inserted) {
                  const selection = window.getSelection();
                  if (!selection || selection.rangeCount === 0) return false;
                  const range = selection.getRangeAt(0);
                  range.deleteContents();
                  const textNode = document.createTextNode(text);
                  range.insertNode(textNode);
                  range.setStartAfter(textNode);
                  range.setEndAfter(textNode);
                  selection.removeAllRanges();
                  selection.addRange(range);
                  dispatchContentEditableDomChange(editor);
                }
            
                return true;
              }
            
              function insertTextIntoEditor(editor, rawText) {
                const type = getEditorType(editor);
                if (type === 'input') return insertTextIntoInput(editor, rawText);
                if (type === 'contenteditable') return insertTextIntoContentEditable(editor, rawText);
                return false;
              }
            
              function resetInterimState() {
                state.interim = {
                  editor: null,
                  type: null,
                  node: null,
                  prefix: '',
                  rawText: '',
                  previewText: '',
                  inputStart: null,
                  inputLength: 0
                };
              }
            
              function clearInterim({ keepVisualText = false, dispatch = true } = {}) {
                const interim = state.interim;
                if (!interim.editor) {
                  resetInterimState();
                  return;
                }
            
                if (interim.type === 'contenteditable' && interim.node && document.contains(interim.node)) {
                  if (keepVisualText && interim.node.textContent) {
                    const textNode = document.createTextNode(interim.node.textContent);
                    const parent = interim.node.parentNode;
                    if (parent) {
                      parent.replaceChild(textNode, interim.node);
            
                      const selection = window.getSelection();
                      if (selection) {
                        const range = document.createRange();
                        range.setStartAfter(textNode);
                        range.collapse(true);
                        selection.removeAllRanges();
                        selection.addRange(range);
                      }
                    }
                  } else {
                    interim.node.remove();
                  }
            
                  if (dispatch) dispatchContentEditableDomChange(interim.editor);
                }
            
                if (interim.type === 'input' && interim.inputStart !== null && !keepVisualText) {
                  const start = interim.inputStart;
                  const end = start + interim.inputLength;
                  setInputRangeText(interim.editor, '', start, end);
                }
            
                resetInterimState();
              }
            
              function inputPreviewIsIntact(interim = state.interim) {
                if (!interim.editor || interim.type !== 'input' || interim.inputStart === null || interim.inputLength <= 0) return true;
            
                const value = String(interim.editor.value || '');
                const expected = String(interim.previewText || '');
                if (!expected) return true;
            
                const exact = value.slice(interim.inputStart, interim.inputStart + interim.inputLength) === expected;
                if (exact) return true;
            
                const fallbackIndex = value.lastIndexOf(expected);
                if (fallbackIndex >= 0) {
                  interim.inputStart = fallbackIndex;
                  return true;
                }
            
                return false;
              }
            
              function contentEditablePreviewIsIntact(interim = state.interim) {
                if (!interim.editor || interim.type !== 'contenteditable' || interim.inputLength <= 0) return true;
                const expected = String(interim.previewText || '');
                if (!expected) return true;
                return getPlainText(interim.editor).includes(expected);
              }
            
              function interimPreviewIsIntact() {
                if (!state.interim.editor) return true;
                if (state.interim.type === 'input') return inputPreviewIsIntact();
                if (state.interim.type === 'contenteditable') return contentEditablePreviewIsIntact();
                return true;
              }
            
              function discardCurrentSpeechSegment() {
                state.discardUntilFinal = true;
                resetInterimState();
                updateBufferPreview();
                setStatus('הטקסט נמחק — מדלג על המקטע');
              }
            
              function renderInterimInInput(editor, rawText) {
                const normalized = normalizeTranscript(rawText);
                if (!normalized) return false;
            
                editor.focus();
            
                if (state.interim.editor !== editor || state.interim.type !== 'input') {
                  clearInterim({ keepVisualText: true });
                  const start = editor.selectionStart ?? String(editor.value || '').length;
                  state.interim.editor = editor;
                  state.interim.type = 'input';
                  state.interim.inputStart = start;
                  state.interim.prefix = getInputPrefix(editor, start);
                  state.interim.inputLength = 0;
                  state.interim.previewText = '';
                } else if (!inputPreviewIsIntact()) {
                  discardCurrentSpeechSegment();
                  return false;
                }
            
                const text = `${state.interim.prefix}${normalized}`;
                const start = state.interim.inputStart;
                const end = start + state.interim.inputLength;
                setInputRangeText(editor, text, start, end);
                state.interim.inputLength = text.length;
                state.interim.previewText = text;
                state.interim.rawText = rawText;
                return true;
              }
            
              function ensureInterimNode(editor) {
                if (state.interim.editor === editor && state.interim.type === 'contenteditable' && state.interim.node && document.contains(state.interim.node)) {
                  return state.interim.node;
                }
            
                clearInterim({ keepVisualText: true });
            
                editor.focus();
                if (!editorContainsSelection(editor)) {
                  moveCaretToEndContentEditable(editor);
                }
            
                const selection = window.getSelection();
                if (!selection || selection.rangeCount === 0) return null;
            
                const span = document.createElement('span');
                span.setAttribute(INTERIM_ATTR, '1');
                span.style.opacity = '0.72';
                span.style.whiteSpace = 'pre-wrap';
                span.style.borderBottom = '1px dotted currentColor';
            
                state.interim.editor = editor;
                state.interim.type = 'contenteditable';
                state.interim.node = span;
                state.interim.prefix = getContentEditablePrefix(editor);
                state.interim.rawText = '';
            
                span.textContent = state.interim.prefix;
            
                const range = selection.getRangeAt(0);
                range.deleteContents();
                range.insertNode(span);
            
                const after = document.createRange();
                after.setStartAfter(span);
                after.collapse(true);
                selection.removeAllRanges();
                selection.addRange(after);
            
                return span;
              }
            
              function renderInterimInContentEditable(editor, rawText) {
                const normalized = normalizeTranscript(rawText);
                if (!normalized) return false;
            
                if (state.interim.editor !== editor || state.interim.type !== 'contenteditable') {
                  clearInterim({ keepVisualText: true });
                  editor.focus();
                  if (!editorContainsSelection(editor)) moveCaretToEndContentEditable(editor);
                  state.interim.editor = editor;
                  state.interim.type = 'contenteditable';
                  state.interim.node = null;
                  state.interim.prefix = getContentEditablePrefix(editor);
                  state.interim.inputLength = 0;
                  state.interim.previewText = '';
                  state.interim.rawText = '';
                }
            
                const text = `${state.interim.prefix}${normalized}`;
                const ok = replaceTrackedContentEditableText(
                  editor,
                  state.interim.inputLength,
                  state.interim.previewText,
                  text
                );
            
                if (!ok) {
                  discardCurrentSpeechSegment();
                  return false;
                }
            
                state.interim.inputLength = text.length;
                state.interim.previewText = text;
                state.interim.rawText = rawText;
                return true;
              }
            
              function renderInterimTranscript(editor, rawText) {
                const type = getEditorType(editor);
                if (type === 'input') return renderInterimInInput(editor, rawText);
                if (type === 'contenteditable') return renderInterimInContentEditable(editor, rawText);
                return false;
              }
            
              function commitFinalToInput(editor, rawText) {
                const normalized = normalizeTranscript(rawText);
                if (!normalized) return false;
            
                if (state.interim.editor === editor && state.interim.type === 'input' && state.interim.inputStart !== null) {
                  if (!inputPreviewIsIntact()) {
                    resetInterimState();
                    setStatus('דילג על הטקסט שנמחק');
                    return false;
                  }
            
                  const text = `${state.interim.prefix}${normalized}`;
                  const start = state.interim.inputStart;
                  const end = start + state.interim.inputLength;
                  setInputRangeText(editor, text, start, end);
                  resetInterimState();
                  return true;
                }
            
                return insertTextIntoInput(editor, normalized);
              }
            
              function commitFinalToContentEditable(editor, rawText) {
                const normalized = normalizeTranscript(rawText);
                if (!normalized) return false;
            
                if (state.interim.editor === editor && state.interim.type === 'contenteditable') {
                  const finalText = `${state.interim.prefix}${normalized}`;
                  const ok = replaceTrackedContentEditableText(
                    editor,
                    state.interim.inputLength,
                    state.interim.previewText,
                    finalText
                  );
                  resetInterimState();
                  if (!ok) setStatus('דילג על הטקסט שנמחק');
                  return ok;
                }
            
                return insertTextIntoContentEditable(editor, normalized);
              }
            
              function commitFinalTranscript(editor, rawText) {
                const type = getEditorType(editor);
                if (type === 'input') return commitFinalToInput(editor, rawText);
                if (type === 'contenteditable') return commitFinalToContentEditable(editor, rawText);
                return false;
              }
            
              function appendToBuffer(rawText) {
                const normalized = normalizeTranscript(rawText);
                if (!normalized) return;
            
                const prefix = state.bufferText && !/[\s\n]$/.test(state.bufferText) ? ' ' : '';
                state.bufferText = `${state.bufferText}${prefix}${normalized}`;
                updateBufferPreview();
              }
            
              function updateBufferPreview(interimText = '') {
                if (!bufferPreview) return;
                const interim = normalizeTranscript(interimText);
                const full = `${state.bufferText}${interim ? `${state.bufferText ? ' ' : ''}${interim}` : ''}`.trim();
                bufferPreview.textContent = full || 'אין טקסט שמור עדיין.';
                bufferPreview.classList.toggle('empty', !full);
              }
            
              async function copyBufferToClipboard() {
                const text = state.bufferText.trim();
                if (!text) {
                  setStatus('אין טקסט להעתקה');
                  return;
                }
            
                try {
                  await navigator.clipboard.writeText(text);
                  setStatus('הטקסט הועתק');
                } catch (_) {
                  setStatus('לא ניתן להעתיק אוטומטית');
                }
              }
            
              function pasteBufferToActiveEditor() {
                const text = state.bufferText.trim();
                if (!text) {
                  setStatus('אין טקסט להדבקה');
                  return;
                }
            
                const editor = getBestEditor();
                if (!editor) {
                  setStatus('בחר קודם שדה טקסט בדף');
                  return;
                }
            
                if (insertTextIntoEditor(editor, text)) {
                  state.bufferText = '';
                  updateBufferPreview();
                  setStatus('הטקסט הודבק לשדה הפעיל');
                }
              }
            
              function updateButtonState() {
                if (!micButton) return;
            
                const supported = supportsSpeechRecognition();
                micButton.classList.toggle('recording', state.listening);
                micButton.classList.toggle('unsupported', !supported);
                micButton.title = supported
                  ? (state.listening ? 'עצור תמלול · Alt+גרירה להזזה' : 'התחל תמלול · Alt+גרירה להזזה')
                  : 'הדפדפן לא תומך בתמלול קולי';
                micButton.setAttribute('aria-pressed', state.listening ? 'true' : 'false');
            
                if (!supported) {
                  setStatus('לא נתמך בדפדפן הזה');
                }
              }
            
              function refreshUiVisibilitySettings() {
                if (!wrap) return;
                wrap.classList.toggle('hide-status', !state.settings.showStatus);
                wrap.classList.toggle('hide-lang', !state.settings.showLanguageSelect);
              }
            
              function applyPosition() {
                if (!wrap) return;
            
                if (state.position.mode === 'free' && Number.isFinite(state.position.left) && Number.isFinite(state.position.top)) {
                  // Intentionally do not clamp to the viewport. Users can move the button
                  // partially or fully outside the visible page, and restore it with
                  // Alt+Shift+M or the reset button when the panel is visible.
                  wrap.style.left = `${state.position.left}px`;
                  wrap.style.top = `${state.position.top}px`;
                  wrap.style.right = 'auto';
                  wrap.style.bottom = 'auto';
                  return;
                }
            
                wrap.style.left = 'auto';
                wrap.style.top = 'auto';
                wrap.style.right = `${state.position.right ?? 24}px`;
                wrap.style.bottom = `${state.position.bottom ?? 24}px`;
              }
            
              function resetPosition() {
                savePosition({ ...DEFAULT_POSITION });
                requestAnimationFrame(applyPosition);
                setStatus('המיקום אופס לאתר הזה');
              }
            
              function ensureUi() {
                if (state.mounted) return;
            
                host = document.createElement('div');
                host.id = APP_ID;
                host.setAttribute('aria-hidden', 'false');
                document.documentElement.appendChild(host);
            
                shadow = host.attachShadow({ mode: 'open' });
                shadow.innerHTML = `
                  <style>
                    :host { all: initial; }
            
                    .wrap {
                      position: fixed;
                      z-index: 2147483647;
                      display: inline-flex;
                      align-items: center;
                      gap: 8px;
                      direction: rtl;
                      font-family: Arial, sans-serif;
                      user-select: none;
                      touch-action: none;
                    }
            
                    .options {
                      display: inline-flex;
                      align-items: center;
                      gap: 8px;
                    }
            
                    .wrap.collapsed .options,
                    .wrap.collapsed .panel { display: none !important; }
            
                    .status,
                    .lang,
                    .settings-toggle,
                    .expand-toggle {
                      background: rgba(17, 24, 39, 0.94);
                      color: #fff;
                      border-radius: 999px;
                      box-shadow: 0 8px 20px rgba(0,0,0,0.18);
                    }
            
                    .status {
                      padding: 7px 11px;
                      font-size: 12px;
                      line-height: 1;
                      white-space: nowrap;
                      max-width: min(280px, 40vw);
                      overflow: hidden;
                      text-overflow: ellipsis;
                    }
            
                    .hide-status .status { display: none; }
                    .hide-lang .lang { display: none; }
            
                    .lang {
                      border: 0;
                      outline: 0;
                      padding: 7px 10px;
                      font-size: 12px;
                      cursor: pointer;
                      max-width: 125px;
                    }
            
                    .mic-shell {
                      position: relative;
                      width: 54px;
                      height: 54px;
                      display: inline-flex;
                      align-items: center;
                      justify-content: center;
                      flex: 0 0 auto;
                    }
            
                    .btn,
                    .settings-toggle,
                    .expand-toggle {
                      border: none;
                      cursor: pointer;
                      display: inline-flex;
                      align-items: center;
                      justify-content: center;
                      transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease, background 120ms ease;
                    }
            
                    .btn {
                      width: 54px;
                      height: 54px;
                      border-radius: 999px;
                      background: #1a73e8;
                      color: #fff;
                      box-shadow: 0 12px 26px rgba(26, 115, 232, 0.34);
                      position: relative;
                    }
            
                    .btn:hover,
                    .settings-toggle:hover,
                    .expand-toggle:hover { transform: translateY(-1px); }
                    .btn:active,
                    .settings-toggle:active,
                    .expand-toggle:active { transform: translateY(0); }
            
                    .btn.recording {
                      background: #d93025;
                      box-shadow: 0 12px 26px rgba(217, 48, 37, 0.38);
                      animation: pulse 1.2s infinite;
                    }
            
                    .btn.unsupported {
                      cursor: not-allowed;
                      opacity: 0.65;
                      box-shadow: none;
                      animation: none;
                    }
            
                    .wrap.dragging .btn { cursor: grabbing; }
            
                    .settings-toggle {
                      width: 34px;
                      height: 34px;
                      font-size: 16px;
                    }
            
                    .expand-toggle {
                      position: absolute;
                      left: -3px;
                      bottom: -3px;
                      z-index: 2;
                      width: 22px;
                      height: 22px;
                      padding: 0;
                      font-size: 12px;
                      font-weight: 700;
                      line-height: 1;
                      box-shadow: 0 6px 14px rgba(0,0,0,0.22);
                    }
            
                    .wrap.expanded .expand-toggle {
                      background: rgba(17, 24, 39, 0.98);
                    }
            
                    .icon {
                      width: 26px;
                      height: 26px;
                      fill: currentColor;
                      pointer-events: none;
                    }
            
                    .panel {
                      position: absolute;
                      right: 0;
                      bottom: 66px;
                      display: none;
                      width: min(320px, calc(100vw - 24px));
                      padding: 12px;
                      border-radius: 16px;
                      background: rgba(255,255,255,0.99);
                      color: #202124;
                      box-shadow: 0 16px 42px rgba(0,0,0,0.24);
                      border: 1px solid rgba(0,0,0,0.08);
                    }
            
                    .panel.open { display: block; }
            
                    .panel-title {
                      font-weight: 700;
                      margin: 0 0 10px;
                      font-size: 14px;
                    }
            
                    .row {
                      display: flex;
                      align-items: center;
                      justify-content: space-between;
                      gap: 12px;
                      margin: 9px 0;
                      font-size: 13px;
                    }
            
                    .row label { cursor: pointer; }
            
                    .panel select {
                      max-width: 150px;
                      border: 1px solid #dadce0;
                      border-radius: 8px;
                      padding: 5px 7px;
                      background: #fff;
                    }
            
                    .buffer {
                      margin-top: 10px;
                      padding: 9px;
                      border: 1px solid #e0e0e0;
                      border-radius: 12px;
                      background: #f8fafd;
                      max-height: 110px;
                      overflow: auto;
                      white-space: pre-wrap;
                      line-height: 1.45;
                      font-size: 12px;
                      user-select: text;
                    }
            
                    .buffer.empty { color: #6b7280; }
            
                    .actions {
                      display: flex;
                      flex-wrap: wrap;
                      gap: 8px;
                      margin-top: 10px;
                    }
            
                    .panel-button {
                      border: 1px solid #dadce0;
                      background: #fff;
                      border-radius: 999px;
                      padding: 6px 10px;
                      cursor: pointer;
                      font-size: 12px;
                    }
            
                    .panel-button:hover { background: #f1f3f4; }
            
                    .hint {
                      margin: 10px 0 0;
                      font-size: 11px;
                      color: #5f6368;
                      line-height: 1.45;
                    }
            
                    @keyframes pulse {
                      0% { transform: scale(1); }
                      50% { transform: scale(1.06); }
                      100% { transform: scale(1); }
                    }
                  </style>
            
                  <div class="wrap collapsed" id="wrap">
                    <div class="options" id="quickOptions">
                      <div class="status" id="status">מוכן</div>
                      <select class="lang" id="languageSelect" title="שפת תמלול"></select>
                      <button class="settings-toggle" id="settingsBtn" type="button" title="הגדרות">⚙</button>
                    </div>
                    <div class="mic-shell" id="micShell">
                      <button class="btn" id="micBtn" type="button" title="התחל תמלול">
                        <svg class="icon" viewBox="0 0 24 24" aria-hidden="true">
                          <path d="M12 15a3.75 3.75 0 0 0 3.75-3.75V6.75a3.75 3.75 0 1 0-7.5 0v4.5A3.75 3.75 0 0 0 12 15Zm6-3.75a.75.75 0 0 1 1.5 0A7.5 7.5 0 0 1 12.75 18.7V21a.75.75 0 0 1-1.5 0v-2.3A7.5 7.5 0 0 1 4.5 11.25a.75.75 0 0 1 1.5 0 6 6 0 0 0 12 0Z"></path>
                        </svg>
                      </button>
                      <button class="expand-toggle" id="expandBtn" type="button" title="פתח אפשרויות" aria-expanded="false">▴</button>
                    </div>
            
                    <div class="panel" id="settingsPanel">
                      <p class="panel-title">תמלול קולי גלובלי</p>
            
                      <div class="row">
                        <label for="panelLanguage">שפה</label>
                        <select id="panelLanguage"></select>
                      </div>
            
                      <div class="row">
                        <label for="continuousToggle">האזנה רציפה</label>
                        <input id="continuousToggle" type="checkbox">
                      </div>
            
                      <div class="row">
                        <label for="spaceToggle">להוסיף רווח לפני התמלול</label>
                        <input id="spaceToggle" type="checkbox">
                      </div>
            
                      <div class="row">
                        <label for="capitalizeToggle">Capital באנגלית</label>
                        <input id="capitalizeToggle" type="checkbox">
                      </div>
            
                      <div class="row">
                        <label for="statusToggle">להציג סטטוס</label>
                        <input id="statusToggle" type="checkbox">
                      </div>
            
                      <div class="row">
                        <label for="langToggle">להציג בחירת שפה</label>
                        <input id="langToggle" type="checkbox">
                      </div>
            
                      <div class="buffer empty" id="bufferPreview">אין טקסט שמור עדיין.</div>
            
                      <div class="actions">
                        <button class="panel-button" id="pasteBtn" type="button">הדבק לשדה הפעיל</button>
                        <button class="panel-button" id="copyBtn" type="button">העתק</button>
                        <button class="panel-button" id="clearBtn" type="button">נקה</button>
                        <button class="panel-button" id="resetPositionBtn" type="button">אפס מיקום</button>
                      </div>
            
                      <div class="hint">
                        לחץ בתוך שדה טקסט ואז על המיקרופון. כדי להזיז את הכפתור: החזק Alt וגרור את המיקרופון. המיקום נשמר בנפרד לכל אתר. אם הכפתור יצא מהמסך, Alt+Shift+M מחזיר אותו לברירת המחדל. אם אין שדה פעיל, התמלול נשמר כאן ואפשר להעתיק/להדביק אותו אחר כך.
                      </div>
                    </div>
                  </div>
                `;
            
                wrap = shadow.getElementById('wrap');
                micButton = shadow.getElementById('micBtn');
                expandButton = shadow.getElementById('expandBtn');
                statusPill = shadow.getElementById('status');
                languageSelect = shadow.getElementById('languageSelect');
                settingsButton = shadow.getElementById('settingsBtn');
                settingsPanel = shadow.getElementById('settingsPanel');
                bufferPreview = shadow.getElementById('bufferPreview');
                copyButton = shadow.getElementById('copyBtn');
                pasteButton = shadow.getElementById('pasteBtn');
                clearButton = shadow.getElementById('clearBtn');
                resetPositionButton = shadow.getElementById('resetPositionBtn');
            
                hydrateLanguageSelects();
                hydratePanelControls();
                bindUiEvents();
            
                state.mounted = true;
                refreshUiVisibilitySettings();
                updateBufferPreview();
                updateButtonState();
            
                requestAnimationFrame(applyPosition);
              }
            
              function setExpanded(expanded) {
                if (!wrap || !expandButton) return;
            
                wrap.classList.toggle('expanded', Boolean(expanded));
                wrap.classList.toggle('collapsed', !expanded);
                expandButton.textContent = expanded ? '▾' : '▴';
                expandButton.title = expanded ? 'סגור אפשרויות' : 'פתח אפשרויות';
                expandButton.setAttribute('aria-expanded', expanded ? 'true' : 'false');
            
                if (settingsPanel) settingsPanel.classList.toggle('open', Boolean(expanded));
                requestAnimationFrame(applyPosition);
              }
            
              function toggleExpanded() {
                setExpanded(!wrap.classList.contains('expanded'));
              }
            
              function hydrateLanguageSelects() {
                const panelLanguage = shadow.getElementById('panelLanguage');
                const optionsHtml = LANGUAGES.map((lang) => `<option value="${lang.value}">${lang.label}</option>`).join('');
            
                languageSelect.innerHTML = optionsHtml;
                panelLanguage.innerHTML = optionsHtml;
                languageSelect.value = state.settings.language;
                panelLanguage.value = state.settings.language;
            
                const onLanguageChange = (value) => {
                  saveSetting('language', value);
                  languageSelect.value = value;
                  panelLanguage.value = value;
                  setStatus(`שפה: ${getLanguageLabel(value)}`);
                };
            
                languageSelect.addEventListener('change', () => onLanguageChange(languageSelect.value));
                panelLanguage.addEventListener('change', () => onLanguageChange(panelLanguage.value));
              }
            
              function hydratePanelControls() {
                const continuousToggle = shadow.getElementById('continuousToggle');
                const spaceToggle = shadow.getElementById('spaceToggle');
                const capitalizeToggle = shadow.getElementById('capitalizeToggle');
                const statusToggle = shadow.getElementById('statusToggle');
                const langToggle = shadow.getElementById('langToggle');
            
                continuousToggle.checked = Boolean(state.settings.continuous);
                spaceToggle.checked = Boolean(state.settings.addSpaceBeforeText);
                capitalizeToggle.checked = Boolean(state.settings.autoCapitalize);
                statusToggle.checked = Boolean(state.settings.showStatus);
                langToggle.checked = Boolean(state.settings.showLanguageSelect);
            
                continuousToggle.addEventListener('change', () => saveSetting('continuous', continuousToggle.checked));
                spaceToggle.addEventListener('change', () => saveSetting('addSpaceBeforeText', spaceToggle.checked));
                capitalizeToggle.addEventListener('change', () => saveSetting('autoCapitalize', capitalizeToggle.checked));
                statusToggle.addEventListener('change', () => saveSetting('showStatus', statusToggle.checked));
                langToggle.addEventListener('change', () => saveSetting('showLanguageSelect', langToggle.checked));
              }
            
              function bindUiEvents() {
                const preventFocusLoss = (event) => {
                  event.preventDefault();
                  event.stopPropagation();
                };
            
                micButton.addEventListener('pointerdown', onPointerDownForDragAndClick, true);
            
                for (const el of [expandButton, settingsButton, languageSelect, copyButton, pasteButton, clearButton, resetPositionButton]) {
                  el.addEventListener('pointerdown', preventFocusLoss, true);
                }
            
                expandButton.addEventListener('click', toggleExpanded);
            
                settingsButton.addEventListener('click', () => {
                  setExpanded(true);
                  settingsPanel.classList.toggle('open');
                  requestAnimationFrame(applyPosition);
                });
            
                copyButton.addEventListener('click', copyBufferToClipboard);
                pasteButton.addEventListener('click', pasteBufferToActiveEditor);
                clearButton.addEventListener('click', () => {
                  state.bufferText = '';
                  updateBufferPreview();
                  setStatus('הטקסט השמור נמחק');
                });
                resetPositionButton.addEventListener('click', resetPosition);
            
                document.addEventListener('click', (event) => {
                  const path = event.composedPath ? event.composedPath() : [];
                  if (!path.includes(host)) {
                    setExpanded(false);
                  }
                }, true);
              }
            
              function onPointerDownForDragAndClick(event) {
                event.preventDefault();
                event.stopPropagation();
            
                const rect = wrap.getBoundingClientRect();
                const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
                const startedOnMic = event.currentTarget === micButton || path.includes(micButton);
            
                state.dragging = {
                  active: true,
                  moved: false,
                  pointerId: event.pointerId,
                  startedOnMic,
                  canDrag: Boolean(event.altKey && startedOnMic),
                  startX: event.clientX,
                  startY: event.clientY,
                  startLeft: rect.left,
                  startTop: rect.top
                };
            
                if (state.dragging.canDrag) wrap.classList.add('dragging');
            
                try { event.currentTarget.setPointerCapture(event.pointerId); } catch (_) {}
            
                window.addEventListener('pointermove', onPointerMoveDrag, true);
                window.addEventListener('pointerup', onPointerUpDrag, true);
                window.addEventListener('pointercancel', onPointerCancelDrag, true);
              }
            
              function onPointerMoveDrag(event) {
                if (!state.dragging.active || event.pointerId !== state.dragging.pointerId) return;
                if (!state.dragging.canDrag) return;
            
                const dx = event.clientX - state.dragging.startX;
                const dy = event.clientY - state.dragging.startY;
            
                if (!state.dragging.moved && Math.hypot(dx, dy) < 6) return;
                state.dragging.moved = true;
            
                const left = state.dragging.startLeft + dx;
                const top = state.dragging.startTop + dy;
            
                wrap.style.left = `${left}px`;
                wrap.style.top = `${top}px`;
                wrap.style.right = 'auto';
                wrap.style.bottom = 'auto';
              }
            
              function onPointerUpDrag(event) {
                if (!state.dragging.active || event.pointerId !== state.dragging.pointerId) return;
            
                const wasDrag = state.dragging.moved;
                const rect = wrap.getBoundingClientRect();
            
                window.removeEventListener('pointermove', onPointerMoveDrag, true);
                window.removeEventListener('pointerup', onPointerUpDrag, true);
                window.removeEventListener('pointercancel', onPointerCancelDrag, true);
                wrap.classList.remove('dragging');
            
                if (wasDrag) {
                  savePosition({ mode: 'free', left: rect.left, top: rect.top, right: null, bottom: null });
                  // Keep the UI quiet after dragging; the saved position is obvious visually.
                  setStatus(state.listening ? 'מקשיב...' : 'מוכן');
                } else {
                  const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
                  const endedOnMic = event.target === micButton || micButton.contains(event.target) || path.includes(micButton);
                  if (!state.dragging.canDrag && (state.dragging.startedOnMic || endedOnMic)) toggleListening();
                }
            
                state.dragging.active = false;
                state.dragging.moved = false;
                state.dragging.startedOnMic = false;
                state.dragging.canDrag = false;
              }
            
              function onPointerCancelDrag(event) {
                if (!state.dragging.active || event.pointerId !== state.dragging.pointerId) return;
                window.removeEventListener('pointermove', onPointerMoveDrag, true);
                window.removeEventListener('pointerup', onPointerUpDrag, true);
                window.removeEventListener('pointercancel', onPointerCancelDrag, true);
                wrap.classList.remove('dragging');
                state.dragging.active = false;
                state.dragging.moved = false;
                state.dragging.startedOnMic = false;
                state.dragging.canDrag = false;
              }
            
              function toggleListening() {
                if (!supportsSpeechRecognition()) {
                  setStatus('הדפדפן לא תומך בתמלול קולי');
                  updateButtonState();
                  return;
                }
            
                if (state.listening) stopListening();
                else startListening();
              }
            
              function createRecognition() {
                const Recognition = getSpeechRecognitionCtor();
                const recognition = new Recognition();
            
                recognition.lang = state.settings.language || 'he-IL';
                recognition.interimResults = true;
                recognition.continuous = Boolean(state.settings.continuous);
                recognition.maxAlternatives = 1;
            
                recognition.onstart = () => {
                  state.listening = true;
                  setStatus('מקשיב...');
                  updateButtonState();
                };
            
                recognition.onresult = (event) => {
                  let finalText = '';
                  let interimText = '';
            
                  for (let i = event.resultIndex; i < event.results.length; i += 1) {
                    const result = event.results[i];
                    const transcript = result[0]?.transcript || '';
                    if (result.isFinal) finalText += transcript;
                    else interimText += transcript;
                  }
            
                  if (state.interim.editor && !interimPreviewIsIntact()) {
                    discardCurrentSpeechSegment();
                  }
            
                  if (state.discardUntilFinal) {
                    if (finalText.trim()) {
                      state.discardUntilFinal = false;
                      state.lastTranscriptAt = Date.now();
                      updateBufferPreview();
                      setStatus('דילג על הטקסט שנמחק');
                    } else if (interimText.trim()) {
                      updateBufferPreview();
                      setStatus('הטקסט נמחק — מדלג על המקטע');
                    }
                    return;
                  }
            
                  const editor = getBestEditor();
                  if (editor) {
                    state.currentEditor = editor;
                    state.lastFocusedEditor = editor;
            
                    if (finalText.trim()) {
                      const committed = commitFinalTranscript(editor, finalText);
                      if (committed) updateBufferPreview(finalText);
                      state.lastTranscriptAt = Date.now();
                    }
            
                    if (interimText.trim()) {
                      renderInterimTranscript(editor, interimText);
                      updateBufferPreview(interimText);
                      setStatus(`מתמלל: ${normalizeTranscript(interimText).slice(0, 46)}`);
                    } else if (finalText.trim()) {
                      clearInterim({ keepVisualText: false });
                      setStatus('ממשיך להאזין...');
                    }
            
                    return;
                  }
            
                  if (finalText.trim()) {
                    appendToBuffer(finalText);
                    state.lastTranscriptAt = Date.now();
                    setStatus('נשמר בחלונית');
                  }
            
                  if (interimText.trim()) {
                    updateBufferPreview(interimText);
                    setStatus(`מתמלל ללא שדה: ${normalizeTranscript(interimText).slice(0, 38)}`);
                  }
                };
            
                recognition.onerror = (event) => {
                  const code = event.error || 'unknown';
                  const messages = {
                    'not-allowed': 'אין הרשאה למיקרופון',
                    'service-not-allowed': 'שירות התמלול נחסם',
                    'audio-capture': 'לא נמצא מיקרופון',
                    'no-speech': 'לא זוהה דיבור',
                    'network': 'שגיאת רשת בתמלול',
                    'aborted': 'התמלול הופסק'
                  };
            
                  if (code === 'not-allowed' || code === 'service-not-allowed') {
                    clearInterim({ keepVisualText: true });
                    state.suppressNextEndRestart = true;
                  }
            
                  setStatus(messages[code] || `שגיאה: ${code}`);
                };
            
                recognition.onend = () => {
                  if (state.interim.editor && !state.discardUntilFinal) {
                    clearInterim({ keepVisualText: true });
                  } else if (state.discardUntilFinal) {
                    resetInterimState();
                  }
            
                  const shouldRestart = state.listening && state.settings.continuous && !state.suppressNextEndRestart;
                  if (shouldRestart) {
                    try {
                      recognition.start();
                      return;
                    } catch (_) {}
                  }
            
                  state.listening = false;
                  state.recognition = null;
                  state.suppressNextEndRestart = false;
                  state.discardUntilFinal = false;
            
                  if (Date.now() - state.lastTranscriptAt > 1200 && ['מקשיב...', 'ממשיך להאזין...'].includes(state.statusText)) {
                    setStatus('מוכן');
                  }
            
                  updateButtonState();
                };
            
                return recognition;
              }
            
              function startListening() {
                state.currentEditor = getBestEditor();
                if (state.currentEditor) state.lastFocusedEditor = state.currentEditor;
            
                state.suppressNextEndRestart = false;
            
                try {
                  state.recognition = createRecognition();
                  state.recognition.start();
                } catch (_) {
                  setStatus('לא ניתן להפעיל תמלול כעת');
                  state.recognition = null;
                  state.listening = false;
                  updateButtonState();
                }
              }
            
              function stopListening() {
                state.suppressNextEndRestart = true;
            
                if (state.interim.editor && !interimPreviewIsIntact()) {
                  discardCurrentSpeechSegment();
                }
            
                // Do not preserve/clear the interim text here.
                // When SpeechRecognition.stop() is called, Chrome may still emit a final
                // result after this click. Keeping the interim state alive lets the final
                // result replace the live preview instead of being inserted a second time.
                // If no final result arrives, recognition.onend will preserve the current
                // interim text once.
                if (state.recognition) {
                  try { state.recognition.stop(); } catch (_) {}
                }
            
                state.listening = false;
                setStatus('נעצר');
                updateButtonState();
              }
            
              function onFocusIn(event) {
                const target = event.target;
                if (isEditableElement(target)) {
                  state.currentEditor = target;
                  state.lastFocusedEditor = target;
                  if (state.listening) setStatus('מקשיב לשדה הפעיל...');
                }
              }
            
              function onMouseDown(event) {
                const target = event.target;
                if (isEditableElement(target)) {
                  state.currentEditor = target;
                  state.lastFocusedEditor = target;
                }
              }
            
              function onSelectionChange() {
                const editor = getBestEditor();
                if (editor) {
                  state.currentEditor = editor;
                  state.lastFocusedEditor = editor;
                }
              }
            
              function onKeyDown(event) {
                if (event.altKey && event.shiftKey && !event.ctrlKey && !event.metaKey && event.code === 'KeyM') {
                  event.preventDefault();
                  resetPosition();
                }
              }
            
              function registerMenuCommands() {
                try {
                  if (typeof GM_registerMenuCommand !== 'function') return;
            
                  GM_registerMenuCommand('Voice to Text: הפעלה/עצירה', toggleListening);
            
                  GM_registerMenuCommand('Voice to Text: החלף עברית/אנגלית', () => {
                    const next = state.settings.language === 'he-IL' ? 'en-US' : 'he-IL';
                    saveSetting('language', next);
                    if (languageSelect) languageSelect.value = next;
                    const panelLanguage = shadow?.getElementById('panelLanguage');
                    if (panelLanguage) panelLanguage.value = next;
                    setStatus(`שפה: ${getLanguageLabel(next)}`);
                  });
            
                  GM_registerMenuCommand('Voice to Text: אפס מיקום כפתור', resetPosition);
                } catch (_) {}
              }
            
              function init() {
                loadSettings();
                ensureUi();
                registerMenuCommands();
            
                document.addEventListener('focusin', onFocusIn, true);
                document.addEventListener('mousedown', onMouseDown, true);
                document.addEventListener('selectionchange', onSelectionChange, true);
                document.addEventListener('keydown', onKeyDown, true);
                window.addEventListener('resize', () => requestAnimationFrame(applyPosition), { passive: true });
            
                setStatus(supportsSpeechRecognition() ? 'מוכן' : 'לא נתמך בדפדפן הזה');
                updateButtonState();
              }
            
              function waitForBodyThenInit() {
                if (document.body && document.documentElement) {
                  init();
                  return;
                }
            
                const timer = window.setInterval(() => {
                  if (document.body && document.documentElement) {
                    window.clearInterval(timer);
                    init();
                  }
                }, 250);
              }
            
              waitForBodyThenInit();
            })();
            
            
            אפשרות 2 - תוסף Chrome רגיל

            יש גם גרסה כתוסף Google Chrome, שעובדת אותו דבר כמו הסקריפט, רק בלי צורך ב־Tampermonkey.

            התקנה:

            מורידים את תיקיית התוסף.
            פותחים בכרום:
            chrome://extensions/

            מפעילים Developer mode.
            לוחצים על Load unpacked.
            בוחרים את תיקיית התוסף.
            מרעננים את האתרים הפתוחים.

            קובץ ה־ZIP לתוסף:
            universal-voice-to-text-chrome-extension-v2.6.0.zip

            לא עובד בדפי מערכת של כרום כמו chrome://extensions.
            איכות התמלול תלויה במיקרופון וברעש סביבתי.

            מי שרוצה לבדוק, לשפר או להעיר על באגים - בשמחה.

            ט מנותק
            ט מנותק
            טופ שבמתמחים
            כתב נערך לאחרונה על ידי
            #16

            @אברהם-גלסר עובד בנטפרי?

            אברהם גלסרא א 2 תגובות תגובה אחרונה
            0
            • ט טופ שבמתמחים

              @אברהם-גלסר עובד בנטפרי?

              אברהם גלסרא מחובר
              אברהם גלסרא מחובר
              אברהם גלסר
              כתב נערך לאחרונה על ידי
              #17

              @טופ-שבמתמחים לא יודע, אמור.

              תגובה 1 תגובה אחרונה
              0
              • מייבין במקצתמ מנותק
                מייבין במקצתמ מנותק
                מייבין במקצת
                כתב נערך לאחרונה על ידי
                #18

                @אברהם-גלסר יפה מאוד!
                אולי כדאי בשביל הסקריפט לעשות התקנה בלחיצה באמצעות המדריך הזה

                אברהם גלסרא תגובה 1 תגובה אחרונה
                1
                • ט טופ שבמתמחים

                  @אברהם-גלסר עובד בנטפרי?

                  א מנותק
                  א מנותק
                  אהרן
                  כתב נערך לאחרונה על ידי
                  #19

                  @טופ-שבמתמחים לי לא עובד, כנראה בגלל נטפרי.

                  ט ע"ה דכו"עע המלאךה 3 תגובות תגובה אחרונה
                  1
                  • א אהרן

                    @טופ-שבמתמחים לי לא עובד, כנראה בגלל נטפרי.

                    ט מנותק
                    ט מנותק
                    טופ שבמתמחים
                    כתב נערך לאחרונה על ידי
                    #20

                    @אהרן עוד משהו טוב שהם חסמו סתם?!

                    תגובה 1 תגובה אחרונה
                    0
                    • א אהרן

                      @טופ-שבמתמחים לי לא עובד, כנראה בגלל נטפרי.

                      ע"ה דכו"עע מנותק
                      ע"ה דכו"עע מנותק
                      ע"ה דכו"ע
                      כתב נערך לאחרונה על ידי
                      #21

                      @אהרן כתב בשיתוף | תוסף וסקריפט לתמלול דיבור לטקסט בכל אתר - ללא API:

                      @טופ-שבמתמחים לי לא עובד, כנראה בגלל נטפרי.

                      לא כל כך הגיוני שזה בגלל נטפרי, זה אמור לעבוד גם באופליין.

                      לפני שאתה עושה הקלטת תעבורה אין על מה לדבר

                      תגובה 1 תגובה אחרונה
                      1
                      • א אהרן

                        @טופ-שבמתמחים לי לא עובד, כנראה בגלל נטפרי.

                        המלאךה מנותק
                        המלאךה מנותק
                        המלאך
                        כתב נערך לאחרונה על ידי המלאך
                        #22

                        @אהרן לא קשור לנטפרי.
                        זהה רכיב מובנה בדפדפן בשם window.webkitSpeechRecognition.
                        קשה לי להאמין שנטפרי יכולים לבטל אותו.
                        הם שולטים ברמה כזו במחשב? יכולים למנוע מתוכנות לעבוד?

                        @אברהם-גלסר
                        😄

                        Avraham + ChatGPT
                        

                        חשבתי שהקרדיט רק לו? ולא קשור אליך? או שהפרומפט כן נחשב עבודה..?😉

                        אברהם גלסרא א 2 תגובות תגובה אחרונה
                        0
                        • מייבין במקצתמ מייבין במקצת

                          @אברהם-גלסר יפה מאוד!
                          אולי כדאי בשביל הסקריפט לעשות התקנה בלחיצה באמצעות המדריך הזה

                          אברהם גלסרא מחובר
                          אברהם גלסרא מחובר
                          אברהם גלסר
                          כתב נערך לאחרונה על ידי
                          #23

                          @מייבין-במקצת תודה, אני מעדכן בפוסט הראשי.

                          ט תגובה 1 תגובה אחרונה
                          1
                          • אברהם גלסרא אברהם גלסר

                            @מייבין-במקצת תודה, אני מעדכן בפוסט הראשי.

                            ט מנותק
                            ט מנותק
                            טופ שבמתמחים
                            כתב נערך לאחרונה על ידי
                            #24

                            @אברהם-גלסר עד כמה האיכות של התמלול?

                            אברהם גלסרא תגובה 1 תגובה אחרונה
                            0
                            • ט טופ שבמתמחים

                              @אברהם-גלסר עד כמה האיכות של התמלול?

                              אברהם גלסרא מחובר
                              אברהם גלסרא מחובר
                              אברהם גלסר
                              כתב נערך לאחרונה על ידי
                              #25

                              @טופ-שבמתמחים הכי טובה שאני מכיר... של גוגל

                              ט תגובה 1 תגובה אחרונה
                              0
                              • אברהם גלסרא אברהם גלסר

                                @טופ-שבמתמחים הכי טובה שאני מכיר... של גוגל

                                ט מנותק
                                ט מנותק
                                טופ שבמתמחים
                                כתב נערך לאחרונה על ידי
                                #26

                                @אברהם-גלסר זה יעבוד אופליין?

                                אברהם גלסרא תגובה 1 תגובה אחרונה
                                0
                                • ט טופ שבמתמחים

                                  @אברהם-גלסר זה יעבוד אופליין?

                                  אברהם גלסרא מחובר
                                  אברהם גלסרא מחובר
                                  אברהם גלסר
                                  כתב נערך לאחרונה על ידי
                                  #27

                                  @טופ-שבמתמחים לא.

                                  תגובה 1 תגובה אחרונה
                                  1
                                  • המלאךה המלאך

                                    @אהרן לא קשור לנטפרי.
                                    זהה רכיב מובנה בדפדפן בשם window.webkitSpeechRecognition.
                                    קשה לי להאמין שנטפרי יכולים לבטל אותו.
                                    הם שולטים ברמה כזו במחשב? יכולים למנוע מתוכנות לעבוד?

                                    @אברהם-גלסר
                                    😄

                                    Avraham + ChatGPT
                                    

                                    חשבתי שהקרדיט רק לו? ולא קשור אליך? או שהפרומפט כן נחשב עבודה..?😉

                                    אברהם גלסרא מחובר
                                    אברהם גלסרא מחובר
                                    אברהם גלסר
                                    כתב נערך לאחרונה על ידי
                                    #28

                                    @המלאך שלנו ביחד. הרעיון ושיפוץ קצת שלי אבל הוא כתב את העיקר (רובו ככולו).

                                    תגובה 1 תגובה אחרונה
                                    1
                                    • המלאךה המלאך

                                      @אהרן לא קשור לנטפרי.
                                      זהה רכיב מובנה בדפדפן בשם window.webkitSpeechRecognition.
                                      קשה לי להאמין שנטפרי יכולים לבטל אותו.
                                      הם שולטים ברמה כזו במחשב? יכולים למנוע מתוכנות לעבוד?

                                      @אברהם-גלסר
                                      😄

                                      Avraham + ChatGPT
                                      

                                      חשבתי שהקרדיט רק לו? ולא קשור אליך? או שהפרומפט כן נחשב עבודה..?😉

                                      א מנותק
                                      א מנותק
                                      אהרן
                                      כתב נערך לאחרונה על ידי
                                      #29

                                      @המלאך אשמח לשמוע שיש דרך להפעיל את זה בנטפרי. זה מה שיצא בהקלטת תעבורה: [גמיני טען שזה בגלל נטפרי]
                                      ff4b1c3b-fffc-4941-be3f-5cd03d63355a-image.png

                                      תשובת גמיני:
                                      כן, הבעיה אכן קשורה באופן ישיר לסינון של נטפרי.

                                      להלן הסבר מפורט על מה שקורה ברקע, מדוע זה נחסם וכיצד ניתן להתמודד עם זה:

                                      1. מהם האתרים/כתובות ה-IP האלו?

                                      כתובת ה-IP שמופיעה בצילום המסך שלך (142.251.153.119) שייכת ל-Google.

                                      בסקריפט שצירפת נעשה שימוש בפונקציית התמלול המובנית של הדפדפן:

                                      window.SpeechRecognition || window.webkitSpeechRecognition

                                      בדפדפני כרום (Chrome) ודפדפנים מבוססי כרומיום, כאשר משתמשים במנגנון זה, הדפדפן
                                      אינו מבצע את פענוח הדיבור לטקסט באופן מקומי על המחשב שלך. במקום זאת, הדפדפן
                                      מקליט את השמע מהמיקרופון ושולח אותו בזמן אמת לשרתים של גוגל (בפורט 443 המוצג
                                      בצילום) כדי ששרתי הבינה המלאכותית של גוגל יתרגמו את הקול למילים ויחזירו אותם
                                      לדפדפן כטקסט.

                                      1. מדוע נטפרי חוסמת זאת?

                                      סימני האזהרה האדומים (⚠️) בעמוד "הקלטת תעבורה" מעידים שנטפרי חסמה את התקשורת הזו
                                      באופן יזום. נטפרי חוסמת את הגישה לשרתי התמלול והקול הלא-מבוקרים של גוגל. מכיוון
                                      שמדובר בהזרמת שמע מוצפנת בזמן אמת (encrypted audio stream), מערכת הסינון
                                      האוטומטית של נטפרי אינה יכולה לבדוק או לסנן את התוכן הקולי שנשלח ומתקבל,
                                      ולכן ברירת המחדל היא חסימת הגישה לשירותים אלו.

                                      מה ניתן לעשות?

                                      1. שליחת פנייה לנטפרי: תוכל לשלוח פנייה למערכת הפניות של נטפרי ולצרף לה את
                                        הקלטת התעבורה הזו. לפעמים, עבור צרכי עבודה מוגדרים ובקשות ספציפיות, הם
                                        יכולים לאשר נקודת קצה (endpoint) מסוימת, אם כי בנושא תמלול קולי מול גוגל הם
                                        נוטים להקשות מכיוון שלא ניתן לסנן את השמע.

                                      2. שימוש בכלי תמלול מקומיים (אופליין): מאחר שהחסימה היא ברשת האינטרנט, הפתרון
                                        היעיל ביותר עבור משתמשי סינון הוא שימוש בתוכנות המבצעות תמלול על גבי המחשב
                                        עצמו ללא צורך באינטרנט. למשל, כלים מבוססי Whisper (של OpenAI) שיכולים לרוץ
                                        באופן מקומי (Local) על המעבד או כרטיס המסך של המחשב שלך יפעלו בצורה חלקה
                                        לחלוטין וללא שום תלות בסינון הרשת.

                                      המלאךה תגובה 1 תגובה אחרונה
                                      0
                                      • א אהרן

                                        @המלאך אשמח לשמוע שיש דרך להפעיל את זה בנטפרי. זה מה שיצא בהקלטת תעבורה: [גמיני טען שזה בגלל נטפרי]
                                        ff4b1c3b-fffc-4941-be3f-5cd03d63355a-image.png

                                        תשובת גמיני:
                                        כן, הבעיה אכן קשורה באופן ישיר לסינון של נטפרי.

                                        להלן הסבר מפורט על מה שקורה ברקע, מדוע זה נחסם וכיצד ניתן להתמודד עם זה:

                                        1. מהם האתרים/כתובות ה-IP האלו?

                                        כתובת ה-IP שמופיעה בצילום המסך שלך (142.251.153.119) שייכת ל-Google.

                                        בסקריפט שצירפת נעשה שימוש בפונקציית התמלול המובנית של הדפדפן:

                                        window.SpeechRecognition || window.webkitSpeechRecognition

                                        בדפדפני כרום (Chrome) ודפדפנים מבוססי כרומיום, כאשר משתמשים במנגנון זה, הדפדפן
                                        אינו מבצע את פענוח הדיבור לטקסט באופן מקומי על המחשב שלך. במקום זאת, הדפדפן
                                        מקליט את השמע מהמיקרופון ושולח אותו בזמן אמת לשרתים של גוגל (בפורט 443 המוצג
                                        בצילום) כדי ששרתי הבינה המלאכותית של גוגל יתרגמו את הקול למילים ויחזירו אותם
                                        לדפדפן כטקסט.

                                        1. מדוע נטפרי חוסמת זאת?

                                        סימני האזהרה האדומים (⚠️) בעמוד "הקלטת תעבורה" מעידים שנטפרי חסמה את התקשורת הזו
                                        באופן יזום. נטפרי חוסמת את הגישה לשרתי התמלול והקול הלא-מבוקרים של גוגל. מכיוון
                                        שמדובר בהזרמת שמע מוצפנת בזמן אמת (encrypted audio stream), מערכת הסינון
                                        האוטומטית של נטפרי אינה יכולה לבדוק או לסנן את התוכן הקולי שנשלח ומתקבל,
                                        ולכן ברירת המחדל היא חסימת הגישה לשירותים אלו.

                                        מה ניתן לעשות?

                                        1. שליחת פנייה לנטפרי: תוכל לשלוח פנייה למערכת הפניות של נטפרי ולצרף לה את
                                          הקלטת התעבורה הזו. לפעמים, עבור צרכי עבודה מוגדרים ובקשות ספציפיות, הם
                                          יכולים לאשר נקודת קצה (endpoint) מסוימת, אם כי בנושא תמלול קולי מול גוגל הם
                                          נוטים להקשות מכיוון שלא ניתן לסנן את השמע.

                                        2. שימוש בכלי תמלול מקומיים (אופליין): מאחר שהחסימה היא ברשת האינטרנט, הפתרון
                                          היעיל ביותר עבור משתמשי סינון הוא שימוש בתוכנות המבצעות תמלול על גבי המחשב
                                          עצמו ללא צורך באינטרנט. למשל, כלים מבוססי Whisper (של OpenAI) שיכולים לרוץ
                                          באופן מקומי (Local) על המעבד או כרטיס המסך של המחשב שלך יפעלו בצורה חלקה
                                          לחלוטין וללא שום תלות בסינון הרשת.

                                        המלאךה מנותק
                                        המלאךה מנותק
                                        המלאך
                                        כתב נערך לאחרונה על ידי
                                        #30

                                        @אהרן אכן.
                                        זה באמת שייך לנטפרי, זה מוזר, כי כל התכנים האלה מוזרמים באמצעות המקירופון, אז ממה החשש? שאתה תגיד מילים מסוימות וגוגל יקריאו אותם?

                                        menajemmendelM תגובה 1 תגובה אחרונה
                                        0
                                        • menajemmendelM menajemmendel התייחס לנושא זה
                                        • המלאךה המלאך

                                          @אהרן אכן.
                                          זה באמת שייך לנטפרי, זה מוזר, כי כל התכנים האלה מוזרמים באמצעות המקירופון, אז ממה החשש? שאתה תגיד מילים מסוימות וגוגל יקריאו אותם?

                                          menajemmendelM מנותק
                                          menajemmendelM מנותק
                                          menajemmendel
                                          כתב נערך לאחרונה על ידי
                                          #31

                                          @המלאך דבר זה עובד בנטפרי מצויין
                                          https://mitmachim.top/topic/97481/שיתוף-שיתוף-תוסף-לתמלול-דיבור-לטקסט

                                          תגובה 1 תגובה אחרונה
                                          1

                                          • התחברות

                                          • אין לך חשבון עדיין? הרשמה

                                          • התחברו או הירשמו כדי לחפש.
                                          • פוסט ראשון
                                            פוסט אחרון
                                          0
                                          • חוקי הפורום
                                          • פופולרי
                                          • לא נפתר
                                          • משתמשים
                                          • חיפוש גוגל בפורום
                                          • צור קשר