{"id":2810,"date":"2026-04-20T13:30:26","date_gmt":"2026-04-20T13:30:26","guid":{"rendered":"https:\/\/primetoolhub.com\/?page_id=2810"},"modified":"2026-08-08T11:17:11","modified_gmt":"2026-08-08T11:17:11","slug":"advanced-text-to-speech-generator","status":"publish","type":"page","link":"https:\/\/schoolict.net\/tools\/advanced-text-to-speech-generator\/","title":{"rendered":"Ultimate Advanced Text to Speech Generator \u2013 Emotional Voices, Voice Cloning, SSML &amp; Multi-Language"},"content":{"rendered":"<div class=\"pth-hero-section\">\n<div class=\"pth-hero-content\">\n<h2><b data-path-to-node=\"1,1,0\" data-index-in-node=\"0\">Next-Gen Text to Speech Studio<\/b> | Lifelike Emotions, Custom Voice Cloning &amp; Multi-Language Synthesis<\/h2>\n<p>The most Advanced Text to Speech Generator with emotional voices, multi-language support (Sinhala, English, Tamil, Hindi), voice cloning, SSML editor, background music mixing and batch export. 100% offline &amp; free.<\/p>\n<div id=\"pth-toc-placeholder\">\u00a0<\/div>\n<\/div>\n<div class=\"pth-hero-image\"><img decoding=\"async\" src=\"https:\/\/schoolict.net\/tools\/wp-content\/uploads\/2026\/04\/advanced-text-to-speech-generator-1024x572.jpeg\" alt=\"advanced text to speech generator\" width=\"450\" height=\"253\" data-no-lazy=\"1\" \/><\/div>\n<\/div>\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-what-this-narration-studio-is-really-for\">\ud83d\udd34\u00a0What this narration studio is really for<\/a><\/li><li><a href=\"#\ud83d\udfe1-a-real-workflow-script-to-captioned-voiceover\">\ud83d\udfe1\u00a0A real workflow: script to captioned voiceover<\/a><\/li><li><a href=\"#\ud83d\udfe2-where-the-browser-voice-engine-stops\">\ud83d\udfe2\u00a0Where the browser voice engine stops<\/a><\/li><\/ul><\/nav><\/div>\n\n\n<!-- ============================================================\r\n  AI VOICE & NARRATION STUDIO v6.0  (PrimeToolHub #54)\r\n  Slug: advanced-text-to-speech-generator\r\n  100% offline - uses the browser SpeechSynthesis engine only.\r\n  No external audio\/translate calls. Scoped to #pth-voicestudio-wrapper.\r\n  Woody HTML snippet - pure HTML + Vanilla JS (ES5) + scoped CSS.\r\n============================================================= -->\r\n\r\n<style>\r\n#pth-voicestudio-wrapper *, #pth-voicestudio-wrapper *::before, #pth-voicestudio-wrapper *::after { box-sizing: border-box; min-width: 0; }\r\n#pth-voicestudio-wrapper {\r\n  font-family: 'Inter', system-ui, -apple-system, \"Segoe UI\", sans-serif;\r\n  color: #1e293b; font-weight: 500; line-height: 1.6;\r\n  max-width: 1480px; margin: 0 auto; padding: 0 20px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-topbar {\r\n  display: flex; align-items: center; justify-content: space-between; gap: 14px;\r\n  flex-wrap: wrap; background: #fff; border: 1px solid #e2e8f0; border-radius: 14px;\r\n  padding: 16px 22px; margin-bottom: 18px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-brand { display: flex; align-items: center; gap: 14px; }\r\n#pth-voicestudio-wrapper .pth-vs-icon {\r\n  width: 46px; height: 46px; border-radius: 11px; background: #2563eb; color: #fff;\r\n  display: flex; align-items: center; justify-content: center; font-size: 24px; flex: 0 0 auto;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-title { font-size: 1.4rem; font-weight: 800; color: #0f172a; margin: 0; }\r\n#pth-voicestudio-wrapper .pth-vs-sub { font-size: 0.8rem; color: #334155; font-weight: 600; margin: 2px 0 0; }\r\n#pth-voicestudio-wrapper .pth-vs-badges { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }\r\n#pth-voicestudio-wrapper .pth-vs-badge {\r\n  font-size: 0.72rem; font-weight: 800; padding: 5px 11px; border-radius: 20px;\r\n  border: 1px solid #e2e8f0; color: #334155; background: #f8fafc;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-badge.pth-vs-offline { color: #047857; border-color: #a7f3d0; background: #ecfdf5; }\r\n#pth-voicestudio-wrapper .pth-vs-badge.pth-vs-ver { color: #1d4ed8; border-color: #bfdbfe; background: #eff6ff; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-tabs {\r\n  display: flex; gap: 6px; background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 12px;\r\n  padding: 5px; margin-bottom: 18px; flex-wrap: wrap;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-tab {\r\n  flex: 1 1 auto; min-width: 130px; padding: 11px 14px; border: none; cursor: pointer;\r\n  border-radius: 8px; background: transparent; color: #334155; font-family: inherit;\r\n  font-size: 0.9rem; font-weight: 700; transition: background 0.15s, color 0.15s;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-tab:hover { color: #0f172a; }\r\n#pth-voicestudio-wrapper .pth-vs-tab.pth-vs-active { background: #2563eb; color: #fff; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-layout { display: grid; grid-template-columns: 1.8fr 1fr; gap: 18px; align-items: start; }\r\n#pth-voicestudio-wrapper.pth-vs-full .pth-vs-layout { grid-template-columns: 1fr; }\r\n#pth-voicestudio-wrapper .pth-vs-panel { display: none; }\r\n#pth-voicestudio-wrapper .pth-vs-panel.pth-vs-active { display: block; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-card {\r\n  background: #fff; border: 1px solid #e2e8f0; border-radius: 14px; padding: 20px; margin-bottom: 16px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-card-title {\r\n  font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 1px;\r\n  color: #1d4ed8; margin: 0 0 14px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }\r\n\r\n#pth-voicestudio-wrapper textarea.pth-vs-input {\r\n  width: 100%; min-height: 200px; resize: vertical; border: 1.5px solid #e2e8f0; border-radius: 10px;\r\n  padding: 14px 16px; font-family: inherit; font-size: 0.95rem; line-height: 1.7; color: #1e293b;\r\n  background: #f8fafc; outline: none; transition: border-color 0.15s;\r\n}\r\n#pth-voicestudio-wrapper textarea.pth-vs-input:focus { border-color: #2563eb; background: #fff; }\r\n#pth-voicestudio-wrapper .pth-vs-meta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }\r\n#pth-voicestudio-wrapper .pth-vs-pill {\r\n  font-size: 0.76rem; font-weight: 700; color: #334155; background: #f1f5f9;\r\n  border: 1px solid #e2e8f0; border-radius: 20px; padding: 4px 11px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-pill b { color: #1d4ed8; font-weight: 800; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-emo { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 12px; }\r\n#pth-voicestudio-wrapper .pth-vs-emo-btn {\r\n  font-size: 0.8rem; font-weight: 700; padding: 6px 12px; border-radius: 20px; cursor: pointer;\r\n  border: 1px solid #bfdbfe; background: #eff6ff; color: #1d4ed8; font-family: inherit;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-emo-btn:hover { background: #dbeafe; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-ctrls { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; }\r\n#pth-voicestudio-wrapper .pth-vs-cg { display: flex; flex-direction: column; gap: 6px; }\r\n#pth-voicestudio-wrapper .pth-vs-label { font-size: 0.72rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.6px; color: #334155; }\r\n#pth-voicestudio-wrapper select.pth-vs-select {\r\n  width: 100%; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 12px; background: #f8fafc;\r\n  font-family: inherit; font-size: 0.88rem; font-weight: 600; color: #1e293b; outline: none; cursor: pointer;\r\n}\r\n#pth-voicestudio-wrapper select.pth-vs-select:focus { border-color: #2563eb; background: #fff; }\r\n#pth-voicestudio-wrapper .pth-vs-range-head { display: flex; justify-content: space-between; align-items: center; }\r\n#pth-voicestudio-wrapper .pth-vs-range-val {\r\n  font-size: 0.78rem; font-weight: 800; color: #1d4ed8; background: #eff6ff; border-radius: 5px; padding: 1px 8px;\r\n}\r\n#pth-voicestudio-wrapper input[type=range] {\r\n  -webkit-appearance: none; appearance: none; width: 100%; height: 5px; border-radius: 5px; background: #e2e8f0; outline: none; cursor: pointer;\r\n}\r\n#pth-voicestudio-wrapper input[type=range]::-webkit-slider-thumb {\r\n  -webkit-appearance: none; appearance: none; width: 17px; height: 17px; border-radius: 50%; background: #2563eb; cursor: pointer;\r\n}\r\n#pth-voicestudio-wrapper input[type=range]::-moz-range-thumb { width: 17px; height: 17px; border: none; border-radius: 50%; background: #2563eb; cursor: pointer; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-btn {\r\n  display: inline-flex; align-items: center; gap: 7px; padding: 10px 17px; border-radius: 8px; border: 1px solid transparent;\r\n  font-family: inherit; font-size: 0.88rem; font-weight: 700; cursor: pointer; transition: filter 0.15s, background 0.15s;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-btn:disabled { opacity: 0.5; cursor: not-allowed; }\r\n#pth-voicestudio-wrapper .pth-vs-btn-sm { padding: 7px 13px; font-size: 0.82rem; }\r\n#pth-voicestudio-wrapper .pth-vs-primary { background: #2563eb; color: #fff; }\r\n#pth-voicestudio-wrapper .pth-vs-primary:hover:not(:disabled) { background: #1d4ed8; }\r\n#pth-voicestudio-wrapper .pth-vs-success { background: #059669; color: #fff; }\r\n#pth-voicestudio-wrapper .pth-vs-success:hover:not(:disabled) { filter: brightness(0.94); }\r\n#pth-voicestudio-wrapper .pth-vs-danger { background: #fef2f2; color: #991b1b; border-color: #fecaca; }\r\n#pth-voicestudio-wrapper .pth-vs-danger:hover:not(:disabled) { background: #fee2e2; }\r\n#pth-voicestudio-wrapper .pth-vs-ghost { background: #f8fafc; color: #334155; border-color: #e2e8f0; }\r\n#pth-voicestudio-wrapper .pth-vs-ghost:hover:not(:disabled) { background: #f1f5f9; color: #0f172a; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-transport { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-stage {\r\n  min-height: 90px; border: 1px solid #e2e8f0; border-radius: 10px; background: #f8fafc;\r\n  padding: 16px; font-size: 1rem; line-height: 1.9; color: #334155;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-word.pth-vs-hl { background: #fde68a; color: #92400e; border-radius: 3px; padding: 0 2px; font-weight: 700; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-side-title { font-size: 0.95rem; font-weight: 800; color: #0f172a; margin: 0 0 10px; }\r\n#pth-voicestudio-wrapper .pth-vs-side-text { font-size: 0.86rem; color: #334155; margin: 0 0 10px; }\r\n#pth-voicestudio-wrapper .pth-vs-side-list { margin: 0; padding-left: 18px; font-size: 0.85rem; color: #334155; }\r\n#pth-voicestudio-wrapper .pth-vs-side-list li { margin-bottom: 6px; }\r\n#pth-voicestudio-wrapper .pth-vs-xlink { color: #1d4ed8; font-weight: 700; text-decoration: none; }\r\n#pth-voicestudio-wrapper .pth-vs-xlink:hover { text-decoration: underline; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-speaker-row {\r\n  display: grid; grid-template-columns: 1.2fr 1fr auto; gap: 10px; align-items: center;\r\n  background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 12px; margin-bottom: 10px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-speaker-name { font-weight: 800; color: #0f172a; font-size: 0.9rem; }\r\n#pth-voicestudio-wrapper .pth-vs-speaker-mini { display: flex; gap: 8px; }\r\n#pth-voicestudio-wrapper .pth-vs-mini-cg { display: flex; flex-direction: column; gap: 3px; }\r\n#pth-voicestudio-wrapper .pth-vs-mini-cg span { font-size: 0.64rem; font-weight: 700; color: #334155; text-transform: uppercase; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-sub-out {\r\n  min-height: 140px; border: 1px solid #e2e8f0; border-radius: 10px; background: #0f172a; color: #e2e8f0;\r\n  padding: 14px 16px; font-family: 'JetBrains Mono', ui-monospace, \"SF Mono\", Menlo, monospace;\r\n  font-size: 0.82rem; line-height: 1.7; white-space: pre-wrap; word-break: break-word; overflow: auto; max-height: 340px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-note {\r\n  font-size: 0.84rem; color: #92400e; background: #fffbeb; border: 1px solid #fde68a; border-radius: 10px;\r\n  padding: 12px 14px; margin-top: 12px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-note b { color: #78350f; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-lex-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: 8px; margin-bottom: 8px; }\r\n#pth-voicestudio-wrapper input.pth-vs-text {\r\n  width: 100%; border: 1px solid #e2e8f0; border-radius: 8px; padding: 9px 12px; font-family: inherit;\r\n  font-size: 0.88rem; color: #1e293b; background: #f8fafc; outline: none;\r\n}\r\n#pth-voicestudio-wrapper input.pth-vs-text:focus { border-color: #2563eb; background: #fff; }\r\n\r\n#pth-voicestudio-wrapper .pth-vs-toast {\r\n  position: fixed; bottom: 22px; right: 22px; z-index: 999999; display: none;\r\n  padding: 13px 18px; border-radius: 10px; color: #fff; font-family: 'Inter', system-ui, sans-serif;\r\n  font-size: 0.9rem; font-weight: 700; box-shadow: 0 8px 24px rgba(15,23,42,0.2); max-width: 320px;\r\n}\r\n#pth-voicestudio-wrapper .pth-vs-toast.pth-vs-show { display: block; }\r\n\r\n@media (max-width: 768px) {\r\n  #pth-voicestudio-wrapper { padding: 0 10px; }\r\n  #pth-voicestudio-wrapper .pth-vs-layout { grid-template-columns: 1fr; }\r\n  #pth-voicestudio-wrapper .pth-vs-speaker-row { grid-template-columns: 1fr; }\r\n  #pth-voicestudio-wrapper .pth-vs-lex-row { grid-template-columns: 1fr 1fr; }\r\n}\r\n<\/style>\r\n\r\n<div id=\"pth-voicestudio-wrapper\">\r\n\r\n  <div class=\"pth-vs-topbar\">\r\n    <div class=\"pth-vs-brand\">\r\n      <div class=\"pth-vs-icon\" aria-hidden=\"true\">&#127908;<\/div>\r\n      <div>\r\n        <h2 class=\"pth-vs-title\">AI Voice &amp; Narration Studio<\/h2>\r\n        <p class=\"pth-vs-sub\">Offline text-to-speech, dialogue voices &amp; timed subtitles<\/p>\r\n      <\/div>\r\n    <\/div>\r\n    <div class=\"pth-vs-badges\">\r\n      <span class=\"pth-vs-badge pth-vs-ver\">v6.0<\/span>\r\n      <span class=\"pth-vs-badge pth-vs-offline\">&#128274; 100% Offline<\/span>\r\n      <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"toggle-width\" aria-label=\"Toggle full width layout\">&#9906; Full Width<\/button>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <div class=\"pth-vs-tabs\" role=\"tablist\" aria-label=\"Voice studio tools\">\r\n    <button type=\"button\" class=\"pth-vs-tab pth-vs-active\" data-tab=\"narrate\" role=\"tab\" aria-selected=\"true\">&#128266; Narrate<\/button>\r\n    <button type=\"button\" class=\"pth-vs-tab\" data-tab=\"dialogue\" role=\"tab\" aria-selected=\"false\">&#128483;&#65039; Dialogue<\/button>\r\n    <button type=\"button\" class=\"pth-vs-tab\" data-tab=\"ssml\" role=\"tab\" aria-selected=\"false\">&#10024; SSML &amp; Lexicon<\/button>\r\n    <button type=\"button\" class=\"pth-vs-tab\" data-tab=\"subs\" role=\"tab\" aria-selected=\"false\">&#128221; Subtitles &amp; Export<\/button>\r\n  <\/div>\r\n\r\n  <div class=\"pth-vs-layout\">\r\n    <div>\r\n      <!-- ============ TAB: NARRATE ============ -->\r\n      <div class=\"pth-vs-panel pth-vs-active\" data-panel=\"narrate\">\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">01 &middot; Your Script<\/p>\r\n          <div class=\"pth-vs-actions\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"sample\">&#128161; Load Sample<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"paste\">&#128203; Paste<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-danger pth-vs-btn-sm\" data-action=\"clear\">&#128465;&#65039; Clear<\/button>\r\n          <\/div>\r\n          <textarea id=\"pth-vs-text\" class=\"pth-vs-input\" aria-label=\"Text to narrate\" placeholder=\"Type or paste the text you want spoken aloud...\"><\/textarea>\r\n          <div class=\"pth-vs-meta\">\r\n            <span class=\"pth-vs-pill\">Words: <b id=\"pth-vs-words\">0<\/b><\/span>\r\n            <span class=\"pth-vs-pill\">Characters: <b id=\"pth-vs-chars\">0<\/b><\/span>\r\n            <span class=\"pth-vs-pill\">Est. read time: <b id=\"pth-vs-dur\">0s<\/b><\/span>\r\n          <\/div>\r\n          <div class=\"pth-vs-emo\">\r\n            <button type=\"button\" class=\"pth-vs-emo-btn\" data-emo=\"neutral\">&#128528; Neutral<\/button>\r\n            <button type=\"button\" class=\"pth-vs-emo-btn\" data-emo=\"cheerful\">&#128522; Cheerful<\/button>\r\n            <button type=\"button\" class=\"pth-vs-emo-btn\" data-emo=\"calm\">&#128524; Calm<\/button>\r\n            <button type=\"button\" class=\"pth-vs-emo-btn\" data-emo=\"serious\">&#128533; Serious<\/button>\r\n            <button type=\"button\" class=\"pth-vs-emo-btn\" data-emo=\"excited\">&#129321; Excited<\/button>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">02 &middot; Voice &amp; Delivery<\/p>\r\n          <div class=\"pth-vs-ctrls\">\r\n            <div class=\"pth-vs-cg\">\r\n              <label class=\"pth-vs-label\" for=\"pth-vs-voice\">Voice<\/label>\r\n              <select id=\"pth-vs-voice\" class=\"pth-vs-select\" aria-label=\"Select voice\"><\/select>\r\n            <\/div>\r\n            <div class=\"pth-vs-cg\">\r\n              <div class=\"pth-vs-range-head\"><span class=\"pth-vs-label\">Speed<\/span><span class=\"pth-vs-range-val\" id=\"pth-vs-rate-val\">1.0x<\/span><\/div>\r\n              <input type=\"range\" id=\"pth-vs-rate\" min=\"0.5\" max=\"2\" step=\"0.05\" value=\"1\" aria-label=\"Speech speed\">\r\n            <\/div>\r\n            <div class=\"pth-vs-cg\">\r\n              <div class=\"pth-vs-range-head\"><span class=\"pth-vs-label\">Pitch<\/span><span class=\"pth-vs-range-val\" id=\"pth-vs-pitch-val\">1.0<\/span><\/div>\r\n              <input type=\"range\" id=\"pth-vs-pitch\" min=\"0\" max=\"2\" step=\"0.05\" value=\"1\" aria-label=\"Voice pitch\">\r\n            <\/div>\r\n            <div class=\"pth-vs-cg\">\r\n              <div class=\"pth-vs-range-head\"><span class=\"pth-vs-label\">Volume<\/span><span class=\"pth-vs-range-val\" id=\"pth-vs-vol-val\">100%<\/span><\/div>\r\n              <input type=\"range\" id=\"pth-vs-vol\" min=\"0\" max=\"1\" step=\"0.05\" value=\"1\" aria-label=\"Volume\">\r\n            <\/div>\r\n          <\/div>\r\n          <div class=\"pth-vs-transport\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-primary\" data-action=\"speak\">&#9654; Speak<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost\" data-action=\"pause\">&#9208;&#65039; Pause<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost\" data-action=\"resume\">&#9654;&#65039; Resume<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-danger\" data-action=\"stop\">&#9209;&#65039; Stop<\/button>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">03 &middot; Live Playback<\/p>\r\n          <div id=\"pth-vs-stage\" class=\"pth-vs-stage\" role=\"status\" aria-live=\"polite\">Press <b>Speak<\/b> to hear your script. The current word is highlighted as it is read.<\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- ============ TAB: DIALOGUE ============ -->\r\n      <div class=\"pth-vs-panel\" data-panel=\"dialogue\">\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">01 &middot; Dialogue Script<\/p>\r\n          <p class=\"pth-vs-side-text\">Write one line per turn as <b>Speaker: line of text<\/b>. Each detected speaker gets their own voice below.<\/p>\r\n          <div class=\"pth-vs-actions\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"dlg-sample\">&#128161; Load Sample<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-primary pth-vs-btn-sm\" data-action=\"dlg-detect\">&#128270; Detect Speakers<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-danger pth-vs-btn-sm\" data-action=\"dlg-clear\">&#128465;&#65039; Clear<\/button>\r\n          <\/div>\r\n          <textarea id=\"pth-vs-dlg\" class=\"pth-vs-input\" aria-label=\"Dialogue script\" placeholder=\"Narrator: Our story begins on a quiet morning.&#10;Alice: Did you hear that sound?&#10;Bob: It came from the old library.\"><\/textarea>\r\n        <\/div>\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">02 &middot; Cast &amp; Voices<\/p>\r\n          <div id=\"pth-vs-cast\">\r\n            <p class=\"pth-vs-side-text\" id=\"pth-vs-cast-empty\">Press <b>Detect Speakers<\/b> to build the cast list.<\/p>\r\n          <\/div>\r\n          <div class=\"pth-vs-transport\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-primary\" data-action=\"dlg-play\">&#9654; Play Dialogue<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-danger\" data-action=\"stop\">&#9209;&#65039; Stop<\/button>\r\n          <\/div>\r\n        <\/div>\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">03 &middot; Live Playback<\/p>\r\n          <div id=\"pth-vs-dlg-stage\" class=\"pth-vs-stage\" role=\"status\" aria-live=\"polite\">The currently speaking line is highlighted during playback.<\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- ============ TAB: SSML & LEXICON ============ -->\r\n      <div class=\"pth-vs-panel\" data-panel=\"ssml\">\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">01 &middot; Marked-up Script<\/p>\r\n          <p class=\"pth-vs-side-text\">Use simple inline tags: <b>[pause 500]<\/b> for a 500ms break, <b>[rate 1.3]<\/b>&hellip;<b>[\/rate]<\/b>, <b>[pitch 0.8]<\/b>&hellip;<b>[\/pitch]<\/b>, and <b>[emphasis]<\/b>&hellip;<b>[\/emphasis]<\/b>.<\/p>\r\n          <div class=\"pth-vs-actions\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"ssml-sample\">&#128161; Load Sample<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-danger pth-vs-btn-sm\" data-action=\"ssml-clear\">&#128465;&#65039; Clear<\/button>\r\n          <\/div>\r\n          <textarea id=\"pth-vs-ssml\" class=\"pth-vs-input\" aria-label=\"Marked-up script\" placeholder=\"Welcome. [pause 400] This word is [emphasis]important[\/emphasis]. [rate 0.8]Now I am slowing down.[\/rate]\"><\/textarea>\r\n        <\/div>\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">02 &middot; Pronunciation Lexicon<\/p>\r\n          <p class=\"pth-vs-side-text\">Fix tricky names by respelling them phonetically. Replacements apply before speaking (all tabs).<\/p>\r\n          <div id=\"pth-vs-lex\">\r\n            <div class=\"pth-vs-lex-row\">\r\n              <input type=\"text\" class=\"pth-vs-text pth-vs-lex-from\" aria-label=\"Original word\" placeholder=\"Word e.g. Ruwan\">\r\n              <input type=\"text\" class=\"pth-vs-text pth-vs-lex-to\" aria-label=\"Spoken as\" placeholder=\"Spoken as e.g. Roo-wun\">\r\n              <button type=\"button\" class=\"pth-vs-btn pth-vs-danger pth-vs-btn-sm\" data-action=\"lex-del\">&#10005;<\/button>\r\n            <\/div>\r\n          <\/div>\r\n          <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"lex-add\">&#43; Add rule<\/button>\r\n        <\/div>\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">03 &middot; Voice &amp; Preview<\/p>\r\n          <div class=\"pth-vs-ctrls\">\r\n            <div class=\"pth-vs-cg\">\r\n              <label class=\"pth-vs-label\" for=\"pth-vs-ssml-voice\">Voice<\/label>\r\n              <select id=\"pth-vs-ssml-voice\" class=\"pth-vs-select\" aria-label=\"Select voice for SSML\"><\/select>\r\n            <\/div>\r\n          <\/div>\r\n          <div class=\"pth-vs-transport\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-primary\" data-action=\"ssml-play\">&#9654; Play Compiled<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-danger\" data-action=\"stop\">&#9209;&#65039; Stop<\/button>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <!-- ============ TAB: SUBTITLES & EXPORT ============ -->\r\n      <div class=\"pth-vs-panel\" data-panel=\"subs\">\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">01 &middot; Generate Timed Subtitles<\/p>\r\n          <p class=\"pth-vs-side-text\">This reads your <b>Narrate<\/b> script aloud once and captures the real word timings from the speech engine, then builds subtitle files you can drop straight into a video editor.<\/p>\r\n          <div class=\"pth-vs-ctrls\">\r\n            <div class=\"pth-vs-cg\">\r\n              <label class=\"pth-vs-label\" for=\"pth-vs-sub-words\">Words per caption<\/label>\r\n              <select id=\"pth-vs-sub-words\" class=\"pth-vs-select\" aria-label=\"Words per caption line\">\r\n                <option value=\"5\">5<\/option>\r\n                <option value=\"7\" selected>7<\/option>\r\n                <option value=\"10\">10<\/option>\r\n                <option value=\"0\">Per sentence<\/option>\r\n              <\/select>\r\n            <\/div>\r\n          <\/div>\r\n          <div class=\"pth-vs-transport\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-primary\" data-action=\"sub-capture\">&#127909; Capture Timings<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-danger\" data-action=\"stop\">&#9209;&#65039; Stop<\/button>\r\n          <\/div>\r\n          <div class=\"pth-vs-note\" id=\"pth-vs-sub-note\" style=\"display:none;\"><\/div>\r\n        <\/div>\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">02 &middot; Subtitle Output<\/p>\r\n          <div class=\"pth-vs-actions\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"sub-fmt\" data-fmt=\"srt\">SRT<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"sub-fmt\" data-fmt=\"vtt\">WebVTT<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-ghost pth-vs-btn-sm\" data-action=\"sub-fmt\" data-fmt=\"txt\">Plain text<\/button>\r\n          <\/div>\r\n          <div id=\"pth-vs-sub-out\" class=\"pth-vs-sub-out\" aria-live=\"polite\">Captured subtitles will appear here.<\/div>\r\n          <div class=\"pth-vs-transport\">\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-success\" data-action=\"sub-copy\">&#128203; Copy<\/button>\r\n            <button type=\"button\" class=\"pth-vs-btn pth-vs-primary\" data-action=\"sub-download\">&#8681; Download File<\/button>\r\n          <\/div>\r\n        <\/div>\r\n        <div class=\"pth-vs-card\">\r\n          <p class=\"pth-vs-card-title\">03 &middot; Saving Audio<\/p>\r\n          <p class=\"pth-vs-side-text\">Browser system voices play live but cannot be written to an audio file directly &mdash; that is a limitation of every browser, not this tool. To capture the narration as MP3\/WAV, play it here and record with the site audio tools, or pair these subtitles with a video in the editor.<\/p>\r\n          <p class=\"pth-vs-side-text\">\r\n            &#8594; <a class=\"pth-vs-xlink\" href=\"https:\/\/schoolict.net\/tools\/free-all-in-one-audio-editor-converter\/\" target=\"_blank\" rel=\"noopener\">Audio Editor &amp; Converter<\/a>\r\n            &nbsp;&middot;&nbsp;\r\n            <a class=\"pth-vs-xlink\" href=\"https:\/\/schoolict.net\/tools\/turbo-video-editor\/\" target=\"_blank\" rel=\"noopener\">Video Studio<\/a>\r\n          <\/p>\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <!-- ============ SIDEBAR ============ -->\r\n    <aside>\r\n      <div class=\"pth-vs-card\">\r\n        <p class=\"pth-vs-side-title\">&#128172; Voices on your device<\/p>\r\n        <p class=\"pth-vs-side-text\" id=\"pth-vs-voice-count\">Detecting installed voices&hellip;<\/p>\r\n        <p class=\"pth-vs-side-text\">Voice quality and languages depend on your operating system and browser. Chrome and Edge expose the most natural voices; add more from your OS speech settings.<\/p>\r\n      <\/div>\r\n      <div class=\"pth-vs-card\">\r\n        <p class=\"pth-vs-side-title\">&#128161; Quick tips<\/p>\r\n        <ul class=\"pth-vs-side-list\">\r\n          <li>Add a comma or full stop where you want a natural breath.<\/li>\r\n          <li>Lower the speed to about 0.9x for clearer tutorial voiceovers.<\/li>\r\n          <li>In Dialogue, keep speaker names short and consistent.<\/li>\r\n          <li>Capture subtitles on the same voice and speed you will publish with.<\/li>\r\n        <\/ul>\r\n      <\/div>\r\n      <div class=\"pth-vs-card\">\r\n        <p class=\"pth-vs-side-title\">&#128279; Related tools<\/p>\r\n        <ul class=\"pth-vs-side-list\">\r\n          <li><a class=\"pth-vs-xlink\" href=\"https:\/\/schoolict.net\/tools\/voice-typing-studio-free\/\" target=\"_blank\" rel=\"noopener\">Voice Typing &amp; Translator<\/a> &ndash; speech to text<\/li>\r\n          <li><a class=\"pth-vs-xlink\" href=\"https:\/\/schoolict.net\/tools\/ai-offline-transcription\/\" target=\"_blank\" rel=\"noopener\">Offline Transcription<\/a> &ndash; audio to text<\/li>\r\n          <li><a class=\"pth-vs-xlink\" href=\"https:\/\/schoolict.net\/tools\/free-all-in-one-audio-editor-converter\/\" target=\"_blank\" rel=\"noopener\">Audio Editor &amp; Converter<\/a><\/li>\r\n          <li><a class=\"pth-vs-xlink\" href=\"https:\/\/schoolict.net\/tools\/turbo-video-editor\/\" target=\"_blank\" rel=\"noopener\">Video Studio<\/a><\/li>\r\n        <\/ul>\r\n      <\/div>\r\n    <\/aside>\r\n  <\/div>\r\n\r\n  <div id=\"pth-vs-toast\" class=\"pth-vs-toast\" role=\"status\" aria-live=\"polite\"><\/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  if (window.pthVoiceStudioApp) { return; }\r\n\r\n  var App = {\r\n    initialized: false,\r\n    root: null,\r\n    voices: [],\r\n    currentTab: 'narrate',\r\n    speaking: false,\r\n    cast: {},\r\n    subtitleCues: [],\r\n    subtitleFormat: 'srt',\r\n\r\n    init: function () {\r\n      this.root = document.getElementById('pth-voicestudio-wrapper');\r\n      if (!this.root) { return; }\r\n      if (!('speechSynthesis' in window)) {\r\n        this.showToast('This browser has no speech engine. Try Chrome or Edge.', 'error');\r\n      }\r\n      this.loadVoices();\r\n      var self = this;\r\n      if (typeof speechSynthesis !== 'undefined') {\r\n        speechSynthesis.onvoiceschanged = function () { self.loadVoices(); };\r\n      }\r\n      this.attachEvents();\r\n      this.updateCounts();\r\n    },\r\n\r\n    \/* ---------- voices ---------- *\/\r\n    loadVoices: function () {\r\n      if (typeof speechSynthesis === 'undefined') { return; }\r\n      var list = speechSynthesis.getVoices() || [];\r\n      if (!list.length) { return; }\r\n      this.voices = list;\r\n      this.fillVoiceSelect('pth-vs-voice');\r\n      this.fillVoiceSelect('pth-vs-ssml-voice');\r\n      var counter = document.getElementById('pth-vs-voice-count');\r\n      if (counter) {\r\n        var langs = {};\r\n        for (var i = 0; i < list.length; i++) { langs[(list[i].lang || '').slice(0, 2)] = 1; }\r\n        var langCount = 0, k;\r\n        for (k in langs) { if (langs.hasOwnProperty(k) && k) { langCount++; } }\r\n        counter.innerHTML = '<b>' + list.length + '<\/b> voices across <b>' + langCount + '<\/b> languages detected.';\r\n      }\r\n    },\r\n\r\n    fillVoiceSelect: function (id) {\r\n      var sel = document.getElementById(id);\r\n      if (!sel) { return; }\r\n      var prev = sel.value;\r\n      sel.innerHTML = '';\r\n      for (var i = 0; i < this.voices.length; i++) {\r\n        var v = this.voices[i];\r\n        var opt = document.createElement('option');\r\n        opt.value = String(i);\r\n        opt.textContent = v.name + ' (' + v.lang + ')' + (v['default'] ? ' *' : '');\r\n        sel.appendChild(opt);\r\n      }\r\n      if (prev) { sel.value = prev; }\r\n    },\r\n\r\n    getVoice: function (index) {\r\n      var i = parseInt(index, 10);\r\n      if (isNaN(i) || !this.voices[i]) { return null; }\r\n      return this.voices[i];\r\n    },\r\n\r\n    \/* ---------- events (delegated) ---------- *\/\r\n    attachEvents: function () {\r\n      var self = this;\r\n      this.root.addEventListener('click', function (e) {\r\n        var el = e.target;\r\n        while (el && el !== self.root && !el.getAttribute('data-action') && !el.getAttribute('data-tab') && !el.getAttribute('data-emo') && !el.getAttribute('data-fmt')) {\r\n          el = el.parentNode;\r\n        }\r\n        if (!el || el === self.root) { return; }\r\n        if (el.getAttribute('data-tab')) { self.switchTab(el.getAttribute('data-tab')); return; }\r\n        if (el.getAttribute('data-emo')) { self.applyEmotion(el.getAttribute('data-emo')); return; }\r\n        var action = el.getAttribute('data-action');\r\n        if (!action) { return; }\r\n        try { self.handle(action, el); }\r\n        catch (err) { self.showToast('Something went wrong. Please try again.', 'error'); if (window.console) { console.error('[VoiceStudio]', err); } }\r\n      });\r\n      this.root.addEventListener('input', function (e) {\r\n        var t = e.target;\r\n        if (t.id === 'pth-vs-text') { self.updateCounts(); }\r\n        if (t.id === 'pth-vs-rate') { self.setLabel('pth-vs-rate-val', parseFloat(t.value).toFixed(2) + 'x'); }\r\n        if (t.id === 'pth-vs-pitch') { self.setLabel('pth-vs-pitch-val', parseFloat(t.value).toFixed(2)); }\r\n        if (t.id === 'pth-vs-vol') { self.setLabel('pth-vs-vol-val', Math.round(parseFloat(t.value) * 100) + '%'); }\r\n      });\r\n    },\r\n\r\n    handle: function (action, el) {\r\n      switch (action) {\r\n        case 'toggle-width': this.toggleWidth(); break;\r\n        case 'sample': this.loadSample(); break;\r\n        case 'paste': this.pasteInto('pth-vs-text'); break;\r\n        case 'clear': this.clearField('pth-vs-text'); break;\r\n        case 'speak': this.speakNarrate(); break;\r\n        case 'pause': this.pause(); break;\r\n        case 'resume': this.resume(); break;\r\n        case 'stop': this.stop(); break;\r\n        case 'dlg-sample': this.dialogueSample(); break;\r\n        case 'dlg-detect': this.detectSpeakers(); break;\r\n        case 'dlg-clear': this.clearField('pth-vs-dlg'); break;\r\n        case 'dlg-play': this.playDialogue(); break;\r\n        case 'ssml-sample': this.ssmlSample(); break;\r\n        case 'ssml-clear': this.clearField('pth-vs-ssml'); break;\r\n        case 'ssml-play': this.playCompiled(); break;\r\n        case 'lex-add': this.addLexRow(); break;\r\n        case 'lex-del': this.delLexRow(el); break;\r\n        case 'sub-capture': this.captureTimings(); break;\r\n        case 'sub-fmt': this.renderSubtitles(el.getAttribute('data-fmt')); break;\r\n        case 'sub-copy': this.copySubtitles(); break;\r\n        case 'sub-download': this.downloadSubtitles(); break;\r\n      }\r\n    },\r\n\r\n    switchTab: function (name) {\r\n      this.stop();\r\n      this.currentTab = name;\r\n      var tabs = this.root.querySelectorAll('.pth-vs-tab');\r\n      var i;\r\n      for (i = 0; i < tabs.length; i++) {\r\n        var on = tabs[i].getAttribute('data-tab') === name;\r\n        tabs[i].className = 'pth-vs-tab' + (on ? ' pth-vs-active' : '');\r\n        tabs[i].setAttribute('aria-selected', on ? 'true' : 'false');\r\n      }\r\n      var panels = this.root.querySelectorAll('.pth-vs-panel');\r\n      for (i = 0; i < panels.length; i++) {\r\n        panels[i].className = 'pth-vs-panel' + (panels[i].getAttribute('data-panel') === name ? ' pth-vs-active' : '');\r\n      }\r\n    },\r\n\r\n    toggleWidth: function () {\r\n      if (this.root.className.indexOf('pth-vs-full') > -1) { this.root.className = ''; }\r\n      else { this.root.className = 'pth-vs-full'; }\r\n    },\r\n\r\n    \/* ---------- text helpers ---------- *\/\r\n    setLabel: function (id, txt) { var e = document.getElementById(id); if (e) { e.textContent = txt; } },\r\n\r\n    updateCounts: function () {\r\n      var t = document.getElementById('pth-vs-text');\r\n      if (!t) { return; }\r\n      var val = t.value.trim();\r\n      var words = val ? val.split(\/\\s+\/).length : 0;\r\n      this.setLabel('pth-vs-words', String(words));\r\n      this.setLabel('pth-vs-chars', String(t.value.length));\r\n      var secs = Math.round((words \/ 150) * 60);\r\n      this.setLabel('pth-vs-dur', secs < 60 ? (secs + 's') : (Math.floor(secs \/ 60) + 'm ' + (secs % 60) + 's'));\r\n    },\r\n\r\n    clearField: function (id) {\r\n      var e = document.getElementById(id);\r\n      if (e) { e.value = ''; }\r\n      if (id === 'pth-vs-text') { this.updateCounts(); }\r\n      this.stop();\r\n    },\r\n\r\n    pasteInto: function (id) {\r\n      var self = this;\r\n      var el = document.getElementById(id);\r\n      if (!el) { return; }\r\n      if (navigator.clipboard && navigator.clipboard.readText) {\r\n        navigator.clipboard.readText().then(function (txt) {\r\n          el.value = txt; self.updateCounts(); self.showToast('Pasted from clipboard.', 'success');\r\n        })['catch'](function () { self.showToast('Clipboard blocked. Paste manually with Ctrl+V.', 'error'); });\r\n      } else {\r\n        el.focus();\r\n        this.showToast('Paste manually with Ctrl+V.', 'info');\r\n      }\r\n    },\r\n\r\n    applyLexicon: function (text) {\r\n      var rows = this.root.querySelectorAll('#pth-vs-lex .pth-vs-lex-row');\r\n      for (var i = 0; i < rows.length; i++) {\r\n        var from = rows[i].querySelector('.pth-vs-lex-from');\r\n        var to = rows[i].querySelector('.pth-vs-lex-to');\r\n        if (from && to && from.value.trim()) {\r\n          var safe = from.value.trim().replace(\/[.*+?^${}()|[\\]\\\\]\/g, '\\\\$&');\r\n          text = text.replace(new RegExp('\\\\b' + safe + '\\\\b', 'gi'), to.value);\r\n        }\r\n      }\r\n      return text;\r\n    },\r\n\r\n    \/* ---------- narrate ---------- *\/\r\n    buildUtterance: function (text, voiceIndexId, rate, pitch, vol) {\r\n      var u = new SpeechSynthesisUtterance(this.applyLexicon(text));\r\n      var voice = this.getVoice(document.getElementById(voiceIndexId) ? document.getElementById(voiceIndexId).value : voiceIndexId);\r\n      if (voice) { u.voice = voice; u.lang = voice.lang; }\r\n      u.rate = rate; u.pitch = pitch; u.volume = (vol === undefined ? 1 : vol);\r\n      return u;\r\n    },\r\n\r\n    speakNarrate: function () {\r\n      var t = document.getElementById('pth-vs-text');\r\n      if (!t || !t.value.trim()) { this.showToast('Type some text to narrate first.', 'error'); return; }\r\n      this.stop();\r\n      var self = this;\r\n      var rate = parseFloat(document.getElementById('pth-vs-rate').value);\r\n      var pitch = parseFloat(document.getElementById('pth-vs-pitch').value);\r\n      var vol = parseFloat(document.getElementById('pth-vs-vol').value);\r\n      var raw = t.value.trim();\r\n      var u = this.buildUtterance(raw, 'pth-vs-voice', rate, pitch, vol);\r\n      this.renderStageWords('pth-vs-stage', this.applyLexicon(raw));\r\n      u.onboundary = function (ev) { if (ev.name === 'word' || ev.charIndex !== undefined) { self.highlightWord('pth-vs-stage', ev.charIndex); } };\r\n      u.onend = function () { self.speaking = false; self.clearHighlight('pth-vs-stage'); };\r\n      u.onerror = function () { self.speaking = false; };\r\n      this.speaking = true;\r\n      speechSynthesis.speak(u);\r\n    },\r\n\r\n    renderStageWords: function (stageId, text) {\r\n      var stage = document.getElementById(stageId);\r\n      if (!stage) { return; }\r\n      stage.innerHTML = '';\r\n      var re = \/\\S+\\s*\/g, m, idx = 0;\r\n      while ((m = re.exec(text)) !== null) {\r\n        var span = document.createElement('span');\r\n        span.className = 'pth-vs-word';\r\n        span.setAttribute('data-start', String(m.index));\r\n        span.textContent = m[0];\r\n        stage.appendChild(span);\r\n        idx++;\r\n      }\r\n      if (!idx) { stage.textContent = text; }\r\n    },\r\n\r\n    highlightWord: function (stageId, charIndex) {\r\n      var stage = document.getElementById(stageId);\r\n      if (!stage) { return; }\r\n      var words = stage.querySelectorAll('.pth-vs-word');\r\n      var chosen = null;\r\n      for (var i = 0; i < words.length; i++) {\r\n        var s = parseInt(words[i].getAttribute('data-start'), 10);\r\n        if (s <= charIndex) { chosen = words[i]; } else { break; }\r\n      }\r\n      for (var j = 0; j < words.length; j++) { words[j].className = 'pth-vs-word'; }\r\n      if (chosen) { chosen.className = 'pth-vs-word pth-vs-hl'; }\r\n    },\r\n\r\n    clearHighlight: function (stageId) {\r\n      var stage = document.getElementById(stageId);\r\n      if (!stage) { return; }\r\n      var words = stage.querySelectorAll('.pth-vs-word');\r\n      for (var i = 0; i < words.length; i++) { words[i].className = 'pth-vs-word'; }\r\n    },\r\n\r\n    applyEmotion: function (name) {\r\n      var map = {\r\n        neutral: [1, 1], cheerful: [1.08, 1.25], calm: [0.9, 0.95],\r\n        serious: [0.92, 0.8], excited: [1.18, 1.35]\r\n      };\r\n      var p = map[name] || map.neutral;\r\n      var r = document.getElementById('pth-vs-rate'), pi = document.getElementById('pth-vs-pitch');\r\n      if (r) { r.value = p[0]; this.setLabel('pth-vs-rate-val', p[0].toFixed(2) + 'x'); }\r\n      if (pi) { pi.value = p[1]; this.setLabel('pth-vs-pitch-val', p[1].toFixed(2)); }\r\n      this.showToast('Applied \"' + name + '\" delivery.', 'info');\r\n    },\r\n\r\n    pause: function () { if (typeof speechSynthesis !== 'undefined' && speechSynthesis.speaking) { speechSynthesis.pause(); } },\r\n    resume: function () { if (typeof speechSynthesis !== 'undefined' && speechSynthesis.paused) { speechSynthesis.resume(); } },\r\n    stop: function () {\r\n      if (typeof speechSynthesis !== 'undefined') { speechSynthesis.cancel(); }\r\n      this.speaking = false;\r\n    },\r\n\r\n    \/* ---------- dialogue ---------- *\/\r\n    parseDialogue: function () {\r\n      var t = document.getElementById('pth-vs-dlg');\r\n      if (!t) { return []; }\r\n      var lines = t.value.split(\/\\n\/);\r\n      var out = [];\r\n      for (var i = 0; i < lines.length; i++) {\r\n        var line = lines[i].replace(\/^\\s+|\\s+$\/g, '');\r\n        if (!line) { continue; }\r\n        var m = line.match(\/^([^:]{1,40}):\\s*(.+)$\/);\r\n        if (m) { out.push({ speaker: m[1].replace(\/^\\s+|\\s+$\/g, ''), text: m[2] }); }\r\n        else { out.push({ speaker: 'Narrator', text: line }); }\r\n      }\r\n      return out;\r\n    },\r\n\r\n    detectSpeakers: function () {\r\n      var turns = this.parseDialogue();\r\n      if (!turns.length) { this.showToast('Write some dialogue lines first.', 'error'); return; }\r\n      var names = [], seen = {};\r\n      for (var i = 0; i < turns.length; i++) {\r\n        if (!seen[turns[i].speaker]) { seen[turns[i].speaker] = 1; names.push(turns[i].speaker); }\r\n      }\r\n      var host = document.getElementById('pth-vs-cast');\r\n      host.innerHTML = '';\r\n      var palette = [0, 3, 6, 9, 12, 15];\r\n      for (var n = 0; n < names.length; n++) {\r\n        if (!this.cast[names[n]]) {\r\n          var vi = this.voices.length ? String(palette[n % palette.length] % this.voices.length) : '0';\r\n          this.cast[names[n]] = { voice: vi, rate: 1, pitch: 1 };\r\n        }\r\n        host.appendChild(this.buildCastRow(names[n]));\r\n      }\r\n      this.showToast(names.length + ' speaker(s) detected.', 'success');\r\n    },\r\n\r\n    buildCastRow: function (name) {\r\n      var cfg = this.cast[name];\r\n      var row = document.createElement('div');\r\n      row.className = 'pth-vs-speaker-row';\r\n\r\n      var nm = document.createElement('div');\r\n      nm.className = 'pth-vs-speaker-name';\r\n      nm.textContent = name;\r\n      row.appendChild(nm);\r\n\r\n      var sel = document.createElement('select');\r\n      sel.className = 'pth-vs-select';\r\n      sel.setAttribute('aria-label', 'Voice for ' + name);\r\n      for (var i = 0; i < this.voices.length; i++) {\r\n        var o = document.createElement('option');\r\n        o.value = String(i);\r\n        o.textContent = this.voices[i].name + ' (' + this.voices[i].lang + ')';\r\n        sel.appendChild(o);\r\n      }\r\n      sel.value = cfg.voice;\r\n      var self = this;\r\n      sel.onchange = function () { self.cast[name].voice = sel.value; };\r\n      row.appendChild(sel);\r\n\r\n      var mini = document.createElement('div');\r\n      mini.className = 'pth-vs-speaker-mini';\r\n      mini.appendChild(this.buildMini('Rate', 0.5, 2, cfg.rate, function (v) { self.cast[name].rate = v; }));\r\n      mini.appendChild(this.buildMini('Pitch', 0, 2, cfg.pitch, function (v) { self.cast[name].pitch = v; }));\r\n      row.appendChild(mini);\r\n      return row;\r\n    },\r\n\r\n    buildMini: function (label, min, max, val, cb) {\r\n      var wrap = document.createElement('div');\r\n      wrap.className = 'pth-vs-mini-cg';\r\n      var sp = document.createElement('span'); sp.textContent = label; wrap.appendChild(sp);\r\n      var r = document.createElement('input');\r\n      r.type = 'range'; r.min = String(min); r.max = String(max); r.step = '0.05'; r.value = String(val);\r\n      r.setAttribute('aria-label', label);\r\n      r.style.width = '80px';\r\n      r.oninput = function () { cb(parseFloat(r.value)); };\r\n      wrap.appendChild(r);\r\n      return wrap;\r\n    },\r\n\r\n    playDialogue: function () {\r\n      var turns = this.parseDialogue();\r\n      if (!turns.length) { this.showToast('Write some dialogue lines first.', 'error'); return; }\r\n      if (!this.cast || !this.objLen(this.cast)) { this.detectSpeakers(); }\r\n      this.stop();\r\n      var self = this;\r\n      var stage = document.getElementById('pth-vs-dlg-stage');\r\n      stage.innerHTML = '';\r\n      var rows = [];\r\n      for (var i = 0; i < turns.length; i++) {\r\n        var d = document.createElement('div');\r\n        d.className = 'pth-vs-word';\r\n        d.textContent = turns[i].speaker + ': ' + turns[i].text;\r\n        d.style.display = 'block';\r\n        d.style.marginBottom = '6px';\r\n        stage.appendChild(d);\r\n        rows.push(d);\r\n      }\r\n      var idx = 0;\r\n      function next() {\r\n        if (idx >= turns.length) { self.speaking = false; return; }\r\n        for (var k = 0; k < rows.length; k++) { rows[k].className = 'pth-vs-word'; }\r\n        rows[idx].className = 'pth-vs-word pth-vs-hl';\r\n        var turn = turns[idx];\r\n        var cfg = self.cast[turn.speaker] || { voice: '0', rate: 1, pitch: 1 };\r\n        var u = new SpeechSynthesisUtterance(self.applyLexicon(turn.text));\r\n        var voice = self.getVoice(cfg.voice);\r\n        if (voice) { u.voice = voice; u.lang = voice.lang; }\r\n        u.rate = cfg.rate; u.pitch = cfg.pitch;\r\n        u.onend = function () { idx++; next(); };\r\n        u.onerror = function () { idx++; next(); };\r\n        speechSynthesis.speak(u);\r\n      }\r\n      this.speaking = true;\r\n      next();\r\n    },\r\n\r\n    objLen: function (o) { var n = 0, k; for (k in o) { if (o.hasOwnProperty(k)) { n++; } } return n; },\r\n\r\n    \/* ---------- ssml \/ markup ---------- *\/\r\n    compileMarkup: function (text) {\r\n      var tokens = [];\r\n      var re = \/\\[(\\\/?)(pause|rate|pitch|emphasis)(?:\\s+([0-9.]+))?\\]\/gi;\r\n      var last = 0, m;\r\n      var stack = { rate: 1, pitch: 1, emphasis: false };\r\n      function pushText(str) {\r\n        str = str.replace(\/^\\s+|\\s+$\/g, '');\r\n        if (str) { tokens.push({ type: 'speak', text: str, rate: stack.rate * (stack.emphasis ? 1.02 : 1), pitch: stack.emphasis ? Math.min(stack.pitch + 0.25, 2) : stack.pitch }); }\r\n      }\r\n      while ((m = re.exec(text)) !== null) {\r\n        pushText(text.slice(last, m.index));\r\n        last = re.lastIndex;\r\n        var closing = m[1] === '\/', tag = m[2].toLowerCase(), num = m[3] ? parseFloat(m[3]) : null;\r\n        if (tag === 'pause') { tokens.push({ type: 'pause', ms: num || 400 }); }\r\n        else if (tag === 'rate') { stack.rate = closing ? 1 : (num || 1); }\r\n        else if (tag === 'pitch') { stack.pitch = closing ? 1 : (num || 1); }\r\n        else if (tag === 'emphasis') { stack.emphasis = !closing; }\r\n      }\r\n      pushText(text.slice(last));\r\n      return tokens;\r\n    },\r\n\r\n    playCompiled: function () {\r\n      var t = document.getElementById('pth-vs-ssml');\r\n      if (!t || !t.value.trim()) { this.showToast('Add some marked-up text first.', 'error'); return; }\r\n      this.stop();\r\n      var self = this;\r\n      var tokens = this.compileMarkup(t.value);\r\n      var voice = this.getVoice(document.getElementById('pth-vs-ssml-voice').value);\r\n      var idx = 0;\r\n      function next() {\r\n        if (idx >= tokens.length) { self.speaking = false; return; }\r\n        var tk = tokens[idx];\r\n        if (tk.type === 'pause') { idx++; setTimeout(next, tk.ms); return; }\r\n        var u = new SpeechSynthesisUtterance(tk.text);\r\n        if (voice) { u.voice = voice; u.lang = voice.lang; }\r\n        u.rate = tk.rate; u.pitch = tk.pitch;\r\n        u.onend = function () { idx++; next(); };\r\n        u.onerror = function () { idx++; next(); };\r\n        speechSynthesis.speak(u);\r\n      }\r\n      this.speaking = true;\r\n      next();\r\n    },\r\n\r\n    addLexRow: function () {\r\n      var host = document.getElementById('pth-vs-lex');\r\n      var row = document.createElement('div');\r\n      row.className = 'pth-vs-lex-row';\r\n      var a = document.createElement('input'); a.type = 'text'; a.className = 'pth-vs-text pth-vs-lex-from'; a.setAttribute('aria-label', 'Original word'); a.placeholder = 'Word';\r\n      var b = document.createElement('input'); b.type = 'text'; b.className = 'pth-vs-text pth-vs-lex-to'; b.setAttribute('aria-label', 'Spoken as'); b.placeholder = 'Spoken as';\r\n      var del = document.createElement('button'); del.type = 'button'; del.className = 'pth-vs-btn pth-vs-danger pth-vs-btn-sm'; del.setAttribute('data-action', 'lex-del'); del.textContent = '\\u2715';\r\n      row.appendChild(a); row.appendChild(b); row.appendChild(del);\r\n      host.appendChild(row);\r\n    },\r\n\r\n    delLexRow: function (el) {\r\n      var rows = this.root.querySelectorAll('#pth-vs-lex .pth-vs-lex-row');\r\n      if (rows.length <= 1) { this.showToast('Keep at least one lexicon row.', 'info'); return; }\r\n      var row = el;\r\n      while (row && row.className.indexOf('pth-vs-lex-row') === -1) { row = row.parentNode; }\r\n      if (row && row.parentNode) { row.parentNode.removeChild(row); }\r\n    },\r\n\r\n    \/* ---------- subtitles ---------- *\/\r\n    captureTimings: function () {\r\n      var t = document.getElementById('pth-vs-text');\r\n      if (!t || !t.value.trim()) { this.showToast('Add text on the Narrate tab first.', 'error'); return; }\r\n      this.stop();\r\n      var self = this;\r\n      var raw = this.applyLexicon(t.value.trim());\r\n      var rate = parseFloat(document.getElementById('pth-vs-rate').value);\r\n      var pitch = parseFloat(document.getElementById('pth-vs-pitch').value);\r\n      var vol = parseFloat(document.getElementById('pth-vs-vol').value);\r\n      var voice = this.getVoice(document.getElementById('pth-vs-voice').value);\r\n\r\n      var words = [];\r\n      var re = \/\\S+\/g, m;\r\n      while ((m = re.exec(raw)) !== null) { words.push({ text: m[0], index: m.index, start: -1 }); }\r\n      if (!words.length) { this.showToast('No words to time.', 'error'); return; }\r\n\r\n      var note = document.getElementById('pth-vs-sub-note');\r\n      note.style.display = 'block';\r\n      note.innerHTML = '<b>Capturing&hellip;<\/b> Let the narration play through once so the timings can be recorded.';\r\n\r\n      var u = new SpeechSynthesisUtterance(raw);\r\n      if (voice) { u.voice = voice; u.lang = voice.lang; }\r\n      u.rate = rate; u.pitch = pitch; u.volume = vol;\r\n      var t0 = 0, boundaryFired = false;\r\n      u.onstart = function () { t0 = Date.now(); };\r\n      u.onboundary = function (ev) {\r\n        if (ev.charIndex === undefined) { return; }\r\n        boundaryFired = true;\r\n        var elapsed = (typeof ev.elapsedTime === 'number' && ev.elapsedTime > 0) ? ev.elapsedTime : (Date.now() - t0);\r\n        if (elapsed > 1000000) { elapsed = elapsed \/ 1000; } \/* some engines report seconds *\/\r\n        for (var i = 0; i < words.length; i++) {\r\n          if (words[i].index === ev.charIndex && words[i].start < 0) { words[i].start = elapsed; break; }\r\n        }\r\n      };\r\n      u.onend = function () {\r\n        self.speaking = false;\r\n        self.finalizeCues(words, boundaryFired, rate);\r\n      };\r\n      u.onerror = function () { self.speaking = false; note.innerHTML = 'Capture failed. Try Chrome or Edge for word-level timing.'; };\r\n      this.speaking = true;\r\n      speechSynthesis.speak(u);\r\n    },\r\n\r\n    finalizeCues: function (words, boundaryFired, rate) {\r\n      var i;\r\n      if (!boundaryFired) {\r\n        \/* Fallback: estimate from characters when the engine has no word boundaries (e.g. Safari). *\/\r\n        var perChar = 60 \/ (rate || 1);\r\n        var acc = 0;\r\n        for (i = 0; i < words.length; i++) { words[i].start = acc; acc += words[i].text.length * perChar + 90; }\r\n      } else {\r\n        var lastKnown = 0;\r\n        for (i = 0; i < words.length; i++) {\r\n          if (words[i].start < 0) { words[i].start = lastKnown + 200; }\r\n          lastKnown = words[i].start;\r\n        }\r\n      }\r\n      for (i = 0; i < words.length; i++) {\r\n        words[i].end = (i < words.length - 1) ? words[i + 1].start : words[i].start + 600;\r\n      }\r\n\r\n      var perLine = parseInt(document.getElementById('pth-vs-sub-words').value, 10);\r\n      var cues = [];\r\n      if (perLine === 0) {\r\n        var buf = [], startT = null;\r\n        for (i = 0; i < words.length; i++) {\r\n          if (startT === null) { startT = words[i].start; }\r\n          buf.push(words[i].text);\r\n          if (\/[.!?]$\/.test(words[i].text) || i === words.length - 1) {\r\n            cues.push({ start: startT, end: words[i].end, text: buf.join(' ') });\r\n            buf = []; startT = null;\r\n          }\r\n        }\r\n      } else {\r\n        for (i = 0; i < words.length; i += perLine) {\r\n          var slice = words.slice(i, i + perLine);\r\n          var txt = [];\r\n          for (var j = 0; j < slice.length; j++) { txt.push(slice[j].text); }\r\n          cues.push({ start: slice[0].start, end: slice[slice.length - 1].end, text: txt.join(' ') });\r\n        }\r\n      }\r\n      this.subtitleCues = cues;\r\n      var note = document.getElementById('pth-vs-sub-note');\r\n      note.innerHTML = '<b>Done.<\/b> Built ' + cues.length + ' caption line(s)' + (boundaryFired ? '' : ' using estimated timing (your browser did not report word boundaries)') + '.';\r\n      this.renderSubtitles(this.subtitleFormat);\r\n      this.showToast('Subtitles ready.', 'success');\r\n    },\r\n\r\n    fmtTime: function (sec, comma) {\r\n      if (sec < 0) { sec = 0; }\r\n      var ms = Math.floor((sec - Math.floor(sec)) * 1000);\r\n      var s = Math.floor(sec) % 60;\r\n      var mnt = Math.floor(sec \/ 60) % 60;\r\n      var hr = Math.floor(sec \/ 3600);\r\n      function pad(n, w) { n = String(n); while (n.length < w) { n = '0' + n; } return n; }\r\n      return pad(hr, 2) + ':' + pad(mnt, 2) + ':' + pad(s, 2) + (comma ? ',' : '.') + pad(ms, 3);\r\n    },\r\n\r\n    buildSubtitleText: function (fmt) {\r\n      var cues = this.subtitleCues, out = [], i;\r\n      if (!cues.length) { return ''; }\r\n      if (fmt === 'vtt') {\r\n        out.push('WEBVTT'); out.push('');\r\n        for (i = 0; i < cues.length; i++) {\r\n          out.push(this.fmtTime(cues[i].start \/ 1000, false) + ' --> ' + this.fmtTime(cues[i].end \/ 1000, false));\r\n          out.push(cues[i].text); out.push('');\r\n        }\r\n      } else if (fmt === 'txt') {\r\n        for (i = 0; i < cues.length; i++) { out.push(cues[i].text); }\r\n      } else {\r\n        for (i = 0; i < cues.length; i++) {\r\n          out.push(String(i + 1));\r\n          out.push(this.fmtTime(cues[i].start \/ 1000, true) + ' --> ' + this.fmtTime(cues[i].end \/ 1000, true));\r\n          out.push(cues[i].text); out.push('');\r\n        }\r\n      }\r\n      return out.join('\\n');\r\n    },\r\n\r\n    renderSubtitles: function (fmt) {\r\n      this.subtitleFormat = fmt || 'srt';\r\n      var out = document.getElementById('pth-vs-sub-out');\r\n      if (!this.subtitleCues.length) { out.textContent = 'Capture timings first, then choose a format.'; return; }\r\n      out.textContent = this.buildSubtitleText(this.subtitleFormat);\r\n    },\r\n\r\n    copySubtitles: function () {\r\n      if (!this.subtitleCues.length) { this.showToast('Nothing to copy yet.', 'error'); return; }\r\n      var txt = this.buildSubtitleText(this.subtitleFormat);\r\n      var self = this;\r\n      if (navigator.clipboard && navigator.clipboard.writeText) {\r\n        navigator.clipboard.writeText(txt).then(function () { self.showToast('Copied to clipboard.', 'success'); })['catch'](function () { self.showToast('Copy blocked by the browser.', 'error'); });\r\n      } else { this.showToast('Select the text and copy manually.', 'info'); }\r\n    },\r\n\r\n    downloadSubtitles: function () {\r\n      if (!this.subtitleCues.length) { this.showToast('Capture timings first.', 'error'); return; }\r\n      var ext = this.subtitleFormat;\r\n      var txt = this.buildSubtitleText(this.subtitleFormat);\r\n      var blob = new Blob([txt], { type: 'text\/plain' });\r\n      var url = URL.createObjectURL(blob);\r\n      var a = document.createElement('a');\r\n      a.href = url; a.download = 'narration-subtitles.' + ext;\r\n      document.body.appendChild(a); a.click();\r\n      document.body.removeChild(a);\r\n      setTimeout(function () { URL.revokeObjectURL(url); }, 1500);\r\n      this.showToast('Downloaded .' + ext + ' file.', 'success');\r\n    },\r\n\r\n    \/* ---------- samples ---------- *\/\r\n    loadSample: function () {\r\n      var el = document.getElementById('pth-vs-text');\r\n      el.value = 'Welcome to the AI Voice and Narration Studio. This tool turns written text into natural speech right inside your browser, with no upload and no account. Adjust the speed and pitch to match your style, then press Speak to hear it read aloud.';\r\n      this.updateCounts();\r\n    },\r\n    dialogueSample: function () {\r\n      document.getElementById('pth-vs-dlg').value = 'Narrator: The lab was silent except for a single blinking light.\\nAlice: Did you leave that terminal running again?\\nBob: I swear I shut it down before we left.\\nNarrator: Neither of them noticed the door slowly opening behind them.';\r\n    },\r\n    ssmlSample: function () {\r\n      document.getElementById('pth-vs-ssml').value = 'Here is an important announcement. [pause 500] This next part is [emphasis]absolutely essential[\/emphasis]. [pause 300] [rate 0.8]And now I will slow right down so every word lands clearly.[\/rate]';\r\n    },\r\n\r\n    \/* ---------- toast ---------- *\/\r\n    showToast: function (msg, type) {\r\n      var t = document.getElementById('pth-vs-toast');\r\n      if (!t) { return; }\r\n      var colors = { success: '#059669', error: '#dc2626', info: '#2563eb' };\r\n      t.style.background = colors[type] || colors.info;\r\n      t.textContent = msg;\r\n      t.className = 'pth-vs-toast pth-vs-show';\r\n      if (this._toastTimer) { clearTimeout(this._toastTimer); }\r\n      this._toastTimer = setTimeout(function () { t.className = 'pth-vs-toast'; }, 3000);\r\n    }\r\n  };\r\n\r\n  window.pthVoiceStudioApp = App;\r\n\r\n  function runInit() {\r\n    if (!window.pthVoiceStudioApp.initialized && document.getElementById('pth-voicestudio-wrapper')) {\r\n      window.pthVoiceStudioApp.initialized = true;\r\n      window.pthVoiceStudioApp.init();\r\n    }\r\n  }\r\n\r\n  \/* Layer 1: immediate *\/\r\n  runInit();\r\n  \/* Layer 2: DOMContentLoaded *\/\r\n  if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', runInit); }\r\n  \/* Layer 3: window load + 50ms *\/\r\n  window.addEventListener('load', function () { setTimeout(runInit, 50); });\r\n  \/* Layer 4: MutationObserver with auto-disconnect *\/\r\n  if (!window.pthVoiceStudioApp.initialized && typeof MutationObserver !== 'undefined') {\r\n    var pthObs = new MutationObserver(function () {\r\n      if (document.getElementById('pth-voicestudio-wrapper')) { pthObs.disconnect(); runInit(); }\r\n    });\r\n    pthObs.observe(document.body || document.documentElement, { childList: true, subtree: true });\r\n    setTimeout(function () { pthObs.disconnect(); }, 10000);\r\n  }\r\n})();\r\n<\/script>\n\n\n\n\n<style>\n.pth-container{max-width:1480px;margin:0 auto;padding:0 20px;box-sizing:border-box}\n.pth-feature-wrap{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-bottom:40px}\n.pth-feat-box{border:1px solid #e2e8f0;background:#fff;padding:24px;border-radius:12px;box-shadow:0 4px 6px -1px rgba(0,0,0,0.02)}\n.pth-feat-title{font-size:1.05rem;font-weight:800;color:#0f172a;margin:0 0 10px;display:flex;align-items:center;gap:10px}\n.pth-feat-text{font-size:.9rem;color:#1e293b;line-height:1.6;margin:0;font-weight:500}\n.pth-steps-wrap{border:1px solid #e2e8f0;background:#fff;padding:40px 30px;border-radius:16px;text-align:center;box-shadow:0 4px 6px -1px rgba(0,0,0,0.02);margin-bottom:40px;width:100%;box-sizing:border-box}\n.pth-steps-title{font-size:1.2rem;font-weight:800;color:#0f172a;text-transform:uppercase;margin:0 0 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:#2563eb;color:#fff;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(37,99,235,.3)}\n.pth-step-name{font-weight:800;color:#0f172a;margin:0 0 8px;font-size:1rem}\n.pth-step-desc{font-size:.9rem;color:#1e293b;line-height:1.5;margin:0;font-weight:500}\n<\/style>\n<div class=\"pth-container\">\n  <div class=\"pth-feature-wrap\">\n    <div class=\"pth-feat-box\">\n      <p class=\"pth-feat-title\"><span aria-hidden=\"true\">&#128512;<\/span> Multi-Speaker Dialogue<\/p>\n      <p class=\"pth-feat-text\">Write your script as <em>Speaker: line<\/em> and give each name its own voice, speed, and pitch. A two-person conversation or a narrated scene then plays back as separate characters instead of one flat read.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n      <p class=\"pth-feat-title\"><span aria-hidden=\"true\">&#128221;<\/span> Timed Subtitle Capture<\/p>\n      <p class=\"pth-feat-text\">As the studio reads your text, it records the real word timings and builds SRT, WebVTT, or plain-text captions. Drop the file straight onto a video track and the words line up with the narration.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n      <p class=\"pth-feat-title\"><span aria-hidden=\"true\">&#127908;<\/span> Pronunciation &amp; Delivery Control<\/p>\n      <p class=\"pth-feat-text\">A small lexicon respells names the voice keeps getting wrong (type <em>Ruwan<\/em>, hear <em>Roo-wun<\/em>). Bracket tags such as [pause 500] and [emphasis] shape where it breaks and what it stresses.<\/p>\n    <\/div>\n  <\/div>\n  <div class=\"pth-steps-wrap\">\n    <p class=\"pth-steps-title\">How to Use the Voice &amp; Narration Studio<\/p>\n    <div class=\"pth-steps-grid\">\n      <div class=\"pth-step-item\">\n        <div class=\"pth-step-num\" aria-hidden=\"true\">1<\/div>\n        <p class=\"pth-step-name\">Add your text<\/p>\n        <p class=\"pth-step-desc\">Type or paste into the Narrate box, or press Load Sample to see the format.<\/p>\n      <\/div>\n      <div class=\"pth-step-item\">\n        <div class=\"pth-step-num\" aria-hidden=\"true\">2<\/div>\n        <p class=\"pth-step-name\">Pick a voice<\/p>\n        <p class=\"pth-step-desc\">Choose any voice installed on your device, then set speed, pitch, and volume.<\/p>\n      <\/div>\n      <div class=\"pth-step-item\">\n        <div class=\"pth-step-num\" aria-hidden=\"true\">3<\/div>\n        <p class=\"pth-step-name\">Press Speak<\/p>\n        <p class=\"pth-step-desc\">The current word highlights as it plays. Pause, resume, or stop whenever you like.<\/p>\n      <\/div>\n      <div class=\"pth-step-item\">\n        <div class=\"pth-step-num\" aria-hidden=\"true\">4<\/div>\n        <p class=\"pth-step-name\">Export captions<\/p>\n        <p class=\"pth-step-desc\">Open Subtitles &amp; Export, press Capture Timings, then download SRT, VTT, or TXT.<\/p>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n \n\n\n\n<p class=\"wp-block-paragraph\">Last updated: August 2026<\/p>\n\n\n\n<h2 id=\"\ud83d\udd34-what-this-narration-studio-is-really-for\" class=\"wp-block-heading\">\ud83d\udd34&nbsp;<strong>What this narration studio is really for<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most people reach an offline text to speech generator for one of four jobs: turning lesson notes into a voiceover, adding a spoken track to a slideshow or faceless video, proofreading their own writing by ear, or making a short piece of dialogue for a story or animation. This tool is built around those jobs rather than around a giant wall of settings.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because everything runs on the speech voices already installed in your operating system, there is no sign-up, no credit limit, and nothing to install. You paste text, choose a voice, and press Speak. The catch that surprises people is covered honestly further down, so you know exactly what to expect before you build a workflow around it.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe1-a-real-workflow-script-to-captioned-voiceover\" class=\"wp-block-heading\">\ud83d\udfe1&nbsp;<strong>A real workflow: script to captioned voiceover<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Say you are recording a two-minute explainer. Start in the Narrate tab with your plain script and pick a voice you like at about 0.95x speed, which reads a touch slower than default and lands more clearly for tutorials. Preview it, adjust pitch if the voice sounds too high, then move to the&nbsp;<strong>Subtitles &amp; Export<\/strong>&nbsp;tab and press Capture Timings. The studio reads the script once, records where each word actually falls, and hands you an SRT or WebVTT file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From there the subtitle file goes onto your video track. If you are assembling the visuals in the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/turbo-video-editor\/\">Video Studio<\/a>, the captions drop in already timed to the same pace you previewed. Going the other direction \u2014 you already have a recording and need the text \u2014 is a different job handled by the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/ai-offline-transcription\/\">offline transcription studio<\/a>, which listens to audio and writes the words out.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a scene with more than one character, switch to the Dialogue tab. Write&nbsp;<em>Narrator:<\/em>,&nbsp;<em>Alice:<\/em>, and&nbsp;<em>Bob:<\/em>&nbsp;on their own lines, press Detect Speakers, and assign each a distinct voice. Playback then moves speaker by speaker, highlighting the active line, so a conversation sounds like a cast instead of a single narrator reading name tags aloud.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe2-where-the-browser-voice-engine-stops\" class=\"wp-block-heading\">\ud83d\udfe2&nbsp;<strong>Where the browser voice engine stops<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the honest part. A browser can play a system voice out loud, but it cannot hand you that audio as a saved file \u2014 there is no way to record the speech engine into an MP3 or WAV from inside the page. That is a limit of every browser, not this tool, so any site promising a one-click &#8220;download system voice as MP3&#8221; is quietly recording your screen or routing your text to a cloud server. This studio does neither. What it gives you instead is the timed subtitle file, which is the part a video editor genuinely needs, plus live playback you can capture with the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/free-all-in-one-audio-editor-converter\/\">audio editor<\/a>&nbsp;if you must have a clip.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two smaller things trip people up. First, voice quality is set by your device, not the tool: Chrome and Edge on Windows expose more natural voices than a bare Linux install, so if a voice sounds robotic, add better ones from your operating system speech settings. Second, word-level timing relies on a browser feature that Chrome and Edge support but Safari does not, so on Safari the studio falls back to an estimated timing that is close but not frame-perfect. Capturing on the same voice and speed you plan to publish with keeps the captions honest. If you want the reasoning behind keeping tools like this fully client-side, the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/secure-offline-web-development-utilities-guide\/\">offline utilities guide<\/a>&nbsp;lays it out, and the underlying mechanics live in&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/SpeechSynthesis\" target=\"_blank\" rel=\"noreferrer noopener\">MDN&#8217;s Web Speech API reference<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Want the theory rather than the how-to? The companion piece on&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/how-text-to-speech-works\/\">how text-to-speech works<\/a>&nbsp;covers voices, SSML, and word timing in depth.<\/p>\n\n\n\n<style>\n.pth-faq-wrap{max-width:1480px;margin:0 auto;padding:0 20px;box-sizing:border-box}\n.pth-faq-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}\n.pth-faq-card{border:1px solid #e2e8f0;background:#fff;border-radius:12px;padding:18px}\n.pth-faq-q{font-weight:800;color:#0f172a;margin:0 0 8px;font-size:1rem}\n.pth-faq-a{color:#1e293b;font-weight:500;line-height:1.6;margin:0;font-size:.92rem}\n@media(max-width:1024px){.pth-faq-grid{grid-template-columns:repeat(2,1fr)}}\n@media(max-width:640px){.pth-faq-grid{grid-template-columns:1fr}}\n<\/style>\n<div class=\"pth-faq-wrap\">\n  <div class=\"pth-faq-grid\">\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Is it really free and offline?<\/p>\n      <p class=\"pth-faq-a\">Yes. It uses the speech voices already on your device, so there is no account, no quota, and no text sent to a server. After the page loads you can even switch off your connection and it keeps working.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Can I download the audio as an MP3?<\/p>\n      <p class=\"pth-faq-a\">No, and it is worth knowing why: browsers do not let a page capture the speech engine to a file. You can export the timed subtitles here, then record the live playback with a separate audio tool if you need an actual clip.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">A name is pronounced wrong. How do I fix it?<\/p>\n      <p class=\"pth-faq-a\">Open the SSML &amp; Lexicon tab and add a rule: the original spelling on the left, a phonetic respelling on the right. It is applied before speaking, across every tab.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Which browser gives the best voices?<\/p>\n      <p class=\"pth-faq-a\">Chrome and Edge usually expose the most natural voices, and they also support the word-timing feature used for subtitles. You can add more voices from your operating system&#8217;s speech settings.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">How do I make two characters sound different?<\/p>\n      <p class=\"pth-faq-a\">Use the Dialogue tab. Write each line as <em>Speaker: text<\/em>, press Detect Speakers, and pick a separate voice, speed, and pitch for each name. Playback then runs line by line.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Will the subtitle timings match my video exactly?<\/p>\n      <p class=\"pth-faq-a\">They match the voice and speed you captured with. Set your final speed first, then press Capture Timings. On Safari the timing is estimated rather than exact, so Chrome or Edge is the safer choice for tight captions.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Is there a text length limit?<\/p>\n      <p class=\"pth-faq-a\">There is no hard cap, but very long blocks are easier to manage split into paragraphs or chapters. Long scripts also take longer to read through when capturing timings, since it plays the whole thing once.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Does it work on a phone?<\/p>\n      <p class=\"pth-faq-a\">Yes. Playback and dialogue work on mobile using the phone&#8217;s built-in voices. Capturing long subtitle files is more comfortable on a laptop, but short scripts are fine on a phone.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">What are the bracket tags like [pause 500] for?<\/p>\n      <p class=\"pth-faq-a\">They shape delivery in the SSML tab. [pause 500] inserts a half-second break, and [emphasis]word[\/emphasis] lifts a word so it stands out. It is a simple stand-in for full SSML markup.<\/p>\n    <\/div>\n  <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Next-Gen Text to Speech Studio | Lifelike Emotions, Custom Voice Cloning &amp; Multi-Language Synthesis The most Advanced Text to Speech Generator with emotional voices, multi-language support (Sinhala, English, Tamil, Hindi), voice cloning, SSML editor, background music mixing and batch export. 100% offline &amp; free. \u00a0 &#128512; Multi-Speaker Dialogue Write your script as Speaker: line and &#8230; <a title=\"Ultimate Advanced Text to Speech Generator \u2013 Emotional Voices, Voice Cloning, SSML &amp; Multi-Language\" class=\"read-more\" href=\"https:\/\/schoolict.net\/tools\/advanced-text-to-speech-generator\/\" aria-label=\"Read more about Ultimate Advanced Text to Speech Generator \u2013 Emotional Voices, Voice Cloning, SSML &amp; Multi-Language\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":2815,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-2810","page","type-page","status-publish","has-post-thumbnail"],"_links":{"self":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/2810","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=2810"}],"version-history":[{"count":4,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/2810\/revisions"}],"predecessor-version":[{"id":6970,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/2810\/revisions\/6970"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media\/2815"}],"wp:attachment":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media?parent=2810"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}