{"id":978,"date":"2026-03-19T14:40:37","date_gmt":"2026-03-19T14:40:37","guid":{"rendered":"https:\/\/primetoolhub.com\/?page_id=978"},"modified":"2026-07-19T10:34:51","modified_gmt":"2026-07-19T10:34:51","slug":"voice-typing-studio-free","status":"publish","type":"page","link":"https:\/\/schoolict.net\/tools\/voice-typing-studio-free\/","title":{"rendered":"Voice Typing Studio | Free Speech-to-Text &amp; Translator"},"content":{"rendered":"\n<div class=\"pth-hero-section\">\n    <div class=\"pth-hero-content\">\n        <h2>Voice Studio Pro v3.0 <\/h2>\n        <p>Professional Voice Typing Studio by Prime Tool Hub. Free online speech-to-text with custom voice commands and multi-language Google translation working securely in your browser.<\/p>\n\n        <div id=\"pth-toc-placeholder\"><\/div>\n    <\/div>\n\n \n  <div class=\"pth-hero-image\">\n    <img data-no-lazy=\"1\"\n         src=\"https:\/\/schoolict.net\/tools\/wp-content\/uploads\/2026\/03\/Voice-Typing-Studio-1-800x447.jpeg\"\n         width=\"800\"\n         height=\"447\"\n         alt=\"Voice Typing Studio\"\n         fetchpriority=\"high\"\n         loading=\"eager\"\n         decoding=\"async\"\n         style=\"width:100%; height:auto; display:block;\">\n  <\/div>\n<\/div>\n\n\n\n\n<div class=\"wp-block-rank-math-toc-block\" id=\"rank-math-toc\"><h2>Table of Contents<\/h2><nav><ul><li><a href=\"#\ud83d\udd34-four-tabs-one-workflow\">\ud83d\udd34 Four Tabs, One Workflow<\/a><ul><li><a href=\"#dictate-sixteen-languages-and-it-learns-your-words\">Dictate \u2014 sixteen languages, and it learns your words<\/a><\/li><li><a href=\"#cleanup-what-a-transcript-actually-needs\">Cleanup \u2014 what a transcript actually needs<\/a><\/li><li><a href=\"#analytics-numbers-you-can-act-on\">Analytics \u2014 numbers you can act on<\/a><\/li><li><a href=\"#translate-on-your-device-or-not-at-all\">Translate \u2014 on your device or not at all<\/a><\/li><\/ul><\/li><li><a href=\"#\ud83d\udfe1-honest-limits\">\ud83d\udfe1 Honest Limits<\/a><ul><li><a href=\"#where-the-speech-recognition-actually-runs\">Where the speech recognition actually runs<\/a><\/li><li><a href=\"#browser-support\">Browser support<\/a><\/li><li><a href=\"#accuracy-is-not-evenly-distributed\">Accuracy is not evenly distributed<\/a><\/li><li><a href=\"#drafts-live-in-this-browser-only\">Drafts live in this browser only<\/a><\/li><\/ul><\/li><li><a href=\"#\ud83d\udfe2-going-deeper\">\ud83d\udfe2 Going Deeper<\/a><\/li><\/ul><\/nav><\/div>\n\n\n<style>\r\n\/* ==========================================================================\r\n   PRIME TOOL HUB - VOICE STUDIO PRO v3.0\r\n   All-in-One: Dictate \/ Cleanup \/ Analytics \/ On-device Translate\r\n   ========================================================================== *\/\r\n#pth-voice-studio-wrapper { font-family: 'Inter', system-ui, -apple-system, sans-serif; max-width: 1400px; margin: 0 auto; background: #f8fafc; padding: 20px; border-radius: 16px; color: #0f172a; line-height: 1.6; opacity: 1; }\r\n#pth-voice-studio-wrapper * { box-sizing: border-box; min-width: 0; }\r\n\r\n#pth-voice-studio-wrapper .pth-head { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; margin-bottom: 18px; }\r\n#pth-voice-studio-wrapper .pth-h2 { font-size: 30px; font-weight: 800; color: #1d4ed8; margin: 0 0 8px 0; letter-spacing: -0.5px; }\r\n#pth-voice-studio-wrapper .pth-sub { font-size: 15px; color: #475569; margin: 0 0 10px 0; font-weight: 500; }\r\n#pth-voice-studio-wrapper .pth-badges { display: flex; gap: 8px; flex-wrap: wrap; }\r\n#pth-voice-studio-wrapper .pth-version-badge { background: #dbeafe; color: #1e40af; border: 1px solid #bfdbfe; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }\r\n#pth-voice-studio-wrapper .pth-status-offline { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; cursor: help; }\r\n#pth-voice-studio-wrapper .pth-scope-btn { background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; cursor: pointer; font-family: inherit; }\r\n#pth-voice-studio-wrapper .pth-scope-btn:hover { background: #e2e8f0; color: #0f172a; }\r\n#pth-voice-studio-wrapper .pth-scope-btn:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }\r\n#pth-voice-studio-wrapper .pth-scope-note { background: #ffffff; border: 1px solid #e2e8f0; border-left: 4px solid #16a34a; border-radius: 0 10px 10px 0; padding: 15px 18px; margin-top: 12px; max-width: 780px; }\r\n#pth-voice-studio-wrapper .pth-scope-note p { font-size: 13.5px; color: #475569; margin: 0 0 10px 0; line-height: 1.65; }\r\n#pth-voice-studio-wrapper .pth-scope-note strong { color: #0f172a; }\r\n#pth-voice-studio-wrapper .pth-scope-note a { color: #1d4ed8; font-weight: 700; text-decoration: none; }\r\n#pth-voice-studio-wrapper .pth-scope-note a:hover { text-decoration: underline; }\r\n#pth-voice-studio-wrapper .pth-head-btns { display: flex; gap: 8px; flex-wrap: wrap; }\r\n\r\n#pth-voice-studio-wrapper .pth-btn { padding: 9px 16px; border-radius: 8px; font-weight: 700; cursor: pointer; border: 1px solid #e2e8f0; font-family: inherit; font-size: 14px; transition: 0.15s; background: #ffffff; color: #334155; }\r\n#pth-voice-studio-wrapper .pth-btn:hover { background: #f1f5f9; color: #0f172a; }\r\n#pth-voice-studio-wrapper .pth-btn.primary { background: #1d4ed8; color: #ffffff; border-color: #1d4ed8; }\r\n#pth-voice-studio-wrapper .pth-btn.primary:hover { background: #1e40af; color: #ffffff; }\r\n#pth-voice-studio-wrapper .pth-btn.rec { background: #b91c1c; color: #ffffff; border-color: #b91c1c; font-size: 15px; padding: 11px 22px; }\r\n#pth-voice-studio-wrapper .pth-btn.rec:hover { background: #991b1b; color: #ffffff; }\r\n#pth-voice-studio-wrapper .pth-btn.rec.live { background: #047857; border-color: #047857; }\r\n#pth-voice-studio-wrapper .pth-btn.rec.live:hover { background: #065f46; }\r\n#pth-voice-studio-wrapper .pth-btn.danger { background: #fee2e2; color: #991b1b; border-color: #fecaca; }\r\n#pth-voice-studio-wrapper .pth-btn.danger:hover { background: #fecaca; color: #7f1d1d; }\r\n#pth-voice-studio-wrapper .pth-btn:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }\r\n#pth-voice-studio-wrapper .pth-btn[disabled] { opacity: 0.55; cursor: not-allowed; }\r\n\r\n\/* Tabs *\/\r\n#pth-voice-studio-wrapper .pth-tabbar { display: flex; gap: 6px; flex-wrap: wrap; border-bottom: 2px solid #e2e8f0; margin-bottom: 20px; }\r\n#pth-voice-studio-wrapper .pth-tab { padding: 11px 18px; border: none; background: transparent; font-family: inherit; font-size: 14px; font-weight: 700; color: #64748b; cursor: pointer; border-bottom: 3px solid transparent; margin-bottom: -2px; transition: 0.15s; }\r\n#pth-voice-studio-wrapper .pth-tab:hover { color: #1d4ed8; }\r\n#pth-voice-studio-wrapper .pth-tab.active { color: #1d4ed8; border-bottom-color: #1d4ed8; }\r\n#pth-voice-studio-wrapper .pth-tab:focus-visible { outline: 3px solid #93c5fd; outline-offset: -3px; }\r\n#pth-voice-studio-wrapper .pth-panel { display: none; }\r\n#pth-voice-studio-wrapper .pth-panel.active { display: block; }\r\n\r\n\/* Grid *\/\r\n#pth-voice-studio-wrapper .pth-grid { display: grid; grid-template-columns: 1.8fr 1fr; gap: 22px; align-items: start; transition: 0.25s ease; }\r\n#pth-voice-studio-wrapper .pth-grid.pth-full-width { grid-template-columns: 1fr; }\r\n#pth-voice-studio-wrapper .pth-card { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 22px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }\r\n\r\n\/* Controls *\/\r\n#pth-voice-studio-wrapper .pth-ctrlbar { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid #f1f5f9; }\r\n#pth-voice-studio-wrapper .pth-field { display: flex; flex-direction: column; gap: 6px; }\r\n#pth-voice-studio-wrapper .pth-label { font-size: 13px; font-weight: 700; color: #334155; }\r\n#pth-voice-studio-wrapper .pth-select, #pth-voice-studio-wrapper .pth-textbox { padding: 9px 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-family: inherit; font-size: 14px; color: #0f172a; background: #ffffff; }\r\n#pth-voice-studio-wrapper .pth-select:focus, #pth-voice-studio-wrapper .pth-textbox:focus { outline: 3px solid #93c5fd; outline-offset: 1px; border-color: #1d4ed8; }\r\n#pth-voice-studio-wrapper .pth-checkrow { display: flex; align-items: center; gap: 9px; padding: 6px 0; font-size: 13.5px; color: #334155; font-weight: 600; }\r\n#pth-voice-studio-wrapper .pth-checkrow input { width: 16px; height: 16px; accent-color: #1d4ed8; cursor: pointer; }\r\n\r\n#pth-voice-studio-wrapper .pth-io { margin-bottom: 16px; }\r\n#pth-voice-studio-wrapper .pth-io-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 7px; }\r\n#pth-voice-studio-wrapper .pth-textarea { width: 100%; min-height: 240px; padding: 14px; border: 1px solid #cbd5e1; border-radius: 8px; font-family: inherit; font-size: 15px; line-height: 1.75; color: #0f172a; background: #ffffff; resize: vertical; }\r\n#pth-voice-studio-wrapper .pth-textarea:focus { outline: 3px solid #93c5fd; outline-offset: 1px; border-color: #1d4ed8; }\r\n#pth-voice-studio-wrapper .pth-textarea.output { background: #f8fafc; color: #1e293b; }\r\n#pth-voice-studio-wrapper .pth-stat { background: #f1f5f9; color: #475569; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }\r\n\r\n\/* Live status strip *\/\r\n#pth-voice-studio-wrapper .pth-livebar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 11px 14px; border-radius: 8px; background: #f1f5f9; border: 1px solid #e2e8f0; margin-bottom: 14px; min-height: 46px; }\r\n#pth-voice-studio-wrapper .pth-livebar.live { background: #f0fdf4; border-color: #bbf7d0; }\r\n#pth-voice-studio-wrapper .pth-dot { width: 11px; height: 11px; border-radius: 50%; background: #94a3b8; flex-shrink: 0; }\r\n#pth-voice-studio-wrapper .pth-livebar.live .pth-dot { background: #16a34a; }\r\n#pth-voice-studio-wrapper .pth-livetext { font-size: 13.5px; color: #475569; font-weight: 600; flex: 1; min-width: 140px; }\r\n#pth-voice-studio-wrapper .pth-livebar.live .pth-livetext { color: #166534; }\r\n#pth-voice-studio-wrapper .pth-interim { font-size: 13.5px; color: #64748b; font-style: italic; }\r\n#pth-voice-studio-wrapper .pth-timer { font-family: 'Consolas', monospace; font-size: 14px; font-weight: 800; color: #334155; }\r\n\r\n\/* Stat grid *\/\r\n#pth-voice-studio-wrapper .pth-statgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 18px; }\r\n#pth-voice-studio-wrapper .pth-statgrid.three { grid-template-columns: repeat(3, 1fr); }\r\n#pth-voice-studio-wrapper .pth-statbox { border: 1px solid #e2e8f0; border-radius: 8px; padding: 14px; text-align: center; background: #f8fafc; }\r\n#pth-voice-studio-wrapper .pth-statbox .n { font-size: 23px; font-weight: 800; color: #0f172a; font-family: 'Consolas', monospace; }\r\n#pth-voice-studio-wrapper .pth-statbox .t { font-size: 11.5px; color: #64748b; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px; }\r\n#pth-voice-studio-wrapper .pth-statbox.good { background: #f0fdf4; border-color: #bbf7d0; }\r\n#pth-voice-studio-wrapper .pth-statbox.good .n { color: #166534; }\r\n#pth-voice-studio-wrapper .pth-statbox.warn { background: #fffbeb; border-color: #fde68a; }\r\n#pth-voice-studio-wrapper .pth-statbox.warn .n { color: #92400e; }\r\n\r\n#pth-voice-studio-wrapper .pth-note { padding: 12px 15px; border-radius: 8px; font-size: 13.5px; margin-bottom: 16px; line-height: 1.6; }\r\n#pth-voice-studio-wrapper .pth-note.info { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e40af; }\r\n#pth-voice-studio-wrapper .pth-note.ok { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }\r\n#pth-voice-studio-wrapper .pth-note.warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }\r\n#pth-voice-studio-wrapper .pth-note.err { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }\r\n\r\n#pth-voice-studio-wrapper .pth-findings { margin-bottom: 16px; }\r\n#pth-voice-studio-wrapper .pth-finding { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 9px 13px; border-radius: 8px; margin-bottom: 7px; font-size: 13.5px; background: #f8fafc; border: 1px solid #e2e8f0; color: #334155; }\r\n#pth-voice-studio-wrapper .pth-finding .c { font-weight: 800; font-family: 'Consolas', monospace; color: #1d4ed8; }\r\n\r\n#pth-voice-studio-wrapper .pth-wordlist { display: flex; flex-wrap: wrap; gap: 7px; }\r\n#pth-voice-studio-wrapper .pth-wordchip { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e40af; padding: 5px 11px; border-radius: 999px; font-size: 12.5px; font-weight: 700; }\r\n\r\n\/* Sidebar *\/\r\n#pth-voice-studio-wrapper .pth-side h3 { font-size: 15px; font-weight: 800; color: #1e293b; margin: 0 0 9px 0; }\r\n#pth-voice-studio-wrapper .pth-side h3.spaced { margin-top: 22px; }\r\n#pth-voice-studio-wrapper .pth-side p { font-size: 13.5px; color: #475569; margin: 0 0 12px 0; }\r\n#pth-voice-studio-wrapper .pth-side ul { margin: 0 0 12px 0; padding-left: 18px; }\r\n#pth-voice-studio-wrapper .pth-side li { font-size: 13.5px; color: #475569; margin-bottom: 6px; }\r\n#pth-voice-studio-wrapper .pth-side a { color: #1d4ed8; font-weight: 700; text-decoration: none; }\r\n#pth-voice-studio-wrapper .pth-side a:hover { text-decoration: underline; }\r\n#pth-voice-studio-wrapper code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: 'Consolas', monospace; font-size: 12.5px; color: #1e293b; }\r\n#pth-voice-studio-wrapper .pth-tip { background: #eff6ff; border: 1px solid #bfdbfe; padding: 13px; border-radius: 8px; font-size: 13px; color: #1e40af; line-height: 1.6; }\r\n#pth-voice-studio-wrapper .pth-cmdrow { display: grid; grid-template-columns: 100px 1fr; gap: 8px; align-items: center; margin-bottom: 8px; }\r\n#pth-voice-studio-wrapper .pth-cmdrow label { font-size: 12.5px; font-weight: 700; color: #334155; }\r\n#pth-voice-studio-wrapper .pth-cmdrow input { padding: 7px 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-family: inherit; font-size: 12.5px; width: 100%; }\r\n\r\n\/* Toast *\/\r\n#pth-voice-studio-wrapper .pth-toast { position: fixed; bottom: 24px; right: 24px; background: #0f172a; color: #ffffff; padding: 13px 22px; border-radius: 10px; font-size: 14px; font-weight: 700; box-shadow: 0 10px 25px rgba(0,0,0,0.2); z-index: 99999; display: none; max-width: 320px; }\r\n#pth-voice-studio-wrapper .pth-toast.err { background: #991b1b; }\r\n\r\n@media (max-width: 900px) {\r\n    #pth-voice-studio-wrapper { padding: 14px; }\r\n    #pth-voice-studio-wrapper .pth-grid { grid-template-columns: 1fr; }\r\n    #pth-voice-studio-wrapper .pth-h2 { font-size: 24px; }\r\n    #pth-voice-studio-wrapper .pth-tab { padding: 10px 12px; font-size: 13px; }\r\n    #pth-voice-studio-wrapper .pth-statgrid, #pth-voice-studio-wrapper .pth-statgrid.three { grid-template-columns: repeat(2, 1fr); }\r\n}\r\n<\/style>\r\n\r\n<div id=\"pth-voice-studio-wrapper\">\r\n\r\n    <div id=\"pth-toast-voice\" class=\"pth-toast\" role=\"status\" aria-live=\"polite\">Done<\/div>\r\n\r\n    <div class=\"pth-head\">\r\n        <div>\r\n            <h2 class=\"pth-h2\">\ud83c\udf99\ufe0f Voice Studio Pro<\/h2>\r\n            <p class=\"pth-sub\">Dictate with your voice, clean up the transcript, measure how you speak, and translate \u2014 all on your own device.<\/p>\r\n            <div class=\"pth-badges\">\r\n                <span class=\"pth-version-badge\">v3.0<\/span>\r\n                <span class=\"pth-status-offline\" title=\"All text handling, cleanup, analytics and translation run in your browser. Live speech recognition is provided by your browser's own service.\">\ud83d\udd12 100% Offline<\/span>\r\n                <button type=\"button\" class=\"pth-scope-btn\" id=\"pth-scope-btn\" aria-expanded=\"false\" aria-controls=\"pth-scope-note\">\u24d8 What that covers<\/button>\r\n            <\/div>\r\n            <div class=\"pth-scope-note\" id=\"pth-scope-note\" style=\"display:none;\">\r\n                <p><strong>Everything this page does with your text stays on your device.<\/strong> Cleanup, analytics, your vocabulary rules, saved drafts and translation all run in your browser, and none of them send anything anywhere.<\/p>\r\n                <p><strong>Live dictation is the one exception, and it is not ours to control.<\/strong> The microphone is handled by your browser's built-in speech recognition, and most browsers process that audio through their own service \u2014 Chrome through Google's, Safari through Apple's. Every dictation website is built on this same browser feature, and no web page can change where the audio goes.<\/p>\r\n                <p style=\"margin-bottom:0;\">For confidential audio, record it first and transcribe with the <a href=\"https:\/\/schoolict.net\/tools\/ai-offline-transcription\/\">AI Offline Transcription<\/a> tool, which runs a speech model inside your browser so the audio never leaves your machine. Bring the finished text back here for cleanup and analysis.<\/p>\r\n            <\/div>\r\n        <\/div>\r\n        <div class=\"pth-head-btns\">\r\n            <button type=\"button\" class=\"pth-btn\" id=\"pth-vs-sample\">\ud83d\udca1 Load Sample<\/button>\r\n            <button type=\"button\" class=\"pth-btn\" id=\"pth-vs-fw\">\u26f6 Full Width<\/button>\r\n            <button type=\"button\" class=\"pth-btn danger\" id=\"pth-vs-clear\">\ud83d\uddd1\ufe0f Clear<\/button>\r\n        <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"pth-tabbar\" role=\"tablist\">\r\n        <button type=\"button\" class=\"pth-tab active\" data-pth-panel=\"dictate\" role=\"tab\">\ud83c\udf99\ufe0f Dictate<\/button>\r\n        <button type=\"button\" class=\"pth-tab\" data-pth-panel=\"cleanup\" role=\"tab\">\ud83e\uddf9 Cleanup<\/button>\r\n        <button type=\"button\" class=\"pth-tab\" data-pth-panel=\"stats\" role=\"tab\">\ud83d\udcca Analytics<\/button>\r\n        <button type=\"button\" class=\"pth-tab\" data-pth-panel=\"translate\" role=\"tab\">\ud83c\udf10 Translate<\/button>\r\n    <\/div>\r\n\r\n    <div class=\"pth-grid\" id=\"pth-vs-grid\">\r\n\r\n        <div>\r\n            <!-- ============ PANEL 1: DICTATE ============ -->\r\n            <div class=\"pth-panel active\" id=\"pth-panel-dictate\" role=\"tabpanel\">\r\n                <div class=\"pth-card\">\r\n                    <div id=\"pth-mic-support\" class=\"pth-note info\" style=\"display:none;\"><\/div>\r\n\r\n                    <div class=\"pth-note info\" style=\"margin-bottom:16px;\">\r\n                        <strong>\ud83d\udd12 Where things run:<\/strong> your text, the cleanup, the analytics and the translation all stay in this browser. The microphone itself is handled by your browser's speech recognition, which in Chrome and Safari processes the audio on their servers. For confidential recordings, use <a href=\"https:\/\/schoolict.net\/tools\/ai-offline-transcription\/\" style=\"color:#1e40af; font-weight:700;\">AI Offline Transcription<\/a> instead, where the model runs on your machine.\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-ctrlbar\">\r\n                        <div class=\"pth-field\" style=\"min-width:210px;\">\r\n                            <label class=\"pth-label\" for=\"pth-dict-lang\">Dictation language<\/label>\r\n                            <select id=\"pth-dict-lang\" class=\"pth-select\" aria-label=\"Dictation language\">\r\n                                <option value=\"en-US\">\ud83c\uddfa\ud83c\uddf8 English (US)<\/option>\r\n                                <option value=\"en-GB\">\ud83c\uddec\ud83c\udde7 English (UK)<\/option>\r\n                                <option value=\"en-IN\">\ud83c\uddee\ud83c\uddf3 English (India)<\/option>\r\n                                <option value=\"si-LK\">\ud83c\uddf1\ud83c\uddf0 \u0dc3\u0dd2\u0d82\u0dc4\u0dbd \u2014 Sinhala<\/option>\r\n                                <option value=\"ta-LK\">\ud83c\uddf1\ud83c\uddf0 \u0ba4\u0bae\u0bbf\u0bb4\u0bcd \u2014 Tamil<\/option>\r\n                                <option value=\"hi-IN\">\ud83c\uddee\ud83c\uddf3 \u0939\u093f\u0928\u094d\u0926\u0940 \u2014 Hindi<\/option>\r\n                                <option value=\"es-ES\">\ud83c\uddea\ud83c\uddf8 Espa\u00f1ol<\/option>\r\n                                <option value=\"fr-FR\">\ud83c\uddeb\ud83c\uddf7 Fran\u00e7ais<\/option>\r\n                                <option value=\"de-DE\">\ud83c\udde9\ud83c\uddea Deutsch<\/option>\r\n                                <option value=\"it-IT\">\ud83c\uddee\ud83c\uddf9 Italiano<\/option>\r\n                                <option value=\"pt-BR\">\ud83c\udde7\ud83c\uddf7 Portugu\u00eas<\/option>\r\n                                <option value=\"ar-SA\">\ud83c\uddf8\ud83c\udde6 \u0627\u0644\u0639\u0631\u0628\u064a\u0629<\/option>\r\n                                <option value=\"ru-RU\">\ud83c\uddf7\ud83c\uddfa \u0420\u0443\u0441\u0441\u043a\u0438\u0439<\/option>\r\n                                <option value=\"ja-JP\">\ud83c\uddef\ud83c\uddf5 \u65e5\u672c\u8a9e<\/option>\r\n                                <option value=\"ko-KR\">\ud83c\uddf0\ud83c\uddf7 \ud55c\uad6d\uc5b4<\/option>\r\n                                <option value=\"zh-CN\">\ud83c\udde8\ud83c\uddf3 \u7b80\u4f53\u4e2d\u6587<\/option>\r\n                            <\/select>\r\n                        <\/div>\r\n                        <button type=\"button\" class=\"pth-btn rec\" id=\"pth-rec-btn\">\ud83c\udf99\ufe0f Start Dictation<\/button>\r\n                        <button type=\"button\" class=\"pth-btn\" id=\"pth-undo-btn\">\u21b6 Undo last<\/button>\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-livebar\" id=\"pth-livebar\">\r\n                        <span class=\"pth-dot\" id=\"pth-dot\"><\/span>\r\n                        <span class=\"pth-livetext\" id=\"pth-livetext\">Idle \u2014 press Start Dictation and begin speaking<\/span>\r\n                        <span class=\"pth-interim\" id=\"pth-interim\"><\/span>\r\n                        <span class=\"pth-timer\" id=\"pth-timer\">00:00<\/span>\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-io\">\r\n                        <div class=\"pth-io-head\">\r\n                            <label class=\"pth-label\" for=\"pth-editor\">Your text<\/label>\r\n                            <div style=\"display:flex; gap:8px; align-items:center; flex-wrap:wrap;\">\r\n                                <span class=\"pth-stat\" id=\"pth-wc\">0 words<\/span>\r\n                                <button type=\"button\" class=\"pth-btn\" id=\"pth-copy-btn\">\ud83d\udccb Copy<\/button>\r\n                                <button type=\"button\" class=\"pth-btn\" id=\"pth-export-btn\">\ud83d\udcbe Export .txt<\/button>\r\n                            <\/div>\r\n                        <\/div>\r\n                        <textarea id=\"pth-editor\" class=\"pth-textarea\" placeholder=\"Pick a language, press Start Dictation and speak. You can also type here directly.\" aria-label=\"Dictated text\"><\/textarea>\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-opt-punct\" checked><label for=\"pth-opt-punct\">Add punctuation automatically after a pause<\/label><\/div>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-opt-cap\" checked><label for=\"pth-opt-cap\">Capitalise the first word of each sentence<\/label><\/div>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-opt-vocab\" checked><label for=\"pth-opt-vocab\">Apply my custom vocabulary corrections<\/label><\/div>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-opt-autosave\" checked><label for=\"pth-opt-autosave\">Keep a local draft in this browser<\/label><\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- ============ PANEL 2: CLEANUP ============ -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-cleanup\" role=\"tabpanel\">\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-io\">\r\n                        <div class=\"pth-io-head\">\r\n                            <label class=\"pth-label\" for=\"pth-clean-in\">Text to clean<\/label>\r\n                            <button type=\"button\" class=\"pth-btn\" id=\"pth-pull-btn\">\u2b07 Pull from Dictate tab<\/button>\r\n                        <\/div>\r\n                        <textarea id=\"pth-clean-in\" class=\"pth-textarea\" style=\"min-height:150px;\" placeholder=\"Paste a transcript, or pull the text you just dictated.\" aria-label=\"Text to clean\"><\/textarea>\r\n                    <\/div>\r\n\r\n                    <h3 style=\"font-size:15px; font-weight:800; color:#1e293b; margin:0 0 10px 0;\">What we found<\/h3>\r\n                    <div class=\"pth-findings\" id=\"pth-clean-findings\"><\/div>\r\n\r\n                    <h3 style=\"font-size:15px; font-weight:800; color:#1e293b; margin:18px 0 6px 0;\">Fixes to apply<\/h3>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-fix-filler\" checked><label for=\"pth-fix-filler\">Remove filler words (um, uh, you know, basically)<\/label><\/div>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-fix-repeat\" checked><label for=\"pth-fix-repeat\">Collapse accidental word repeats (the the \u2192 the)<\/label><\/div>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-fix-space\" checked><label for=\"pth-fix-space\">Tidy double spaces and spacing before punctuation<\/label><\/div>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-fix-sentcap\" checked><label for=\"pth-fix-sentcap\">Capitalise sentence starts and the word I<\/label><\/div>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-fix-para\"><label for=\"pth-fix-para\">Break into paragraphs every few sentences<\/label><\/div>\r\n                    <div class=\"pth-checkrow\"><input type=\"checkbox\" id=\"pth-fix-stops\"><label for=\"pth-fix-stops\">Add a full stop to lines that end without one<\/label><\/div>\r\n\r\n                    <div style=\"display:flex; gap:9px; flex-wrap:wrap; margin:16px 0;\">\r\n                        <button type=\"button\" class=\"pth-btn primary\" id=\"pth-clean-btn\">\ud83e\uddf9 Clean Text<\/button>\r\n                        <button type=\"button\" class=\"pth-btn\" id=\"pth-clean-copy\">\ud83d\udccb Copy cleaned<\/button>\r\n                        <button type=\"button\" class=\"pth-btn\" id=\"pth-clean-push\">\u2b06 Send back to editor<\/button>\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-io\" style=\"margin-bottom:0;\">\r\n                        <div class=\"pth-io-head\"><label class=\"pth-label\" for=\"pth-clean-out\">Cleaned result<\/label><\/div>\r\n                        <textarea id=\"pth-clean-out\" class=\"pth-textarea output\" style=\"min-height:150px;\" readonly placeholder=\"Cleaned text appears here...\" aria-label=\"Cleaned result\"><\/textarea>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- ============ PANEL 3: ANALYTICS ============ -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-stats\" role=\"tabpanel\">\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-ctrlbar\">\r\n                        <button type=\"button\" class=\"pth-btn primary\" id=\"pth-stats-btn\">\ud83d\udcca Analyse my text<\/button>\r\n                        <div class=\"pth-field\" style=\"width:170px;\">\r\n                            <label class=\"pth-label\" for=\"pth-speak-secs\">Speaking time (seconds)<\/label>\r\n                            <input type=\"number\" id=\"pth-speak-secs\" class=\"pth-textbox\" value=\"0\" min=\"0\" aria-label=\"Speaking time in seconds\">\r\n                        <\/div>\r\n                        <span class=\"pth-stat\" id=\"pth-stats-src\">from your last session<\/span>\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-statgrid\">\r\n                        <div class=\"pth-statbox\"><div class=\"n\" id=\"pth-st-words\">0<\/div><div class=\"t\">Words<\/div><\/div>\r\n                        <div class=\"pth-statbox\"><div class=\"n\" id=\"pth-st-sent\">0<\/div><div class=\"t\">Sentences<\/div><\/div>\r\n                        <div class=\"pth-statbox\" id=\"pth-box-wpm\"><div class=\"n\" id=\"pth-st-wpm\">\u2014<\/div><div class=\"t\">Words \/ min<\/div><\/div>\r\n                        <div class=\"pth-statbox\" id=\"pth-box-filler\"><div class=\"n\" id=\"pth-st-filler\">0<\/div><div class=\"t\">Filler words<\/div><\/div>\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-statgrid three\">\r\n                        <div class=\"pth-statbox\"><div class=\"n\" id=\"pth-st-avg\">0<\/div><div class=\"t\">Words \/ sentence<\/div><\/div>\r\n                        <div class=\"pth-statbox\"><div class=\"n\" id=\"pth-st-read\">\u2014<\/div><div class=\"t\">Reading ease<\/div><\/div>\r\n                        <div class=\"pth-statbox\"><div class=\"n\" id=\"pth-st-grade\">\u2014<\/div><div class=\"t\">Grade level<\/div><\/div>\r\n                    <\/div>\r\n\r\n                    <div id=\"pth-pace-note\" class=\"pth-note info\">Dictate something, or paste text into the Cleanup tab, then press Analyse.<\/div>\r\n\r\n                    <h3 style=\"font-size:15px; font-weight:800; color:#1e293b; margin:18px 0 10px 0;\">Words you lean on most<\/h3>\r\n                    <div class=\"pth-wordlist\" id=\"pth-topwords\"><span style=\"font-size:13px; color:#94a3b8;\">Nothing analysed yet.<\/span><\/div>\r\n\r\n                    <h3 style=\"font-size:15px; font-weight:800; color:#1e293b; margin:20px 0 10px 0;\">Longest sentences<\/h3>\r\n                    <div id=\"pth-longsent\" style=\"font-size:13.5px; color:#475569;\">Nothing analysed yet.<\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- ============ PANEL 4: TRANSLATE ============ -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-translate\" role=\"tabpanel\">\r\n                <div class=\"pth-card\">\r\n                    <div id=\"pth-tr-status\" class=\"pth-note info\">Checking whether your browser can translate on-device...<\/div>\r\n\r\n                    <div class=\"pth-ctrlbar\">\r\n                        <div class=\"pth-field\" style=\"min-width:170px;\">\r\n                            <label class=\"pth-label\" for=\"pth-tr-from\">From<\/label>\r\n                            <select id=\"pth-tr-from\" class=\"pth-select\" aria-label=\"Source language\">\r\n                                <option value=\"en\">English<\/option>\r\n                                <option value=\"es\">Espa\u00f1ol<\/option>\r\n                                <option value=\"fr\">Fran\u00e7ais<\/option>\r\n                                <option value=\"de\">Deutsch<\/option>\r\n                                <option value=\"it\">Italiano<\/option>\r\n                                <option value=\"pt\">Portugu\u00eas<\/option>\r\n                                <option value=\"hi\">\u0939\u093f\u0928\u094d\u0926\u0940<\/option>\r\n                                <option value=\"ar\">\u0627\u0644\u0639\u0631\u0628\u064a\u0629<\/option>\r\n                                <option value=\"ru\">\u0420\u0443\u0441\u0441\u043a\u0438\u0439<\/option>\r\n                                <option value=\"ja\">\u65e5\u672c\u8a9e<\/option>\r\n                                <option value=\"ko\">\ud55c\uad6d\uc5b4<\/option>\r\n                                <option value=\"zh\">\u4e2d\u6587<\/option>\r\n                            <\/select>\r\n                        <\/div>\r\n                        <div class=\"pth-field\" style=\"min-width:170px;\">\r\n                            <label class=\"pth-label\" for=\"pth-tr-to\">To<\/label>\r\n                            <select id=\"pth-tr-to\" class=\"pth-select\" aria-label=\"Target language\">\r\n                                <option value=\"es\">Espa\u00f1ol<\/option>\r\n                                <option value=\"en\">English<\/option>\r\n                                <option value=\"fr\">Fran\u00e7ais<\/option>\r\n                                <option value=\"de\">Deutsch<\/option>\r\n                                <option value=\"it\">Italiano<\/option>\r\n                                <option value=\"pt\">Portugu\u00eas<\/option>\r\n                                <option value=\"hi\">\u0939\u093f\u0928\u094d\u0926\u0940<\/option>\r\n                                <option value=\"ar\">\u0627\u0644\u0639\u0631\u0628\u064a\u0629<\/option>\r\n                                <option value=\"ru\">\u0420\u0443\u0441\u0441\u043a\u0438\u0439<\/option>\r\n                                <option value=\"ja\">\u65e5\u672c\u8a9e<\/option>\r\n                                <option value=\"ko\">\ud55c\uad6d\uc5b4<\/option>\r\n                                <option value=\"zh\">\u4e2d\u6587<\/option>\r\n                            <\/select>\r\n                        <\/div>\r\n                        <button type=\"button\" class=\"pth-btn primary\" id=\"pth-tr-btn\" disabled>\ud83c\udf10 Translate<\/button>\r\n                        <button type=\"button\" class=\"pth-btn\" id=\"pth-tr-pull\">\u2b07 Pull from Dictate tab<\/button>\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-io\">\r\n                        <div class=\"pth-io-head\"><label class=\"pth-label\" for=\"pth-tr-in\">Source text<\/label><\/div>\r\n                        <textarea id=\"pth-tr-in\" class=\"pth-textarea\" style=\"min-height:150px;\" placeholder=\"Type or pull the text you want translated.\" aria-label=\"Source text\"><\/textarea>\r\n                    <\/div>\r\n\r\n                    <div class=\"pth-io\" style=\"margin-bottom:0;\">\r\n                        <div class=\"pth-io-head\">\r\n                            <label class=\"pth-label\" for=\"pth-tr-out\">Translation<\/label>\r\n                            <button type=\"button\" class=\"pth-btn\" id=\"pth-tr-copy\">\ud83d\udccb Copy<\/button>\r\n                        <\/div>\r\n                        <textarea id=\"pth-tr-out\" class=\"pth-textarea output\" style=\"min-height:150px;\" readonly placeholder=\"Translation appears here...\" aria-label=\"Translation output\"><\/textarea>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n\r\n        <!-- ============ SIDEBAR ============ -->\r\n        <div class=\"pth-card pth-side\" id=\"pth-vs-side\">\r\n\r\n            <div id=\"pth-side-dictate\">\r\n                <h3>Voice commands<\/h3>\r\n                <p>Say any of these while dictating and the word is turned into the action instead of being typed out.<\/p>\r\n                <div class=\"pth-cmdrow\"><label for=\"pth-cmd-nl\">New line<\/label><input type=\"text\" id=\"pth-cmd-nl\" value=\"new line, next line\" aria-label=\"New line commands\"><\/div>\r\n                <div class=\"pth-cmdrow\"><label for=\"pth-cmd-para\">Paragraph<\/label><input type=\"text\" id=\"pth-cmd-para\" value=\"new paragraph\" aria-label=\"Paragraph commands\"><\/div>\r\n                <div class=\"pth-cmdrow\"><label for=\"pth-cmd-fs\">Full stop<\/label><input type=\"text\" id=\"pth-cmd-fs\" value=\"full stop, period\" aria-label=\"Full stop commands\"><\/div>\r\n                <div class=\"pth-cmdrow\"><label for=\"pth-cmd-cm\">Comma<\/label><input type=\"text\" id=\"pth-cmd-cm\" value=\"comma\" aria-label=\"Comma commands\"><\/div>\r\n                <div class=\"pth-cmdrow\"><label for=\"pth-cmd-q\">Question mark<\/label><input type=\"text\" id=\"pth-cmd-q\" value=\"question mark\" aria-label=\"Question mark commands\"><\/div>\r\n                <div class=\"pth-cmdrow\"><label for=\"pth-cmd-st\">Stop<\/label><input type=\"text\" id=\"pth-cmd-st\" value=\"stop dictation, stop recording\" aria-label=\"Stop commands\"><\/div>\r\n                <button type=\"button\" class=\"pth-btn\" id=\"pth-save-cmds\" style=\"width:100%; margin-top:6px;\">Save commands<\/button>\r\n\r\n                <h3 class=\"spaced\">Custom vocabulary<\/h3>\r\n                <p>One correction per line, written as <code>heard = wanted<\/code>. Useful for names and technical terms the recogniser keeps getting wrong.<\/p>\r\n                <textarea id=\"pth-vocab\" class=\"pth-textarea\" style=\"min-height:110px; font-size:13px;\" placeholder=\"prime tool hub = Prime Tool Hub&#10;java script = JavaScript\" aria-label=\"Custom vocabulary rules\"><\/textarea>\r\n                <button type=\"button\" class=\"pth-btn\" id=\"pth-save-vocab\" style=\"width:100%; margin-top:8px;\">Save vocabulary<\/button>\r\n\r\n                <div class=\"pth-tip\" style=\"margin-top:16px;\"><strong>Getting better results:<\/strong> speak at a normal pace rather than slowly. Recognisers use surrounding words for context, so over-enunciating one word at a time usually makes accuracy worse, not better.<\/div>\r\n            <\/div>\r\n\r\n            <div id=\"pth-side-cleanup\" style=\"display:none;\">\r\n                <h3>Why dictated text needs cleaning<\/h3>\r\n                <p>Speech recognisers transcribe what you say, including the sounds you did not mean to make. Every \"um\" and every false start lands in the text exactly as spoken.<\/p>\r\n                <p>They also repeat words when you hesitate mid-phrase, and they rarely place punctuation the way a writer would. None of that is a fault in the recogniser \u2014 it is doing its job faithfully.<\/p>\r\n                <div class=\"pth-tip\"><strong>Order matters:<\/strong> remove fillers before fixing capitalisation. Delete a sentence-opening \"um\" afterwards and you are left with a lower-case sentence start.<\/div>\r\n                <h3 class=\"spaced\">For heavier text work<\/h3>\r\n                <p>Full case conversion, keyword density and detailed counts live in the <a href=\"https:\/\/schoolict.net\/tools\/free-word-counter-and-text-case-converter\/\">Word Counter &amp; Case Converter<\/a>.<\/p>\r\n            <\/div>\r\n\r\n            <div id=\"pth-side-stats\" style=\"display:none;\">\r\n                <h3>Reading the numbers<\/h3>\r\n                <p><strong>Words per minute<\/strong> is the one to watch if you are rehearsing a talk. Comfortable listening sits around 130 to 160. Below 110 tends to drag; above 180 leaves an audience behind, especially if they are not native speakers.<\/p>\r\n                <p><strong>Filler ratio<\/strong> matters more than the raw count. Two \"um\"s in 500 words is nothing. Twenty is a habit worth hearing yourself do.<\/p>\r\n                <p><strong>Reading ease<\/strong> is the Flesch score: 60 to 70 is plain everyday writing, below 30 reads like a legal document. Spoken language usually scores higher than written, so a low score on a transcript means the sentences ran long.<\/p>\r\n                <div class=\"pth-tip\"><strong>Practical use:<\/strong> dictate your talk, check the words per minute, then work out whether it fits the slot you were given before you stand up.<\/div>\r\n            <\/div>\r\n\r\n            <div id=\"pth-side-translate\" style=\"display:none;\">\r\n                <h3>Translation without uploading<\/h3>\r\n                <p>This tab uses your browser's built-in translation models. The language pack is downloaded once by the browser itself, then everything runs on your machine \u2014 your text is never sent to a translation service.<\/p>\r\n                <p>That is a deliberate trade. A server-based translator would work in every browser, but it would mean posting whatever you just dictated to somebody else's machine. For notes, medical dictation or anything confidential, that is not a trade worth making.<\/p>\r\n                <div class=\"pth-tip\"><strong>Support today:<\/strong> on-device translation currently ships in Chrome and Edge 138 and later. Other browsers will show an unavailable message here rather than quietly falling back to an online service.<\/div>\r\n            <\/div>\r\n\r\n            <h3 class=\"spaced\">Related tools<\/h3>\r\n            <ul>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/ai-offline-transcription\/\">AI Offline Transcription<\/a> \u2014 for an existing audio file rather than live speech<\/li>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/advanced-text-to-speech-generator\/\">Text to Speech Generator<\/a> \u2014 the reverse direction<\/li>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/free-word-counter-and-text-case-converter\/\">Word Counter &amp; Case Converter<\/a> \u2014 deeper text statistics<\/li>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/free-ai-text-summarizer-tldr-generator\/\">AI Text Summarizer<\/a> \u2014 condense a long transcript<\/li>\r\n            <\/ul>\r\n        <\/div>\r\n\r\n    <\/div>\r\n<\/div>\r\n\r\n<script data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\">\r\n(function() {\r\n    'use strict';\r\n\r\n    window.pthVoiceStudioApp = {\r\n        initialized: false,\r\n        currentTab: 'dictate',\r\n        recognition: null,\r\n        listening: false,\r\n        bound: false,\r\n        manualStop: false,\r\n        startedAt: 0,\r\n        elapsed: 0,\r\n        timerHandle: null,\r\n        lastChunk: '',\r\n        lastFinalAt: 0,\r\n        translator: null,\r\n        translatorKey: '',\r\n\r\n        FILLERS: ['um', 'uh', 'erm', 'ah', 'hmm', 'mhm', 'you know', 'i mean', 'sort of', 'kind of', 'basically', 'actually', 'literally', 'obviously', 'like'],\r\n        STOPWORDS: ['the', 'a', 'an', 'and', 'or', 'but', 'if', 'of', 'to', 'in', 'on', 'at', 'for', 'with', 'is', 'are', 'was', 'were', 'be', 'been', 'it', 'its', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'we', 'they', 'my', 'your', 'as', 'so', 'do', 'does', 'did', 'have', 'has', 'had', 'not', 'no', 'yes', 'from', 'by', 'about', 'will', 'can', 'just'],\r\n\r\n        \/* ================= init ================= *\/\r\n        \/* Each step is isolated. If one subsystem fails - a blocked storage API,\r\n           a missing speech engine \\u2014 the rest of the tool still binds and works,\r\n           and the failure is reported instead of being swallowed. *\/\r\n        init: function() {\r\n            var self = this;\r\n            var failures = [];\r\n            var step = function(name, fn) {\r\n                try { fn(); } catch (err) {\r\n                    failures.push(name + ': ' + (err && err.message ? err.message : err));\r\n                    if (window.console && window.console.error) {\r\n                        window.console.error('[Voice Studio] ' + name + ' failed', err);\r\n                    }\r\n                }\r\n            };\r\n\r\n            step('bindAll', function() { self.bindAll(); });\r\n            step('loadSettings', function() { self.loadSettings(); });\r\n            step('setupRecognition', function() { self.setupRecognition(); });\r\n            step('checkTranslator', function() { self.checkTranslator(); });\r\n            step('restoreDraft', function() { self.restoreDraft(); });\r\n            step('updateCounts', function() { self.updateCounts(); });\r\n            step('runCleanScan', function() { self.runCleanScan(); });\r\n\r\n            if (failures.length) { this.reportFailures(failures); }\r\n        },\r\n\r\n        reportFailures: function(list) {\r\n            var note = document.getElementById('pth-mic-support');\r\n            if (!note) { return; }\r\n            note.className = 'pth-note warn';\r\n            note.style.display = 'block';\r\n            note.innerHTML = '<strong>Part of this tool could not start.<' + '\/strong> The rest still works. Technical detail: ' + list.join(' | ');\r\n        },\r\n\r\n        \/* Every listener is attached to the document rather than to each button.\r\n           WordPress themes and optimisation plugins sometimes replace nodes after\r\n           load, which silently discards listeners bound directly to elements.\r\n           Delegation keeps the tool working through any of that. *\/\r\n        bindAll: function() {\r\n            var self = this;\r\n            if (this.bound) { return; }\r\n            this.bound = true;\r\n\r\n            document.addEventListener('click', function(e) {\r\n                var node = self.owned(e.target);\r\n                if (!node) { return; }\r\n\r\n                var tab = self.closestClass(node, 'pth-tab');\r\n                if (tab) {\r\n                    e.preventDefault();\r\n                    self.switchTab(tab.getAttribute('data-pth-panel'));\r\n                    return;\r\n                }\r\n\r\n                var btn = self.closestTag(node, 'BUTTON');\r\n                if (!btn || !btn.id) { return; }\r\n                self.route(btn.id, btn, e);\r\n            }, false);\r\n\r\n            document.addEventListener('input', function(e) {\r\n                var node = self.owned(e.target);\r\n                if (!node || !node.id) { return; }\r\n                if (node.id === 'pth-editor') {\r\n                    self.updateCounts();\r\n                    if (self.checked('pth-opt-autosave')) { self.store('draft', node.value); }\r\n                } else if (node.id === 'pth-clean-in') {\r\n                    self.runCleanScan();\r\n                }\r\n            }, false);\r\n\r\n            document.addEventListener('change', function(e) {\r\n                var node = self.owned(e.target);\r\n                if (!node || !node.id) { return; }\r\n                if (node.id === 'pth-dict-lang') {\r\n                    self.store('lang', node.value);\r\n                    if (self.recognition) { self.recognition.lang = node.value; }\r\n                } else if (node.id === 'pth-tr-from' || node.id === 'pth-tr-to') {\r\n                    self.checkTranslator();\r\n                }\r\n            }, false);\r\n        },\r\n\r\n        owned: function(node) {\r\n            while (node && node.nodeType === 1) {\r\n                if (node.id === 'pth-voice-studio-wrapper') { return null; }\r\n                if (node.parentNode && node.parentNode.id === 'pth-voice-studio-wrapper') { return node; }\r\n                if (this.insideWrapper(node)) { return node; }\r\n                node = node.parentNode;\r\n            }\r\n            return null;\r\n        },\r\n\r\n        insideWrapper: function(node) {\r\n            var p = node;\r\n            while (p && p.nodeType === 1) {\r\n                if (p.id === 'pth-voice-studio-wrapper') { return true; }\r\n                p = p.parentNode;\r\n            }\r\n            return false;\r\n        },\r\n\r\n        closestClass: function(node, cls) {\r\n            while (node && node.nodeType === 1) {\r\n                if (node.className && (' ' + node.className + ' ').indexOf(' ' + cls + ' ') >= 0) { return node; }\r\n                if (node.id === 'pth-voice-studio-wrapper') { return null; }\r\n                node = node.parentNode;\r\n            }\r\n            return null;\r\n        },\r\n\r\n        closestTag: function(node, tag) {\r\n            while (node && node.nodeType === 1) {\r\n                if (node.tagName === tag) { return node; }\r\n                if (node.id === 'pth-voice-studio-wrapper') { return null; }\r\n                node = node.parentNode;\r\n            }\r\n            return null;\r\n        },\r\n\r\n        route: function(id, btn, e) {\r\n            var self = this;\r\n            if (id === 'pth-vs-sample') { this.loadSample(); }\r\n            else if (id === 'pth-vs-fw') { this.toggleWidth(); }\r\n            else if (id === 'pth-vs-clear') { this.clearAll(); }\r\n            else if (id === 'pth-scope-btn') { this.toggleScope(btn); }\r\n            else if (id === 'pth-rec-btn') { this.toggleRecording(); }\r\n            else if (id === 'pth-undo-btn') { this.undoLast(); }\r\n            else if (id === 'pth-copy-btn') { this.copyText(this.val('pth-editor'), 'Text'); }\r\n            else if (id === 'pth-export-btn') { this.download(this.val('pth-editor'), 'dictation.txt'); }\r\n            else if (id === 'pth-save-cmds') { this.saveCommands(); }\r\n            else if (id === 'pth-save-vocab') { this.store('vocab', this.val('pth-vocab')); this.toast('Vocabulary saved in this browser'); }\r\n            else if (id === 'pth-pull-btn') { this.pullInto('pth-clean-in'); this.runCleanScan(); }\r\n            else if (id === 'pth-clean-btn') { this.runClean(); }\r\n            else if (id === 'pth-clean-copy') { this.copyText(this.val('pth-clean-out'), 'Cleaned text'); }\r\n            else if (id === 'pth-clean-push') { this.pushBack(); }\r\n            else if (id === 'pth-stats-btn') { this.runStats(); }\r\n            else if (id === 'pth-tr-btn') { this.runTranslate(); }\r\n            else if (id === 'pth-tr-copy') { this.copyText(this.val('pth-tr-out'), 'Translation'); }\r\n            else if (id === 'pth-tr-pull') { this.pullInto('pth-tr-in'); }\r\n            else { return; }\r\n            if (e && e.preventDefault) { e.preventDefault(); }\r\n        },\r\n\r\n        toggleScope: function(btn) {\r\n            var note = document.getElementById('pth-scope-note');\r\n            if (!note) { return; }\r\n            var open = note.style.display !== 'none';\r\n            note.style.display = open ? 'none' : 'block';\r\n            btn.setAttribute('aria-expanded', open ? 'false' : 'true');\r\n            btn.innerHTML = open ? '\\u24D8 What that covers' : '\\u2715 Hide details';\r\n        },\r\n\r\n        saveCommands: function() {\r\n            this.store('cmdNl', this.val('pth-cmd-nl'));\r\n            this.store('cmdPara', this.val('pth-cmd-para'));\r\n            this.store('cmdFs', this.val('pth-cmd-fs'));\r\n            this.store('cmdCm', this.val('pth-cmd-cm'));\r\n            this.store('cmdQ', this.val('pth-cmd-q'));\r\n            this.store('cmdSt', this.val('pth-cmd-st'));\r\n            this.toast('Voice commands saved in this browser');\r\n        },\r\n\r\n        pullInto: function(targetId) {\r\n            var src = this.val('pth-editor');\r\n            var box = document.getElementById(targetId);\r\n            if (!src) { this.toast('The editor is empty', true); return; }\r\n            if (box) { box.value = src; }\r\n            this.toast('Text pulled from the editor');\r\n        },\r\n\r\n        pushBack: function() {\r\n            var out = this.val('pth-clean-out');\r\n            var ed = document.getElementById('pth-editor');\r\n            if (!out) { this.toast('Clean the text first', true); return; }\r\n            if (ed) { ed.value = out; }\r\n            this.updateCounts();\r\n            this.switchTab('dictate');\r\n            this.toast('Editor updated');\r\n        },\r\n\r\n        switchTab: function(name) {\r\n            var tabs = document.querySelectorAll('#pth-voice-studio-wrapper .pth-tab');\r\n            var panels = document.querySelectorAll('#pth-voice-studio-wrapper .pth-panel');\r\n            var sides = ['dictate', 'cleanup', 'stats', 'translate'];\r\n            var i, el;\r\n\r\n            for (i = 0; i < tabs.length; i++) {\r\n                tabs[i].classList.remove('active');\r\n                if (tabs[i].getAttribute('data-pth-panel') === name) { tabs[i].classList.add('active'); }\r\n            }\r\n            for (i = 0; i < panels.length; i++) { panels[i].classList.remove('active'); }\r\n            el = document.getElementById('pth-panel-' + name);\r\n            if (el) { el.classList.add('active'); }\r\n            for (i = 0; i < sides.length; i++) {\r\n                el = document.getElementById('pth-side-' + sides[i]);\r\n                if (el) { el.style.display = (sides[i] === name) ? 'block' : 'none'; }\r\n            }\r\n            this.currentTab = name;\r\n        },\r\n\r\n        toggleWidth: function() {\r\n            var grid = document.getElementById('pth-vs-grid');\r\n            var side = document.getElementById('pth-vs-side');\r\n            var btn = document.getElementById('pth-vs-fw');\r\n            if (!grid || !side || !btn) { return; }\r\n            if (grid.classList.contains('pth-full-width')) {\r\n                grid.classList.remove('pth-full-width');\r\n                side.style.display = 'block';\r\n                btn.innerHTML = '\\u26F6 Full Width';\r\n            } else {\r\n                grid.classList.add('pth-full-width');\r\n                side.style.display = 'none';\r\n                btn.innerHTML = '\\u25E8 Split View';\r\n            }\r\n        },\r\n\r\n        toast: function(msg, isError) {\r\n            var t = document.getElementById('pth-toast-voice');\r\n            if (!t) { return; }\r\n            t.innerHTML = msg;\r\n            t.className = isError ? 'pth-toast err' : 'pth-toast';\r\n            t.style.display = 'block';\r\n            if (this.toastTimer) { clearTimeout(this.toastTimer); }\r\n            this.toastTimer = setTimeout(function() { t.style.display = 'none'; }, 3000);\r\n        },\r\n\r\n        val: function(id) {\r\n            var el = document.getElementById(id);\r\n            return el ? el.value : '';\r\n        },\r\n\r\n        checked: function(id) {\r\n            var el = document.getElementById(id);\r\n            return el ? el.checked : false;\r\n        },\r\n\r\n        setText: function(id, txt) {\r\n            var el = document.getElementById(id);\r\n            if (el) { el.innerHTML = txt; }\r\n        },\r\n\r\n        copyText: function(value, label) {\r\n            var self = this;\r\n            if (!value) { this.toast('Nothing to copy yet', true); return; }\r\n            if (navigator.clipboard && navigator.clipboard.writeText) {\r\n                navigator.clipboard.writeText(value).then(function() {\r\n                    self.toast(label + ' copied');\r\n                })['catch'](function() { self.fallbackCopy(value, label); });\r\n            } else {\r\n                this.fallbackCopy(value, label);\r\n            }\r\n        },\r\n\r\n        fallbackCopy: function(value, label) {\r\n            var ta = document.createElement('textarea');\r\n            ta.value = value;\r\n            ta.setAttribute('readonly', 'readonly');\r\n            ta.style.position = 'fixed';\r\n            ta.style.left = '-9999px';\r\n            document.body.appendChild(ta);\r\n            ta.select();\r\n            try {\r\n                document.execCommand('copy');\r\n                this.toast(label + ' copied');\r\n            } catch (err) {\r\n                this.toast('Copy failed \\u2014 select and copy manually', true);\r\n            }\r\n            document.body.removeChild(ta);\r\n        },\r\n\r\n        \/* ================= settings persistence ================= *\/\r\n        store: function(key, value) {\r\n            try { window.localStorage.setItem('pthVoice_' + key, value); } catch (err) { return; }\r\n        },\r\n\r\n        fetchStored: function(key) {\r\n            try { return window.localStorage.getItem('pthVoice_' + key); } catch (err) { return null; }\r\n        },\r\n\r\n        loadSettings: function() {\r\n            var map = { 'pth-cmd-nl': 'cmdNl', 'pth-cmd-para': 'cmdPara', 'pth-cmd-fs': 'cmdFs', 'pth-cmd-cm': 'cmdCm', 'pth-cmd-q': 'cmdQ', 'pth-cmd-st': 'cmdSt', 'pth-vocab': 'vocab', 'pth-dict-lang': 'lang' };\r\n            var id, saved, el;\r\n            for (id in map) {\r\n                if (!map.hasOwnProperty(id)) { continue; }\r\n                saved = this.fetchStored(map[id]);\r\n                el = document.getElementById(id);\r\n                if (el && saved !== null) { el.value = saved; }\r\n            }\r\n        },\r\n\r\n        restoreDraft: function() {\r\n            var draft = this.fetchStored('draft');\r\n            var el = document.getElementById('pth-editor');\r\n            if (el && draft && !el.value) {\r\n                el.value = draft;\r\n                this.toast('Local draft restored');\r\n            }\r\n        },\r\n\r\n        \/* ================= Dictate tab ================= *\/\r\n\r\n        setupRecognition: function() {\r\n            var self = this;\r\n            var Ctor = window.SpeechRecognition || window.webkitSpeechRecognition;\r\n            var note = document.getElementById('pth-mic-support');\r\n            var btn = document.getElementById('pth-rec-btn');\r\n\r\n            if (!Ctor) {\r\n                if (note) {\r\n                    note.className = 'pth-note warn';\r\n                    note.style.display = 'block';\r\n                    note.innerHTML = '<strong>Dictation is not available in this browser.<' + '\/strong> Live speech recognition currently works in Chrome, Edge and Safari. You can still type in the editor and use the Cleanup, Analytics and Translate tabs.';\r\n                }\r\n                if (btn) { btn.disabled = true; btn.innerHTML = '\\uD83C\\uDF99\\uFE0F Not supported here'; }\r\n                return;\r\n            }\r\n\r\n            var rec = new Ctor();\r\n            rec.continuous = true;\r\n            rec.interimResults = true;\r\n            rec.maxAlternatives = 1;\r\n            rec.lang = this.val('pth-dict-lang') || 'en-US';\r\n\r\n            rec.onresult = function(event) { self.handleResult(event); };\r\n\r\n            rec.onerror = function(event) {\r\n                var code = event && event.error ? event.error : 'unknown';\r\n                if (code === 'no-speech') { return; }\r\n                if (code === 'not-allowed' || code === 'service-not-allowed') {\r\n                    self.toast('Microphone permission was refused', true);\r\n                    self.stopRecording();\r\n                } else if (code === 'audio-capture') {\r\n                    self.toast('No microphone was found', true);\r\n                    self.stopRecording();\r\n                } else {\r\n                    self.toast('Recogniser reported: ' + code, true);\r\n                }\r\n            };\r\n\r\n            rec.onend = function() {\r\n                if (self.listening && !self.manualStop) {\r\n                    try { rec.start(); } catch (err) { self.stopRecording(); }\r\n                } else {\r\n                    self.stopRecording();\r\n                }\r\n            };\r\n\r\n            this.recognition = rec;\r\n        },\r\n\r\n        toggleRecording: function() {\r\n            if (this.listening) {\r\n                this.manualStop = true;\r\n                try { this.recognition.stop(); } catch (err) { this.stopRecording(); }\r\n            } else {\r\n                this.startRecording();\r\n            }\r\n        },\r\n\r\n        startRecording: function() {\r\n            var self = this;\r\n            if (!this.recognition) { return; }\r\n            this.manualStop = false;\r\n            this.recognition.lang = this.val('pth-dict-lang') || 'en-US';\r\n            try {\r\n                this.recognition.start();\r\n            } catch (err) {\r\n                this.toast('Could not start \\u2014 try again in a moment', true);\r\n                return;\r\n            }\r\n            this.listening = true;\r\n            this.startedAt = new Date().getTime();\r\n            this.lastFinalAt = this.startedAt;\r\n\r\n            var bar = document.getElementById('pth-livebar');\r\n            var btn = document.getElementById('pth-rec-btn');\r\n            if (bar) { bar.classList.add('live'); }\r\n            if (btn) { btn.classList.add('live'); btn.innerHTML = '\\u23F9\\uFE0F Stop Dictation'; }\r\n            this.setText('pth-livetext', 'Listening \\u2014 speak normally');\r\n\r\n            this.timerHandle = setInterval(function() { self.tickTimer(); }, 500);\r\n        },\r\n\r\n        stopRecording: function() {\r\n            if (this.timerHandle) { clearInterval(this.timerHandle); this.timerHandle = null; }\r\n            if (this.listening && this.startedAt) {\r\n                this.elapsed = this.elapsed + Math.round((new Date().getTime() - this.startedAt) \/ 1000);\r\n                var secs = document.getElementById('pth-speak-secs');\r\n                if (secs) { secs.value = this.elapsed; }\r\n            }\r\n            this.listening = false;\r\n            this.manualStop = false;\r\n\r\n            var bar = document.getElementById('pth-livebar');\r\n            var btn = document.getElementById('pth-rec-btn');\r\n            if (bar) { bar.classList.remove('live'); }\r\n            if (btn) { btn.classList.remove('live'); btn.innerHTML = '\\uD83C\\uDF99\\uFE0F Start Dictation'; }\r\n            this.setText('pth-livetext', 'Stopped \\u2014 press Start Dictation to continue');\r\n            this.setText('pth-interim', '');\r\n        },\r\n\r\n        tickTimer: function() {\r\n            if (!this.listening) { return; }\r\n            var total = this.elapsed + Math.round((new Date().getTime() - this.startedAt) \/ 1000);\r\n            var m = Math.floor(total \/ 60);\r\n            var s = total % 60;\r\n            this.setText('pth-timer', (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s);\r\n        },\r\n\r\n        splitCommands: function(raw) {\r\n            var parts = String(raw || '').toLowerCase().split(',');\r\n            var out = [];\r\n            var i, p;\r\n            for (i = 0; i < parts.length; i++) {\r\n                p = parts[i].replace(\/^\\s+|\\s+$\/g, '');\r\n                if (p) { out.push(p); }\r\n            }\r\n            return out;\r\n        },\r\n\r\n        applyVocabulary: function(text) {\r\n            if (!this.checked('pth-opt-vocab')) { return text; }\r\n            var raw = this.val('pth-vocab');\r\n            if (!raw) { return text; }\r\n            var lines = raw.split('\\n');\r\n            var i, parts, from, to, safe;\r\n            for (i = 0; i < lines.length; i++) {\r\n                parts = lines[i].split('=');\r\n                if (parts.length < 2) { continue; }\r\n                from = parts[0].replace(\/^\\s+|\\s+$\/g, '');\r\n                to = parts.slice(1).join('=').replace(\/^\\s+|\\s+$\/g, '');\r\n                if (!from) { continue; }\r\n                safe = from.replace(\/[.*+?^${}()|[\\]\\\\]\/g, '\\\\$&');\r\n                text = text.replace(new RegExp('\\\\b' + safe + '\\\\b', 'gi'), to);\r\n            }\r\n            return text;\r\n        },\r\n\r\n        handleResult: function(event) {\r\n            var editor = document.getElementById('pth-editor');\r\n            if (!editor) { return; }\r\n\r\n            var interim = '';\r\n            var finalText = '';\r\n            var i;\r\n\r\n            for (i = event.resultIndex; i < event.results.length; i++) {\r\n                if (event.results[i].isFinal) {\r\n                    finalText = finalText + event.results[i][0].transcript;\r\n                } else {\r\n                    interim = interim + event.results[i][0].transcript;\r\n                }\r\n            }\r\n\r\n            this.setText('pth-interim', interim ? '\\u201C' + interim + '\\u201D' : '');\r\n\r\n            if (!finalText) { return; }\r\n\r\n            var chunk = finalText.replace(\/^\\s+|\\s+$\/g, '');\r\n            if (!chunk) { return; }\r\n\r\n            var lower = chunk.toLowerCase();\r\n            var stops = this.splitCommands(this.val('pth-cmd-st'));\r\n            for (i = 0; i < stops.length; i++) {\r\n                if (lower === stops[i]) {\r\n                    this.manualStop = true;\r\n                    try { this.recognition.stop(); } catch (err) { this.stopRecording(); }\r\n                    return;\r\n                }\r\n            }\r\n\r\n            chunk = this.replaceCommands(chunk);\r\n            chunk = this.applyVocabulary(chunk);\r\n\r\n            var now = new Date().getTime();\r\n            var gap = now - this.lastFinalAt;\r\n            this.lastFinalAt = now;\r\n\r\n            var existing = editor.value;\r\n            var joiner = '';\r\n\r\n            if (existing && !\/[\\s\\n]$\/.test(existing)) {\r\n                if (\/^[.,!?;:]\/.test(chunk) || \/\\n$\/.test(chunk)) { joiner = ''; }\r\n                else {\r\n                    if (this.checked('pth-opt-punct') && gap > 1600 && !\/[.,!?;:\\n]$\/.test(existing)) {\r\n                        joiner = '. ';\r\n                    } else {\r\n                        joiner = ' ';\r\n                    }\r\n                }\r\n            }\r\n\r\n            var addition = joiner + chunk;\r\n            this.lastChunk = addition;\r\n            editor.value = existing + addition;\r\n\r\n            if (this.checked('pth-opt-cap')) {\r\n                editor.value = this.capitaliseSentences(editor.value);\r\n            }\r\n\r\n            editor.scrollTop = editor.scrollHeight;\r\n            this.updateCounts();\r\n            if (this.checked('pth-opt-autosave')) { this.store('draft', editor.value); }\r\n        },\r\n\r\n        replaceCommands: function(chunk) {\r\n            var self = this;\r\n            var apply = function(text, list, replacement) {\r\n                var i, safe;\r\n                for (i = 0; i < list.length; i++) {\r\n                    safe = list[i].replace(\/[.*+?^${}()|[\\]\\\\]\/g, '\\\\$&');\r\n                    text = text.replace(new RegExp('\\\\b' + safe + '\\\\b', 'gi'), replacement);\r\n                }\r\n                return text;\r\n            };\r\n\r\n            chunk = apply(chunk, this.splitCommands(this.val('pth-cmd-para')), '\\n\\n');\r\n            chunk = apply(chunk, this.splitCommands(this.val('pth-cmd-nl')), '\\n');\r\n            chunk = apply(chunk, this.splitCommands(this.val('pth-cmd-fs')), '.');\r\n            chunk = apply(chunk, this.splitCommands(this.val('pth-cmd-cm')), ',');\r\n            chunk = apply(chunk, this.splitCommands(this.val('pth-cmd-q')), '?');\r\n\r\n            chunk = chunk.replace(\/\\s+([.,!?;:])\/g, '$1');\r\n            chunk = chunk.replace(\/[ \\t]{2,}\/g, ' ');\r\n            return chunk;\r\n        },\r\n\r\n        capitaliseSentences: function(text) {\r\n            text = text.replace(\/(^|[.!?]\\s+|\\n\\s*)([a-z])\/g, function(m, pre, ch) {\r\n                return pre + ch.toUpperCase();\r\n            });\r\n            text = text.replace(\/\\bi\\b\/g, 'I');\r\n            return text;\r\n        },\r\n\r\n        undoLast: function() {\r\n            var editor = document.getElementById('pth-editor');\r\n            if (!editor || !this.lastChunk) { this.toast('Nothing to undo', true); return; }\r\n            var v = editor.value;\r\n            var idx = v.lastIndexOf(this.lastChunk);\r\n            if (idx >= 0 && idx + this.lastChunk.length === v.length) {\r\n                editor.value = v.substring(0, idx);\r\n                this.lastChunk = '';\r\n                this.updateCounts();\r\n                this.toast('Last phrase removed');\r\n            } else {\r\n                this.toast('That phrase has since been edited', true);\r\n            }\r\n        },\r\n\r\n        countWords: function(text) {\r\n            var t = String(text || '').replace(\/[^\\w\\s'\\u2019-]\/g, ' ').replace(\/^\\s+|\\s+$\/g, '');\r\n            if (!t) { return 0; }\r\n            return t.split(\/\\s+\/).length;\r\n        },\r\n\r\n        updateCounts: function() {\r\n            var text = this.val('pth-editor');\r\n            this.setText('pth-wc', this.countWords(text) + ' words');\r\n        },\r\n\r\n        download: function(text, name) {\r\n            if (!text) { this.toast('Nothing to export yet', true); return; }\r\n            try {\r\n                var blob = new Blob([text], { type: 'text\/plain;charset=utf-8' });\r\n                var url = URL.createObjectURL(blob);\r\n                var a = document.createElement('a');\r\n                a.href = url;\r\n                a.download = name;\r\n                document.body.appendChild(a);\r\n                a.click();\r\n                document.body.removeChild(a);\r\n                setTimeout(function() { URL.revokeObjectURL(url); }, 1000);\r\n                this.toast('File saved');\r\n            } catch (err) {\r\n                this.toast('Your browser blocked the download', true);\r\n            }\r\n        },\r\n\r\n        \/* ================= Cleanup tab ================= *\/\r\n\r\n        fillerRegex: function(word) {\r\n            var safe = word.replace(\/[.*+?^${}()|[\\]\\\\]\/g, '\\\\$&');\r\n            return new RegExp('(^|[\\\\s,.;:!?])' + safe + '(?=[\\\\s,.;:!?]|$)', 'gi');\r\n        },\r\n\r\n        countFillers: function(text) {\r\n            var total = 0;\r\n            var i, m;\r\n            for (i = 0; i < this.FILLERS.length; i++) {\r\n                m = text.match(this.fillerRegex(this.FILLERS[i]));\r\n                if (m) { total = total + m.length; }\r\n            }\r\n            return total;\r\n        },\r\n\r\n        runCleanScan: function() {\r\n            var text = this.val('pth-clean-in');\r\n            var box = document.getElementById('pth-clean-findings');\r\n            if (!box) { return; }\r\n\r\n            if (!text) {\r\n                box.innerHTML = '<div class=\"pth-finding\"><span>Paste or pull some text and the scan runs automatically.<' + '\/span><' + '\/div>';\r\n                return;\r\n            }\r\n\r\n            var fillers = this.countFillers(text);\r\n            var repeats = (text.match(\/\\b(\\w+)(\\s+\\1)\\b\/gi) || []).length;\r\n            var dblSpace = (text.match(\/ {2,}\/g) || []).length;\r\n            var spaceBefore = (text.match(\/\\s+[.,!?;:]\/g) || []).length;\r\n            var lowerStart = (text.match(\/(^|[.!?]\\s+)[a-z]\/g) || []).length;\r\n\r\n            var html = '';\r\n            if (fillers) { html += '<div class=\"pth-finding\"><span>\\uD83D\\uDDE3\\uFE0F Filler words<' + '\/span><span class=\"c\">' + fillers + '<' + '\/span><' + '\/div>'; }\r\n            if (repeats) { html += '<div class=\"pth-finding\"><span>\\uD83D\\uDD01 Repeated words<' + '\/span><span class=\"c\">' + repeats + '<' + '\/span><' + '\/div>'; }\r\n            if (dblSpace) { html += '<div class=\"pth-finding\"><span>\\u2423 Double spaces<' + '\/span><span class=\"c\">' + dblSpace + '<' + '\/span><' + '\/div>'; }\r\n            if (spaceBefore) { html += '<div class=\"pth-finding\"><span>\\u2049\\uFE0F Space before punctuation<' + '\/span><span class=\"c\">' + spaceBefore + '<' + '\/span><' + '\/div>'; }\r\n            if (lowerStart) { html += '<div class=\"pth-finding\"><span>\\uD83D\\uDD24 Sentences starting lower case<' + '\/span><span class=\"c\">' + lowerStart + '<' + '\/span><' + '\/div>'; }\r\n            if (!html) { html = '<div class=\"pth-finding\"><span>\\u2705 Nothing obvious to fix \\u2014 this transcript reads cleanly.<' + '\/span><' + '\/div>'; }\r\n\r\n            box.innerHTML = html;\r\n        },\r\n\r\n        runClean: function() {\r\n            var text = this.val('pth-clean-in');\r\n            var out = document.getElementById('pth-clean-out');\r\n            if (!out) { return; }\r\n            if (!text) { this.toast('There is no text to clean', true); return; }\r\n\r\n            var before = text.length;\r\n            var result = text;\r\n            var i;\r\n\r\n            if (this.checked('pth-fix-filler')) {\r\n                for (i = 0; i < this.FILLERS.length; i++) {\r\n                    result = result.replace(this.fillerRegex(this.FILLERS[i]), '$1');\r\n                }\r\n                \/* Removing a filler can strand the punctuation that surrounded it,\r\n                   turning \"simple, you know.\" into \"simple, .\". Tidy that here\r\n                   rather than leaving it for the spacing pass. *\/\r\n                result = result.replace(\/([,;:])\\s*([.!?])\/g, '$2');\r\n                result = result.replace(\/([.!?])\\s*([,;:])\/g, '$1');\r\n                result = result.replace(\/([,;:])\\s*([,;:])\/g, '$1');\r\n                result = result.replace(\/^[\\s,;:]+\/, '');\r\n                result = result.replace(\/\\n[\\s,;:]+\/g, '\\n');\r\n            }\r\n            if (this.checked('pth-fix-repeat')) {\r\n                result = result.replace(\/\\b(\\w+)(\\s+\\1\\b)+\/gi, '$1');\r\n            }\r\n            if (this.checked('pth-fix-space')) {\r\n                result = result.replace(\/\\s+([.,!?;:])\/g, '$1');\r\n                result = result.replace(\/([.,!?;:])(?=[^\\s\\d])\/g, '$1 ');\r\n                result = result.replace(\/[ \\t]{2,}\/g, ' ');\r\n                result = result.replace(\/[ \\t]+$\/gm, '');\r\n            }\r\n            if (this.checked('pth-fix-stops')) {\r\n                result = result.replace(\/([a-zA-Z0-9])[ \\t]*$\/gm, '$1.');\r\n            }\r\n\r\n            \/* Trim before capitalising: a stray leading space would otherwise stop\r\n               the first letter of the text from being recognised as a sentence start. *\/\r\n            result = result.replace(\/^\\s+|\\s+$\/g, '');\r\n\r\n            if (this.checked('pth-fix-sentcap')) {\r\n                result = this.capitaliseSentences(result);\r\n            }\r\n            if (this.checked('pth-fix-para')) {\r\n                \/* Lookbehind is avoided here on purpose: older Safari builds throw\r\n                   a syntax error on it, which would break the whole script. *\/\r\n                var tokens = result.split(\/([.!?]+)\/);\r\n                var built = '';\r\n                var counter = 0;\r\n                var piece;\r\n                for (i = 0; i < tokens.length; i++) {\r\n                    piece = tokens[i];\r\n                    if (!piece) { continue; }\r\n                    built += piece;\r\n                    if (\/^[.!?]+$\/.test(piece)) {\r\n                        counter++;\r\n                        if (counter >= 4 && i < tokens.length - 1) {\r\n                            built += '\\n\\n';\r\n                            counter = 0;\r\n                        } else if (i < tokens.length - 1) {\r\n                            built += ' ';\r\n                        }\r\n                    }\r\n                }\r\n                result = built.replace(\/[ \\t]+\\n\/g, '\\n').replace(\/[ \\t]{2,}\/g, ' ');\r\n            }\r\n\r\n            result = result.replace(\/\\n{3,}\/g, '\\n\\n').replace(\/^\\s+|\\s+$\/g, '');\r\n            out.value = result;\r\n            this.toast('Cleaned \\u2014 ' + Math.abs(before - result.length) + ' characters changed');\r\n        },\r\n\r\n        \/* ================= Analytics tab ================= *\/\r\n\r\n        syllables: function(word) {\r\n            var w = word.toLowerCase().replace(\/[^a-z]\/g, '');\r\n            if (!w) { return 0; }\r\n            if (w.length <= 3) { return 1; }\r\n            w = w.replace(\/(?:es|ed|[^l]e)$\/, '');\r\n            var groups = w.match(\/[aeiouy]{1,2}\/g);\r\n            return groups ? groups.length : 1;\r\n        },\r\n\r\n        runStats: function() {\r\n            var text = this.val('pth-editor') || this.val('pth-clean-in');\r\n            if (!text) {\r\n                this.toast('Dictate or paste some text first', true);\r\n                return;\r\n            }\r\n\r\n            var words = text.replace(\/[^\\w\\s'\\u2019-]\/g, ' ').replace(\/^\\s+|\\s+$\/g, '').split(\/\\s+\/);\r\n            if (words.length === 1 && words[0] === '') { words = []; }\r\n\r\n            var sentenceParts = text.split(\/[.!?]+(?:\\s|$)\/);\r\n            var sentences = [];\r\n            var i, s;\r\n            for (i = 0; i < sentenceParts.length; i++) {\r\n                s = sentenceParts[i].replace(\/^\\s+|\\s+$\/g, '');\r\n                if (s) { sentences.push(s); }\r\n            }\r\n            if (!sentences.length) { sentences = [text]; }\r\n\r\n            var wordCount = words.length;\r\n            var sentCount = sentences.length;\r\n            var avg = sentCount ? Math.round((wordCount \/ sentCount) * 10) \/ 10 : 0;\r\n\r\n            var syl = 0;\r\n            for (i = 0; i < words.length; i++) { syl += this.syllables(words[i]); }\r\n\r\n            var ease = 0;\r\n            var grade = 0;\r\n            if (wordCount > 0 && sentCount > 0) {\r\n                ease = 206.835 - 1.015 * (wordCount \/ sentCount) - 84.6 * (syl \/ wordCount);\r\n                grade = 0.39 * (wordCount \/ sentCount) + 11.8 * (syl \/ wordCount) - 15.59;\r\n            }\r\n            ease = Math.round(ease);\r\n            if (ease > 100) { ease = 100; }\r\n            if (ease < 0) { ease = 0; }\r\n            grade = Math.round(grade * 10) \/ 10;\r\n            if (grade < 1) { grade = 1; }\r\n\r\n            var fillers = this.countFillers(text);\r\n            var secs = parseInt(this.val('pth-speak-secs'), 10);\r\n            if (isNaN(secs)) { secs = 0; }\r\n            var wpm = secs > 0 ? Math.round(wordCount \/ (secs \/ 60)) : null;\r\n\r\n            this.setText('pth-st-words', wordCount);\r\n            this.setText('pth-st-sent', sentCount);\r\n            this.setText('pth-st-avg', avg);\r\n            this.setText('pth-st-filler', fillers);\r\n            this.setText('pth-st-read', ease);\r\n            this.setText('pth-st-grade', grade);\r\n            this.setText('pth-st-wpm', wpm === null ? '\\u2014' : wpm);\r\n\r\n            var wpmBox = document.getElementById('pth-box-wpm');\r\n            var fillBox = document.getElementById('pth-box-filler');\r\n            if (wpmBox) { wpmBox.className = 'pth-statbox' + (wpm !== null && wpm >= 120 && wpm <= 170 ? ' good' : (wpm !== null ? ' warn' : '')); }\r\n            var ratio = wordCount ? (fillers \/ wordCount) * 100 : 0;\r\n            if (fillBox) { fillBox.className = 'pth-statbox' + (ratio > 2 ? ' warn' : ' good'); }\r\n\r\n            var note = document.getElementById('pth-pace-note');\r\n            if (note) {\r\n                var msg = '';\r\n                if (wpm === null) {\r\n                    note.className = 'pth-note info';\r\n                    msg = 'No speaking time recorded, so words per minute cannot be worked out. Dictate a passage, or type the number of seconds you spoke into the box above.';\r\n                } else if (wpm < 110) {\r\n                    note.className = 'pth-note warn';\r\n                    msg = 'At ' + wpm + ' words a minute you are on the slow side. That suits a technical explanation, but a general audience may find it drags.';\r\n                } else if (wpm > 180) {\r\n                    note.className = 'pth-note warn';\r\n                    msg = 'At ' + wpm + ' words a minute you are moving quickly. Listeners following in a second language will struggle to keep up.';\r\n                } else {\r\n                    note.className = 'pth-note ok';\r\n                    msg = 'At ' + wpm + ' words a minute you are in a comfortable listening range.';\r\n                }\r\n                msg += ' Filler words make up ' + (Math.round(ratio * 10) \/ 10) + '% of what you said';\r\n                msg += ratio > 2 ? ' \\u2014 worth trimming.' : ', which is fine.';\r\n                note.innerHTML = msg;\r\n            }\r\n\r\n            this.renderTopWords(words);\r\n            this.renderLongSentences(sentences);\r\n        },\r\n\r\n        renderTopWords: function(words) {\r\n            var counts = {};\r\n            var i, w;\r\n            for (i = 0; i < words.length; i++) {\r\n                w = words[i].toLowerCase().replace(\/[^a-z'\\u2019-]\/g, '');\r\n                if (!w || w.length < 3) { continue; }\r\n                if (this.STOPWORDS.indexOf(w) >= 0) { continue; }\r\n                counts[w] = (counts[w] || 0) + 1;\r\n            }\r\n            var list = [];\r\n            for (w in counts) {\r\n                if (counts.hasOwnProperty(w) && counts[w] > 1) { list.push([w, counts[w]]); }\r\n            }\r\n            list.sort(function(a, b) { return b[1] - a[1]; });\r\n\r\n            var box = document.getElementById('pth-topwords');\r\n            if (!box) { return; }\r\n            if (!list.length) {\r\n                box.innerHTML = '<span style=\"font-size:13px; color:#94a3b8;\">No word was repeated more than once.<' + '\/span>';\r\n                return;\r\n            }\r\n            var html = '';\r\n            var limit = list.length > 14 ? 14 : list.length;\r\n            for (i = 0; i < limit; i++) {\r\n                html += '<span class=\"pth-wordchip\">' + list[i][0] + ' \\u00B7 ' + list[i][1] + '<' + '\/span>';\r\n            }\r\n            box.innerHTML = html;\r\n        },\r\n\r\n        renderLongSentences: function(sentences) {\r\n            var box = document.getElementById('pth-longsent');\r\n            if (!box) { return; }\r\n            var scored = [];\r\n            var i, n;\r\n            for (i = 0; i < sentences.length; i++) {\r\n                n = this.countWords(sentences[i]);\r\n                scored.push([sentences[i], n]);\r\n            }\r\n            scored.sort(function(a, b) { return b[1] - a[1]; });\r\n\r\n            var html = '';\r\n            var limit = scored.length > 3 ? 3 : scored.length;\r\n            for (i = 0; i < limit; i++) {\r\n                if (scored[i][1] < 12) { continue; }\r\n                var snippet = scored[i][0];\r\n                if (snippet.length > 150) { snippet = snippet.substring(0, 150) + '\\u2026'; }\r\n                html += '<div class=\"pth-finding\"><span>' + snippet.replace(\/&\/g, '&amp;') + '<' + '\/span><span class=\"c\">' + scored[i][1] + 'w<' + '\/span><' + '\/div>';\r\n            }\r\n            box.innerHTML = html || '<span style=\"font-size:13px; color:#94a3b8;\">No sentence ran long enough to flag.<' + '\/span>';\r\n        },\r\n\r\n        \/* ================= Translate tab (on-device only) ================= *\/\r\n\r\n        translatorApi: function() {\r\n            if (typeof window.Translator !== 'undefined' && window.Translator && window.Translator.availability) { return window.Translator; }\r\n            if (typeof self !== 'undefined' && typeof self.Translator !== 'undefined' && self.Translator && self.Translator.availability) { return self.Translator; }\r\n            return null;\r\n        },\r\n\r\n        setTrStatus: function(cls, html) {\r\n            var el = document.getElementById('pth-tr-status');\r\n            if (!el) { return; }\r\n            el.className = 'pth-note ' + cls;\r\n            el.innerHTML = html;\r\n        },\r\n\r\n        checkTranslator: function() {\r\n            var self = this;\r\n            var api = this.translatorApi();\r\n            var btn = document.getElementById('pth-tr-btn');\r\n            var from = this.val('pth-tr-from');\r\n            var to = this.val('pth-tr-to');\r\n\r\n            this.translator = null;\r\n            this.translatorKey = '';\r\n\r\n            if (!api) {\r\n                if (btn) { btn.disabled = true; }\r\n                this.setTrStatus('warn', '<strong>On-device translation is not available in this browser yet.<' + '\/strong> This tab uses the built-in translation models so that your text never leaves your machine. That capability currently ships in Chrome and Edge version 138 and later. We deliberately do not fall back to an online translation service, because that would mean uploading whatever you just dictated.');\r\n                return;\r\n            }\r\n\r\n            if (from === to) {\r\n                if (btn) { btn.disabled = true; }\r\n                this.setTrStatus('info', 'Choose two different languages.');\r\n                return;\r\n            }\r\n\r\n            this.setTrStatus('info', 'Checking whether this language pair is available on your device...');\r\n\r\n            api.availability({ sourceLanguage: from, targetLanguage: to }).then(function(state) {\r\n                if (state === 'unavailable') {\r\n                    if (btn) { btn.disabled = true; }\r\n                    self.setTrStatus('warn', 'Your browser does not have a model for this language pair. Try another combination \\u2014 English paired with a major language is the most widely supported.');\r\n                } else if (state === 'downloadable' || state === 'downloading') {\r\n                    if (btn) { btn.disabled = false; }\r\n                    self.setTrStatus('info', '<strong>\\uD83D\\uDD12 Runs on your device.<' + '\/strong> The language pack has not been downloaded yet. Press Translate and your browser will fetch the model once, after which translation works with no network at all.');\r\n                } else {\r\n                    if (btn) { btn.disabled = false; }\r\n                    self.setTrStatus('ok', '<strong>\\uD83D\\uDD12 Ready \\u2014 translation runs entirely on your device.<' + '\/strong> Nothing you type here is uploaded to any translation service.');\r\n                }\r\n            })['catch'](function() {\r\n                if (btn) { btn.disabled = true; }\r\n                self.setTrStatus('err', 'The browser reported an error while checking translation support.');\r\n            });\r\n        },\r\n\r\n        runTranslate: function() {\r\n            var self = this;\r\n            var api = this.translatorApi();\r\n            var text = this.val('pth-tr-in');\r\n            var out = document.getElementById('pth-tr-out');\r\n            var btn = document.getElementById('pth-tr-btn');\r\n            var from = this.val('pth-tr-from');\r\n            var to = this.val('pth-tr-to');\r\n\r\n            if (!api) { this.toast('On-device translation is not available here', true); return; }\r\n            if (!text) { this.toast('Enter some text to translate', true); return; }\r\n            if (out) { out.value = ''; }\r\n            if (btn) { btn.disabled = true; btn.innerHTML = '\\u23F3 Working...'; }\r\n\r\n            var key = from + '>' + to;\r\n            var finish = function(translator) {\r\n                self.translator = translator;\r\n                self.translatorKey = key;\r\n                translator.translate(text).then(function(result) {\r\n                    if (out) { out.value = result; }\r\n                    if (btn) { btn.disabled = false; btn.innerHTML = '\\uD83C\\uDF10 Translate'; }\r\n                    self.setTrStatus('ok', '<strong>\\uD83D\\uDD12 Translated on your device.<' + '\/strong> Nothing was uploaded.');\r\n                })['catch'](function() {\r\n                    if (btn) { btn.disabled = false; btn.innerHTML = '\\uD83C\\uDF10 Translate'; }\r\n                    self.toast('Translation failed \\u2014 try a shorter passage', true);\r\n                });\r\n            };\r\n\r\n            if (this.translator && this.translatorKey === key) {\r\n                finish(this.translator);\r\n                return;\r\n            }\r\n\r\n            this.setTrStatus('info', 'Preparing the language model. The first run may take a moment while your browser downloads it.');\r\n\r\n            api.create({ sourceLanguage: from, targetLanguage: to }).then(function(translator) {\r\n                finish(translator);\r\n            })['catch'](function() {\r\n                if (btn) { btn.disabled = false; btn.innerHTML = '\\uD83C\\uDF10 Translate'; }\r\n                self.setTrStatus('err', 'The language model could not be prepared. Try a different language pair.');\r\n            });\r\n        },\r\n\r\n        \/* ================= shared ================= *\/\r\n        loadSample: function() {\r\n            var sample = 'um so today i want to talk about how browser based tools actually work  the the main idea is that everything runs on your own machine you know rather than being uploaded somewhere basically this means your files never leave your device . i think that matters a lot for anyone handling private documents , and it is also why these tools keep working when your connection drops um which is more useful than it sounds';\r\n            var el;\r\n            if (this.currentTab === 'dictate') {\r\n                el = document.getElementById('pth-editor');\r\n                if (el) { el.value = sample; }\r\n                this.updateCounts();\r\n            } else if (this.currentTab === 'cleanup') {\r\n                el = document.getElementById('pth-clean-in');\r\n                if (el) { el.value = sample; }\r\n                this.runCleanScan();\r\n            } else if (this.currentTab === 'stats') {\r\n                el = document.getElementById('pth-editor');\r\n                if (el) { el.value = sample; }\r\n                el = document.getElementById('pth-speak-secs');\r\n                if (el) { el.value = '32'; }\r\n                this.runStats();\r\n            } else {\r\n                el = document.getElementById('pth-tr-in');\r\n                if (el) { el.value = 'These tools run entirely inside your browser. Nothing you type is uploaded.'; }\r\n            }\r\n            this.toast('Sample loaded');\r\n        },\r\n\r\n        clearAll: function() {\r\n            var ids = ['pth-editor', 'pth-clean-in', 'pth-clean-out', 'pth-tr-in', 'pth-tr-out'];\r\n            var i, el;\r\n            for (i = 0; i < ids.length; i++) {\r\n                el = document.getElementById(ids[i]);\r\n                if (el) { el.value = ''; }\r\n            }\r\n            this.elapsed = 0;\r\n            this.lastChunk = '';\r\n            el = document.getElementById('pth-speak-secs');\r\n            if (el) { el.value = '0'; }\r\n            this.setText('pth-timer', '00:00');\r\n            this.store('draft', '');\r\n            this.updateCounts();\r\n            this.runCleanScan();\r\n            this.toast('All fields cleared');\r\n        }\r\n    };\r\n\r\n    \/* ================= 4-layer bootstrap ================= *\/\r\n    function pthVoiceRunInit() {\r\n        if (window.pthVoiceStudioApp.initialized) { return; }\r\n        if (!document.getElementById('pth-voice-studio-wrapper')) { return; }\r\n        window.pthVoiceStudioApp.initialized = true;\r\n        try {\r\n            window.pthVoiceStudioApp.init();\r\n        } catch (err) {\r\n            \/* Report rather than hide: a silent failure here leaves every button\r\n               dead with nothing in the console to explain why. *\/\r\n            if (window.console && window.console.error) {\r\n                window.console.error('[Voice Studio] startup failed', err);\r\n            }\r\n            try {\r\n                window.pthVoiceStudioApp.bindAll();\r\n                window.pthVoiceStudioApp.reportFailures(['startup: ' + (err && err.message ? err.message : err)]);\r\n            } catch (inner) { return; }\r\n        }\r\n    }\r\n\r\n    pthVoiceRunInit();\r\n\r\n    if (document.readyState === 'loading') {\r\n        document.addEventListener('DOMContentLoaded', pthVoiceRunInit);\r\n    }\r\n\r\n    window.addEventListener('load', function() {\r\n        setTimeout(pthVoiceRunInit, 50);\r\n    });\r\n\r\n    function pthVoiceWatch() {\r\n        if (typeof MutationObserver === 'undefined' || !document.body) { return; }\r\n        var observer = new MutationObserver(function() {\r\n            if (document.getElementById('pth-voice-studio-wrapper')) { pthVoiceRunInit(); }\r\n        });\r\n        observer.observe(document.body, { childList: true, subtree: true });\r\n        setTimeout(function() { observer.disconnect(); }, 10000);\r\n    }\r\n\r\n    \/* document.body is null when this script runs from the page header,\r\n       so the observer is attached once the body actually exists. *\/\r\n    if (document.body) {\r\n        pthVoiceWatch();\r\n    } else {\r\n        document.addEventListener('DOMContentLoaded', pthVoiceWatch);\r\n    }\r\n})();\r\n<\/script>\n\n\n\n<style>\n.pth-feature-wrap { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 30px; }\n.pth-feat-box { border: 1px solid #e2e8f0; background: #ffffff; padding: 24px; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.02); transition: all 0.3s ease; }\n.pth-feat-box:hover { border-color: #bfdbfe; transform: translateY(-3px); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.05); }\n.pth-feat-title { font-size: 1.1rem; font-weight: 800; color: #0f172a; margin-bottom: 10px; display: flex; align-items: center; gap: 10px; }\n.pth-feat-text { font-size: 0.9rem; color: #475569; line-height: 1.6; margin: 0; }\n.pth-steps-wrap { border: 1px solid #e2e8f0; background: #ffffff; padding: 40px 30px; border-radius: 16px; text-align: center; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.02); margin-bottom: 40px; }\n.pth-steps-title { font-size: 1.2rem; font-weight: 800; color: #1e293b; text-transform: uppercase; margin-bottom: 35px; letter-spacing: 1px; }\n.pth-steps-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 30px; }\n.pth-step-item { display: flex; flex-direction: column; align-items: center; }\n.pth-step-num { width: 45px; height: 45px; background: #3b82f6; color: #ffffff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 1.2rem; margin-bottom: 15px; box-shadow: 0 4px 10px rgba(59,130,246,0.3); }\n.pth-step-name { font-weight: 800; color: #0f172a; margin-bottom: 8px; font-size: 1.05rem; }\n.pth-step-desc { font-size: 0.9rem; color: #64748b; line-height: 1.5; margin: 0; }\n<\/style>\n<div class=\"pth-feature-wrap\">\n    <div class=\"pth-feat-box\">\n        <div class=\"pth-feat-title\">\ud83d\udfe2 Web Speech API Integration<\/div>\n        <p class=\"pth-feat-text\">Accesses native browser acoustic models to perform highly accurate, client-side speech-to-text processing without external server delays.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n        <div class=\"pth-feat-title\">\ud83d\udd35 Local Cache Persistence<\/div>\n        <p class=\"pth-feat-text\">Automatically writes transcription data to the browser&#8217;s local storage, preventing data loss during extended dictation sessions.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n        <div class=\"pth-feat-title\">\ud83d\udfe3 Real-Time DOM Injection<\/div>\n        <p class=\"pth-feat-text\">Executes synchronous document object model updates, converting audio frequency buffers into text streams instantly on screen.<\/p>\n    <\/div>\n<\/div>\n<div class=\"pth-steps-wrap\">\n    <div class=\"pth-steps-title\">How to Use the Voice Typing Studio<\/div>\n    <div class=\"pth-steps-grid\">\n        <div class=\"pth-step-item\">\n            <div class=\"pth-step-num\">1<\/div>\n            <div class=\"pth-step-name\">Initialize Engine<\/div>\n            <p class=\"pth-step-desc\">Select your target language from the dropdown to load the corresponding acoustic model into your browser&#8217;s memory.<\/p>\n        <\/div>\n        <div class=\"pth-step-item\">\n            <div class=\"pth-step-num\">2<\/div>\n            <div class=\"pth-step-name\">Grant Access<\/div>\n            <p class=\"pth-step-desc\">Allow hardware permissions when prompted so the system can intercept and analyze analog audio streams from your microphone.<\/p>\n        <\/div>\n        <div class=\"pth-step-item\">\n            <div class=\"pth-step-num\">3<\/div>\n            <div class=\"pth-step-name\">Execute Dictation<\/div>\n            <p class=\"pth-step-desc\">Speak clearly into the microphone. Use defined trigger phrases like &#8220;new line&#8221; or &#8220;full stop&#8221; to format the text algorithmically.<\/p>\n        <\/div>\n        <div class=\"pth-step-item\">\n            <div class=\"pth-step-num\">4<\/div>\n            <div class=\"pth-step-name\">Export Output<\/div>\n            <p class=\"pth-step-desc\">Once finished, use the document translation API or extract the raw string data locally as a downloaded text file.<\/p>\n        <\/div>\n    <\/div>\n<\/div>\n\n\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Last Updated: July 2026<\/strong><\/p>\n\n\n\n<h2 id=\"\ud83d\udd34-four-tabs-one-workflow\" class=\"wp-block-heading\">\ud83d\udd34 Four Tabs, One Workflow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dictating is the easy part. What usually eats the time is everything after: pulling out the &#8220;um&#8221;s, fixing sentences that all start in lower case, and working out whether the talk you just rehearsed actually fits its time slot. This page now handles that whole run rather than just the first step.<\/p>\n\n\n\n<h3 id=\"dictate-sixteen-languages-and-it-learns-your-words\" class=\"wp-block-heading\">Dictate \u2014 sixteen languages, and it learns your words<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pick a language, press Start Dictation and speak. Two settings are worth turning on before you begin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Custom vocabulary<\/strong>&nbsp;is the one that quietly saves the most time. Recognisers mangle names and technical terms consistently, and always in the same way, so you can just tell the page what to do about it. One rule per line:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>prime tool hub = Prime Tool Hub<\/code><br><code>java script = JavaScript<\/code><br><code>my sequel = MySQL<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The rules are saved in your browser, so they persist between sessions on this device.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Automatic punctuation<\/strong>&nbsp;watches the gap between phrases. Pause for more than about a second and a half and a full stop is added for you. It is not perfect \u2014 a pause for thought mid-sentence gets read as the end of one \u2014 which is why the Cleanup tab exists.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Voice commands are editable in the sidebar. Say &#8220;new paragraph&#8221;, &#8220;full stop&#8221;, &#8220;comma&#8221; or &#8220;question mark&#8221; and the word becomes the mark instead of being typed out. Say &#8220;stop dictation&#8221; and the microphone switches off without you reaching for the keyboard.<\/p>\n\n\n\n<h3 id=\"cleanup-what-a-transcript-actually-needs\" class=\"wp-block-heading\">Cleanup \u2014 what a transcript actually needs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Paste a transcript or pull the text straight from the editor, and the scan counts what is there before changing anything: filler words, repeated words, double spaces, stray spaces before punctuation, and sentences starting in lower case.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A worked example. This went in:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>um so the the main idea is simple , you know . i think it works<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">and this came out:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>So the main idea is simple. I think it works<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note what happened to the comma. Removing &#8220;you know&#8221; would normally leave you with &#8220;simple, .&#8221; \u2014 a comma stranded in front of a full stop. The cleaner collapses that pair rather than handing you the mess it created. Order matters elsewhere too: fillers come out before capitalisation is applied, because deleting a sentence-opening &#8220;um&#8221; afterwards leaves a lower-case sentence start.<\/p>\n\n\n\n<h3 id=\"analytics-numbers-you-can-act-on\" class=\"wp-block-heading\">Analytics \u2014 numbers you can act on<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This tab exists mainly for people rehearsing something out loud. It reports words per minute, sentence length, filler ratio, Flesch reading ease, an approximate grade level, the words you lean on most, and your longest sentences.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The speaking time is captured automatically while you dictate, so words per minute needs no setup. If you are analysing a transcript you did not dictate here, type the duration into the seconds box instead.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A rough guide: 130 to 160 words a minute is comfortable listening. Below 110 tends to drag. Above 180 loses people, particularly anyone following in a second language \u2014 which, if you are presenting internationally, is most of the room.<\/p>\n\n\n\n<h3 id=\"translate-on-your-device-or-not-at-all\" class=\"wp-block-heading\">Translate \u2014 on your device or not at all<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This tab uses the browser&#8217;s own built-in translation models. The language pack downloads once, then translation runs on your machine with no network involved.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is deliberately no fallback to an online translation service. A server-based translator would work in every browser, but it would mean posting whatever you just dictated to somebody else&#8217;s machine \u2014 and for meeting notes, client details or anything medical, that is not a trade worth making silently. If your browser cannot do it locally, the tab says so plainly.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe1-honest-limits\" class=\"wp-block-heading\">\ud83d\udfe1 Honest Limits<\/h2>\n\n\n\n<h3 id=\"where-the-speech-recognition-actually-runs\" class=\"wp-block-heading\">Where the speech recognition actually runs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Worth being straight about this, because the distinction is easy to miss. Everything this page does with your text \u2014 the cleanup, the analytics, the vocabulary rules, the saved drafts, the translation \u2014 runs locally in your browser and is never sent anywhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The speech recognition itself is a different matter. It is provided by your browser, not by this page, and most browsers process the audio through their own service rather than on the device. Chrome sends it to Google&#8217;s speech service; Safari uses Apple&#8217;s. That is a property of the browser feature every dictation site relies on, and no web page can change it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The practical upshot: for genuinely confidential audio, dictate to a locally running model instead. The&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/ai-offline-transcription\/\">AI Offline Transcription<\/a>&nbsp;tool downloads a speech model into your browser and transcribes a recorded file with no audio leaving your machine at all. Record first, transcribe there, then bring the text here for cleanup and analysis.<\/p>\n\n\n\n<h3 id=\"browser-support\" class=\"wp-block-heading\">Browser support<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udd35&nbsp;<strong>Dictation<\/strong>&nbsp;needs the Web Speech API \u2014 Chrome, Edge and Safari have it; Firefox does not. The page detects this and tells you rather than showing a button that does nothing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udfe0&nbsp;<strong>On-device translation<\/strong>&nbsp;ships in Chrome and Edge 138 and later. Elsewhere the tab reports that it is unavailable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udfe3&nbsp;<strong>Cleanup and Analytics<\/strong>&nbsp;work in every modern browser, because they are plain text processing with nothing exotic behind them.<\/p>\n\n\n\n<h3 id=\"accuracy-is-not-evenly-distributed\" class=\"wp-block-heading\">Accuracy is not evenly distributed<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Recognition quality varies a great deal by language. English, Spanish, French, German and Mandarin are well served. Sinhala and Tamil are supported but noticeably weaker, and accented English is handled less reliably than the marketing around these systems suggests. Custom vocabulary helps with the repeated errors; it cannot fix a model that was thinly trained on your accent.<\/p>\n\n\n\n<h3 id=\"drafts-live-in-this-browser-only\" class=\"wp-block-heading\">Drafts live in this browser only<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The auto-saved draft, your vocabulary rules and your voice commands are stored in this browser&#8217;s local storage. They survive a refresh or a closed tab, which is the point. They do not sync to your phone, and clearing site data or using a private window wipes them. Export anything you need to keep.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe2-going-deeper\" class=\"wp-block-heading\">\ud83d\udfe2 Going Deeper<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you want to understand why speaking slowly makes recognition worse rather than better, why homophones are the hardest thing a recogniser faces, and how on-device models changed the privacy picture, the companion article covers it:&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/free-offline-voice-typing-studio-article\/\">how speech recognition actually works<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The technical references are&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Web_Speech_API\" rel=\"noreferrer noopener\" target=\"_blank\">MDN&#8217;s Web Speech API documentation<\/a>&nbsp;and&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Translator\" rel=\"noreferrer noopener\" target=\"_blank\">MDN&#8217;s Translator API reference<\/a>.<\/p>\n\n\n\n\n<style>\n.pth-faq-wrap { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin: 24px 0; }\n.pth-faq-card { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }\n.pth-faq-q { font-size: 15px; font-weight: 800; color: #0f172a; margin: 0 0 8px 0; line-height: 1.45; }\n.pth-faq-a { font-size: 14px; color: #475569; margin: 0; line-height: 1.65; font-weight: 500; }\n@media (max-width: 992px) { .pth-faq-wrap { grid-template-columns: repeat(2, 1fr); } }\n@media (max-width: 640px) { .pth-faq-wrap { grid-template-columns: 1fr; } }\n<\/style>\n \n<h2>\u2753 Frequently Asked Questions<\/h2>\n \n<div class=\"pth-faq-wrap\">\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Is my dictated text uploaded anywhere?<\/p>\n    <p class=\"pth-faq-a\">The text is not. Cleanup, analytics, vocabulary and translation all run in your browser. The audio itself is handled by your browser&#8217;s own speech service, which in Chrome and Safari is processed remotely.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Why does dictation not work in Firefox?<\/p>\n    <p class=\"pth-faq-a\">Firefox has not implemented the Web Speech API. The other three tabs work there normally \u2014 you can paste a transcript and use Cleanup and Analytics.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">The recogniser keeps misspelling a name. Can I fix it?<\/p>\n    <p class=\"pth-faq-a\">Yes. Add a line to the custom vocabulary box as <code>heard = wanted<\/code>. Every future session applies it automatically, and the rule is saved in this browser.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Why is speaking slowly making accuracy worse?<\/p>\n    <p class=\"pth-faq-a\">Recognisers use surrounding words to decide between similar sounds. Isolating each word removes that context, so a natural pace usually produces a better transcript than careful over-enunciation.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">What counts as a filler word here?<\/p>\n    <p class=\"pth-faq-a\">Um, uh, erm, hmm, you know, I mean, sort of, kind of, basically, actually, literally, obviously and like. They are matched as whole words, so &#8220;umbrella&#8221; is left alone.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Is a good words-per-minute figure the goal?<\/p>\n    <p class=\"pth-faq-a\">Only as a guide. Between 130 and 160 suits most audiences. A technical explanation is fine slower; the number matters most when you are fitting a talk to a fixed slot.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Can I transcribe an audio file I already have?<\/p>\n    <p class=\"pth-faq-a\">Not on this page \u2014 this one listens to a live microphone. For an existing recording, use the AI Offline Transcription tool, which runs a model locally on the file.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Will I lose my text if the page reloads?<\/p>\n    <p class=\"pth-faq-a\">No, provided the local draft option is ticked. The text is saved in this browser as you type and restored when you come back on the same device.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Why does translation only work in some browsers?<\/p>\n    <p class=\"pth-faq-a\">It uses the browser&#8217;s built-in on-device models, which currently ship in Chrome and Edge 138 and later. We do not fall back to an online service, because that would mean uploading your text.<\/p>\n  <\/div>\n \n<\/div>\n \n","protected":false},"excerpt":{"rendered":"<p>Voice Studio Pro v3.0 Professional Voice Typing Studio by Prime Tool Hub. Free online speech-to-text with custom voice commands and multi-language Google translation working securely in your browser. \ud83d\udfe2 Web Speech API Integration Accesses native browser acoustic models to perform highly accurate, client-side speech-to-text processing without external server delays. \ud83d\udd35 Local Cache Persistence Automatically writes &#8230; <a title=\"Voice Typing Studio | Free Speech-to-Text &amp; Translator\" class=\"read-more\" href=\"https:\/\/schoolict.net\/tools\/voice-typing-studio-free\/\" aria-label=\"Read more about Voice Typing Studio | Free Speech-to-Text &amp; Translator\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":983,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-978","page","type-page","status-publish","has-post-thumbnail"],"_links":{"self":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/978","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/comments?post=978"}],"version-history":[{"count":1,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/978\/revisions"}],"predecessor-version":[{"id":6730,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/978\/revisions\/6730"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media\/983"}],"wp:attachment":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media?parent=978"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}