{"id":7063,"date":"2026-08-15T05:57:24","date_gmt":"2026-08-15T05:57:24","guid":{"rendered":"https:\/\/primetoolhub.com\/?page_id=7063"},"modified":"2026-10-05T07:52:39","modified_gmt":"2026-10-05T07:52:39","slug":"ai-storyboard-generator","status":"publish","type":"page","link":"https:\/\/schoolict.net\/tools\/ai-storyboard-generator\/","title":{"rendered":"AI Storyboard Generator"},"content":{"rendered":"<div class=\"pth-hero-section\">\n<div class=\"pth-hero-content\">\n<h2>AI Story-to-Storyboard Generator<\/h2>\n<p>Turn a full story or script into sequential, shot-by-shot prompts for AI video tools. Keeps characters consistent across every scene with a running Character Bible, extracts voiceover, builds a master shot list\n, and generates SEO. Bring your own key (Gemini, OpenAI or Claude).<\/p>\n<div id=\"pth-toc-placeholder\"><\/div>\n<\/p>\n<\/div>\n<div class=\"pth-hero-image\">\n    <img data-no-lazy=\"1\"\n         src=\"https:\/\/schoolict.net\/wp-content\/uploads\/2026\/08\/AI-Story-to-Storyboard-Generator-800x447.jpeg\"\n         width=\"800\"\n         height=\"447\"\n         alt=\"AI Story-to-Storyboard Generator\"\n         fetchpriority=\"high\"\n         loading=\"eager\"\n         decoding=\"async\"\n         style=\"width:100%; height:auto; display:block;\">\n  <\/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-the-real-problem-your-character-changes-every-shot\">\ud83d\udd34\u00a0The Real Problem: Your Character Changes Every Shot<\/a><\/li><li><a href=\"#\ud83d\udfe1-why-split-a-story-into-parts-at-all\">\ud83d\udfe1\u00a0Why Split a Story Into Parts At All?<\/a><\/li><li><a href=\"#\ud83d\udd34-an-honest-word-this-tool-sends-your-story-to-an-ai\">\ud83d\udd34\u00a0An Honest Word: This Tool Sends Your Story to an AI<\/a><ul><li><a href=\"#\ud83d\udfe2-bring-your-own-key-pick-your-provider\">\ud83d\udfe2\u00a0Bring Your Own Key, Pick Your Provider<\/a><\/li><\/ul><\/li><li><a href=\"#\ud83d\udfe1-built-with-sinhala-horror-creators-in-mind\">\ud83d\udfe1\u00a0Built With Sinhala Horror Creators in Mind<\/a><\/li><li><a href=\"#\ud83d\udfe2-everything-the-upload-needs-not-just-the-visuals\">\ud83d\udfe2\u00a0Everything the Upload Needs, Not Just the Visuals<\/a><\/li><\/ul><\/nav><\/div>\n\n\n<!-- noptimize -->\r\n<style>\r\n#pth-lang-modal {\r\n    position: fixed;\r\n    top: 0;\r\n    left: 0;\r\n    right: 0;\r\n    bottom: 0;\r\n    width: 100% !important;\r\n    height: 100% !important;\r\n    display: none;\r\n    align-items: center;\r\n    justify-content: center;\r\n    background: rgba(15,23,42,0.6);\r\n    z-index: 999999;\r\n    backdrop-filter: blur(3px);\r\n    margin: 0 !important;\r\n    padding: 0 !important;\r\n}\r\n\r\n#pth-lang-modal.show {\r\n    display: flex !important;\r\n}\r\n\r\n.pth-lang-box {\r\n    background: #fff;\r\n    padding: 24px;\r\n    border-radius: 16px;\r\n    width: 90%;\r\n    max-width: 650px;\r\n    max-height: 85vh;\r\n    display: flex;\r\n    flex-direction: column;\r\n    box-shadow: 0 10px 25px rgba(0,0,0,0.15);\r\n    margin: auto !important;   \/* \u0db8\u0db0\u0dca\u200d\u0dba\u0dba\u0da7 \u0d9c\u0dd9\u0db1 \u0d91\u0dba\u0dd2 *\/\r\n    position: relative;\r\n}\r\n#pth-storyboard-wrapper.pth-sb{--pth-ink:#1e293b;--pth-muted:#475569;--pth-line:#cbd5e1;--pth-bg:#ffffff;--pth-soft:#f1f5f9;--pth-primary:#1d4ed8;--pth-primary-ink:#ffffff;--pth-accent:#7c3aed;--pth-ok:#15803d;--pth-err:#b91c1c;max-width:100%;margin:0 auto;padding:18px;color:var(--pth-ink);font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,Arial,sans-serif;font-size:16px;line-height:1.55;background:var(--pth-bg);opacity:1;}\r\n#pth-storyboard-wrapper.pth-sb *{box-sizing:border-box;min-width:0;}\r\n#pth-storyboard-wrapper .pth-sb-h2{font-size:1.55rem;line-height:1.25;margin:0 0 6px;color:var(--pth-ink);display:flex;align-items:center;gap:8px;flex-wrap:wrap;}\r\n#pth-storyboard-wrapper .pth-sb-badges{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 8px;}\r\n#pth-storyboard-wrapper .pth-version-badge{background:var(--pth-primary);color:#fff;font-size:.72rem;font-weight:700;padding:3px 9px;border-radius:999px;}\r\n#pth-storyboard-wrapper .pth-status-offline{background:#ecfdf5;color:var(--pth-ok);border:1px solid #a7f3d0;font-size:.72rem;font-weight:700;padding:3px 9px;border-radius:999px;}\r\n#pth-storyboard-wrapper .pth-sb-desc{color:var(--pth-muted);margin:0 0 14px;font-size:.97rem;}\r\n#pth-storyboard-wrapper .pth-sb-toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 16px;}\r\n#pth-storyboard-wrapper .pth-sb-btn{font:inherit;font-weight:600;font-size:.9rem;padding:9px 14px;border-radius:9px;border:1px solid var(--pth-line);background:var(--pth-soft);color:var(--pth-ink);cursor:pointer;}\r\n#pth-storyboard-wrapper .pth-sb-btn:hover{background:#e2e8f0;}\r\n#pth-storyboard-wrapper .pth-sb-btn:disabled{opacity:.6;cursor:default;}\r\n#pth-storyboard-wrapper .pth-sb-primary{background:var(--pth-primary);color:var(--pth-primary-ink);border-color:var(--pth-primary);}\r\n#pth-storyboard-wrapper .pth-sb-primary:hover{background:#1e40af;}\r\n#pth-storyboard-wrapper .pth-sb-grid{display:grid;grid-template-columns:1.8fr 1fr;gap:18px;align-items:start;}\r\n#pth-storyboard-wrapper.pth-sb-full{position:fixed;top:0;right:0;bottom:0;left:0;z-index:99998;max-width:100%;width:100%;height:100%;margin:0;padding:24px;overflow:auto;-webkit-overflow-scrolling:touch;background:var(--pth-bg);}\r\n#pth-storyboard-wrapper.pth-sb-full .pth-sb-side{display:none;}\r\n#pth-storyboard-wrapper.pth-sb-full .pth-sb-grid{grid-template-columns:1fr;}\r\nbody.pth-sb-locked{overflow:hidden;}\r\n#pth-storyboard-wrapper .pth-sb-label{display:block;font-weight:700;font-size:.86rem;margin:14px 0 5px;color:var(--pth-ink);}\r\n#pth-storyboard-wrapper .pth-sb-label:first-child{margin-top:0;}\r\n#pth-storyboard-wrapper textarea,#pth-storyboard-wrapper input,#pth-storyboard-wrapper select{font:inherit;width:100%;padding:10px 11px;border:1px solid var(--pth-line);border-radius:9px;background:#fff;color:var(--pth-ink);}\r\n#pth-storyboard-wrapper #pth-sb-story{min-height:200px;resize:vertical;}\r\n#pth-storyboard-wrapper #pth-sb-md{min-height:150px;resize:vertical;font-size:.82rem;font-family:ui-monospace,Menlo,Consolas,monospace;}\r\n#pth-storyboard-wrapper #pth-sb-context{min-height:80px;resize:vertical;font-size:.85rem;}\r\n#pth-storyboard-wrapper #pth-sb-custom{min-height:64px;resize:vertical;}\r\n#pth-storyboard-wrapper #pth-sb-charreq{min-height:64px;resize:vertical;}\r\n#pth-storyboard-wrapper .pth-sb-side{background:var(--pth-soft);border:1px solid var(--pth-line);border-radius:12px;padding:14px;}\r\n#pth-storyboard-wrapper .pth-sb-hint{font-size:.78rem;color:var(--pth-muted);margin:5px 0 0;}\r\n#pth-storyboard-wrapper .pth-sb-keyrow{display:flex;gap:8px;}\r\n#pth-storyboard-wrapper .pth-sb-keyrow input{flex:1;}\r\n#pth-storyboard-wrapper .pth-sb-output{margin-top:16px;min-height:40px;}\r\n#pth-storyboard-wrapper .pth-sb-partbar{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 12px;padding:10px 12px;background:var(--pth-soft);border:1px solid var(--pth-line);border-radius:10px;}\r\n#pth-storyboard-wrapper .pth-sb-partinfo{font-weight:600;font-size:.9rem;}\r\n#pth-storyboard-wrapper .pth-sb-card{border:1px solid var(--pth-line);border-radius:12px;margin:0 0 14px;overflow:hidden;background:#fff;}\r\n#pth-storyboard-wrapper .pth-sb-cardhead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;background:#f8fafc;border-bottom:1px solid var(--pth-line);}\r\n#pth-storyboard-wrapper .pth-sb-cardtitle{font-weight:700;}\r\n#pth-storyboard-wrapper .pth-sb-cardbody{display:grid;grid-template-columns:var(--pth-col-left,1fr) var(--pth-col-right,1fr);}\r\n#pth-storyboard-wrapper .pth-sb-colctl{display:flex;align-items:center;gap:10px;margin:0 0 12px;font-size:.85rem;color:var(--pth-muted);}\r\n#pth-storyboard-wrapper .pth-sb-colctl input[type=range]{flex:1;max-width:280px;width:auto;padding:0;}\r\n#pth-storyboard-wrapper .pth-sb-source{padding:14px;border-right:1px solid var(--pth-line);background:#f8fafc;}\r\n#pth-storyboard-wrapper .pth-sb-outcol{padding:14px;}\r\n#pth-storyboard-wrapper .pth-sb-collabel{font-size:.68rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--pth-muted);margin:0 0 6px;}\r\n#pth-storyboard-wrapper .pth-sb-srctext{white-space:pre-wrap;word-wrap:break-word;font-size:var(--pth-font,0.82rem);line-height:1.5;color:var(--pth-muted);}\r\n#pth-storyboard-wrapper .pth-sb-srcedit{width:100%;min-height:120px;resize:vertical;white-space:pre-wrap;word-wrap:break-word;font-size:var(--pth-font,0.82rem);line-height:1.5;color:var(--pth-ink);background:#fff;border:1px solid var(--pth-line);border-radius:8px;padding:8px 9px;font-family:inherit;}\r\n#pth-storyboard-wrapper .pth-sb-shotrow{display:flex;align-items:center;gap:8px;margin:0 0 8px;flex-wrap:wrap;}\r\n#pth-storyboard-wrapper .pth-sb-shotlabel{font-size:.72rem;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--pth-muted);white-space:nowrap;}\r\n#pth-storyboard-wrapper .pth-sb-shotsel{flex:1;min-width:150px;width:auto;font-size:.82rem;padding:6px 8px;}\r\n#pth-storyboard-wrapper .pth-sb-cardbody .pth-sb-pre{padding:0;}\r\n#pth-storyboard-wrapper .pth-sb-tools{margin-top:20px;padding:14px;border:1px solid var(--pth-line);border-radius:12px;background:#f8fafc;}\r\n#pth-storyboard-wrapper .pth-sb-tools select,#pth-storyboard-wrapper .pth-sb-tools input{width:auto;flex:1;min-width:140px;}\r\n#pth-storyboard-wrapper .pth-sb-tools .pth-sb-toolbar{align-items:center;}\r\n#pth-storyboard-wrapper .pth-sb-collapsed .pth-sb-cardbody,#pth-storyboard-wrapper .pth-sb-collapsed .pth-sb-pre{display:none;}\r\n#pth-storyboard-wrapper .pth-sb-mark{background:#fde047;color:#1e293b;padding:0 1px;border-radius:2px;}\r\n#pth-storyboard-wrapper .pth-sb-pre,#pth-storyboard-wrapper .pth-sb-srctext{font-size:var(--pth-font,0.85rem);}\r\n#pth-storyboard-wrapper .pth-sb-fontctl{display:inline-flex;align-items:center;gap:6px;margin-left:auto;}\r\n#pth-storyboard-wrapper .pth-sb-progress{height:3px;background:#e2e8f0;overflow:hidden;display:none;}\r\n#pth-storyboard-wrapper .pth-sb-loading .pth-sb-progress{display:block;}\r\n#pth-storyboard-wrapper .pth-sb-progress-inner{height:100%;width:40%;background:var(--pth-primary);animation:pth-sb-slide 1.1s ease-in-out infinite;}\r\n@keyframes pth-sb-slide{0%{margin-left:-40%}100%{margin-left:100%}}\r\n#pth-storyboard-wrapper .pth-sb-stat{font-size:.8rem;color:var(--pth-muted);margin-right:auto;}\r\n#pth-storyboard-wrapper .pth-sb-pre{margin:0;padding:14px;white-space:pre-wrap;word-wrap:break-word;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:var(--pth-font,0.85rem);line-height:1.5;color:var(--pth-ink);}\r\n#pth-storyboard-wrapper .pth-sb-toast{position:fixed;right:18px;bottom:18px;background:#0f172a;color:#fff;padding:10px 15px;border-radius:9px;font-size:.88rem;font-weight:600;max-width:320px;transform:translateY(140%);transition:transform .25s ease;z-index:99999;}\r\n#pth-storyboard-wrapper .pth-sb-toast.pth-sb-show{transform:translateY(0);}\r\n#pth-storyboard-wrapper .pth-sb-modelfix{margin:0 12px 12px;padding:12px;background:#fff7ed;border:1px solid #fed7aa;border-radius:9px;}\r\n#pth-storyboard-wrapper .pth-sb-modelfix p{margin:0 0 8px;font-size:.85rem;color:var(--pth-ink);}\r\n#pth-storyboard-wrapper a.pth-sb-btn{display:inline-block;text-decoration:none;}\r\n#pth-storyboard-wrapper .pth-sb-hint a{color:var(--pth-primary);}\r\n\r\n\/* --- Language Modal CSS --- *\/\r\n#pth-lang-modal { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(15,23,42,0.6); z-index:999999; align-items:center; justify-content:center; backdrop-filter: blur(3px); }\r\n#pth-lang-modal.show { display:flex; }\r\n.pth-lang-box { background:#fff; padding:24px; border-radius:16px; width:90%; max-width:650px; max-height:85vh; display:flex; flex-direction:column; box-shadow: 0 10px 25px rgba(0,0,0,0.15); }\r\n.pth-lang-header { display:flex; justify-content:space-between; align-items:center; font-weight:700; font-size:1.25rem; margin-bottom:20px; color:#1e293b; }\r\n.pth-lang-close { cursor:pointer; font-size:1.5rem; line-height:1; color:#94a3b8; border:none; background:none; padding:5px; transition:color 0.2s; }\r\n.pth-lang-close:hover { color:#1e293b; }\r\n.pth-lang-search-wrap { position:relative; margin-bottom:20px; }\r\n.pth-lang-search-wrap:before { content:'\ud83d\udd0d'; position:absolute; left:14px; top:50%; transform:translateY(-50%); font-size:1rem; opacity:0.5; }\r\n.pth-lang-search { width:100%; padding:12px 14px 12px 40px; border:1px solid var(--pth-line); border-radius:99px; font-size:0.95rem; outline:none; transition:border-color 0.2s; background:#f8fafc; }\r\n.pth-lang-search:focus { border-color:var(--pth-primary); background:#fff; }\r\n.pth-lang-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(160px, 1fr)); gap:12px; overflow-y:auto; padding-right:5px; padding-bottom:10px; }\r\n.pth-lang-grid::-webkit-scrollbar { width: 6px; }\r\n.pth-lang-grid::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }\r\n.pth-lang-item { padding:14px; border:1px solid var(--pth-line); border-radius:12px; cursor:pointer; display:flex; align-items:center; gap:12px; transition:all 0.2s ease; font-size:0.95rem; font-weight:500; color:#334155; background:#fff; }\r\n.pth-lang-item:hover { background:#f1f5f9; border-color:#94a3b8; transform:translateY(-2px); box-shadow:0 4px 6px -1px rgba(0,0,0,0.05); }\r\n\r\n@media(max-width:820px){#pth-storyboard-wrapper .pth-sb-grid{grid-template-columns:1fr;}#pth-storyboard-wrapper .pth-sb-cardbody{grid-template-columns:1fr;}#pth-storyboard-wrapper .pth-sb-source{border-right:none;border-bottom:1px solid var(--pth-line);}}\r\n<\/style>\r\n\r\n<div id=\"pth-storyboard-wrapper\" class=\"pth-sb\" data-no-optimize=\"1\">\r\n  <h2 class=\"pth-sb-h2\">\ud83c\udfac AI Story-to-Storyboard Generator<\/h2>\r\n  <div class=\"pth-sb-badges\">\r\n    <span class=\"pth-version-badge\">v3.5<\/span>\r\n    <span class=\"pth-status-offline\">\ud83d\udd12 Client-side \u00b7 your API key stays in your browser<\/span>\r\n  <\/div>\r\n  <p class=\"pth-sb-desc\">Paste a full story or script and let your own AI model turn it into an organized, scene-by-scene storyboard: image prompts, minimalist video prompts with expected timing and dialogue, and voiceover lines. Bring your own API key (Gemini, OpenAI, or Anthropic). Long stories are handled part by part so the model never gets stuck.<\/p>\r\n\r\n  <div class=\"pth-sb-toolbar\">\r\n    <button type=\"button\" id=\"pth-sb-sample\" class=\"pth-sb-btn\">\ud83d\udca1 Load Sample<\/button>\r\n    <button type=\"button\" id=\"pth-sb-fullwidth\" class=\"pth-sb-btn\">\u26f6 Full Width<\/button>\r\n    <button type=\"button\" id=\"pth-sb-clear\" class=\"pth-sb-btn\">\ud83d\uddd1\ufe0f Clear<\/button>\r\n  <\/div>\r\n\r\n  <div class=\"pth-sb-grid\">\r\n    <div class=\"pth-sb-main\">\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-story\">Your full story \/ script<\/label>\r\n      <textarea id=\"pth-sb-story\" aria-label=\"Your full story or script\" placeholder=\"Paste your whole story here. Separate scenes or beats with a blank line for the cleanest split into parts.\"><\/textarea>\r\n      <div class=\"pth-sb-toolbar\" style=\"margin:10px 0 0;\"><button type=\"button\" id=\"pth-sb-generate\" class=\"pth-sb-btn pth-sb-primary\">\ud83c\udfac Split story into parts<\/button><\/div>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-setupgen\">\ud83d\uddc2\ufe0f Part 0 \u2014 Setup (run this first)<\/label>\r\n      <div class=\"pth-sb-toolbar\" style=\"margin:4px 0 0;\"><button type=\"button\" id=\"pth-sb-setupgen\" class=\"pth-sb-btn\">\ud83d\uddc2\ufe0f Generate Part 0 \u2014 all characters + key objects + locations<\/button><\/div>\r\n      <div id=\"pth-sb-setupout\" class=\"pth-sb-output\"><\/div>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-charreq\">\ud83d\udc64 Character generator<\/label>\r\n      <textarea id=\"pth-sb-charreq\" aria-label=\"Character to generate\" placeholder=\"Character name or short description (e.g. 'the doctor'). Leave blank to generate ALL main characters. Outputs character reference prompts only.\"><\/textarea>\r\n      <div class=\"pth-sb-toolbar\" style=\"margin:8px 0 0;\"><button type=\"button\" id=\"pth-sb-chargen\" class=\"pth-sb-btn\">\ud83d\udc64 Generate character reference(s)<\/button><\/div>\r\n      <div id=\"pth-sb-charout\" class=\"pth-sb-output\"><\/div>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-custom\">\u270f\ufe0f Custom scene \/ area request (optional)<\/label>\r\n      <textarea id=\"pth-sb-custom\" aria-label=\"Custom scene or area request\" placeholder=\"Generate one specific scene \u2014 e.g. 'the old well at night', 'the school corridor'. Scene prompts only (no character-bible dump).\"><\/textarea>\r\n      <div class=\"pth-sb-toolbar\" style=\"margin:8px 0 0;\"><button type=\"button\" id=\"pth-sb-customgen\" class=\"pth-sb-btn\">\u270f\ufe0f Generate custom scene<\/button><\/div>\r\n      <div id=\"pth-sb-customout\" class=\"pth-sb-output\"><\/div>\r\n\r\n      <div class=\"pth-sb-colctl\"><label for=\"pth-sb-colsplit\">\u2194 Story \/ Prompts column split<\/label><input type=\"range\" id=\"pth-sb-colsplit\" min=\"20\" max=\"80\" value=\"50\" step=\"5\" aria-label=\"Column split\"><span class=\"pth-sb-fontctl\">Text size: <button type=\"button\" id=\"pth-sb-fontminus\" class=\"pth-sb-btn\">A\u2212<\/button><button type=\"button\" id=\"pth-sb-fontplus\" class=\"pth-sb-btn\">A+<\/button><\/span><\/div>\r\n      <p class=\"pth-sb-hint\" style=\"margin:0 0 12px;\">\ud83d\udca1 Use the \ud83c\udf10 Translate button on each generated part to read prompts in your local language instantly. After splitting, you can edit each part's text and pick a shot type per part below \u2014 leave it on \"Auto\" to follow your MD instruction.<\/p>\r\n      <div id=\"pth-sb-output\" class=\"pth-sb-output\" aria-live=\"polite\"><\/div>\r\n\r\n      <div class=\"pth-sb-tools\">\r\n        <div class=\"pth-sb-collabel\">\ud83e\uddf0 Story tools<\/div>\r\n        <div class=\"pth-sb-toolbar\">\r\n          <button type=\"button\" id=\"pth-sb-voiceover\" class=\"pth-sb-btn\">\ud83c\udf99\ufe0f Extract voiceover<\/button>\r\n          <button type=\"button\" id=\"pth-sb-downloadall\" class=\"pth-sb-btn\">\u2b07 Download all<\/button>\r\n          <button type=\"button\" id=\"pth-sb-genall\" class=\"pth-sb-btn\">\u26a1 Generate all remaining<\/button>\r\n          <button type=\"button\" id=\"pth-sb-shotlist\" class=\"pth-sb-btn\">\ud83d\udccb Master shot list<\/button>\r\n          <button type=\"button\" id=\"pth-sb-thumb\" class=\"pth-sb-btn\">\ud83d\uddbc\ufe0f Thumbnail prompt<\/button>\r\n        <\/div>\r\n        <label class=\"pth-sb-label\" for=\"pth-sb-seoplatform\">\ud83d\udd16 SEO generator<\/label>\r\n        <div class=\"pth-sb-toolbar\">\r\n          <select id=\"pth-sb-seoplatform\" aria-label=\"SEO platform\">\r\n            <option value=\"YouTube\">YouTube<\/option>\r\n            <option value=\"Facebook\">Facebook<\/option>\r\n            <option value=\"TikTok\">TikTok<\/option>\r\n            <option value=\"Instagram\">Instagram<\/option>\r\n          <\/select>\r\n          <input type=\"text\" id=\"pth-sb-seokw\" aria-label=\"Main focus keyword\" placeholder=\"Main focus keyword (optional)\">\r\n          <button type=\"button\" id=\"pth-sb-seogen\" class=\"pth-sb-btn\">\ud83d\udd16 Generate SEO<\/button>\r\n        <\/div>\r\n        <p class=\"pth-sb-hint\">Title & description come back mixed Sinhala + English, tuned for the chosen platform, plus proposed keywords.<\/p>\r\n        <div id=\"pth-sb-toolsout\" class=\"pth-sb-output\"><\/div>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <aside class=\"pth-sb-side\">\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-provider\">AI provider<\/label>\r\n      <select id=\"pth-sb-provider\" aria-label=\"AI provider\">\r\n        <option value=\"gemini\">Google Gemini<\/option>\r\n        <option value=\"openai\">OpenAI<\/option>\r\n        <option value=\"anthropic\">Anthropic (Claude)<\/option>\r\n      <\/select>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-key\">API key<\/label>\r\n      <div class=\"pth-sb-keyrow\">\r\n        <input type=\"password\" id=\"pth-sb-key\" aria-label=\"API key\" autocomplete=\"off\" placeholder=\"Your provider API key\">\r\n      <\/div>\r\n      <p class=\"pth-sb-hint\">Stored only in this browser (localStorage). Requests go straight from your browser to the provider. Never share your key.<\/p>\r\n      <p class=\"pth-sb-hint\">No key? <a href=\"https:\/\/aistudio.google.com\/apikey\" target=\"_blank\" rel=\"noopener\">Get a free Google Gemini key \u2197<\/a><\/p>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-model\">Model<\/label>\r\n      <input type=\"text\" id=\"pth-sb-model\" aria-label=\"Model name\" placeholder=\"Model name\">\r\n      <p class=\"pth-sb-hint\">Editable \u2014 type a current model your provider supports (free Gemini options: gemini-3.5-flash, gemini-3.6-flash, gemini-3.5-flash-lite). Model names change over time.<\/p>\r\n      <p class=\"pth-sb-hint\">Not sure which model? <a id=\"pth-sb-modelhelp\" href=\"https:\/\/ai.google.dev\/gemini-api\/docs\/models\" target=\"_blank\" rel=\"noopener\">See your provider's current models \u2197<\/a><\/p>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-chunk\">Part size (words)<\/label>\r\n      <input type=\"number\" id=\"pth-sb-chunk\" aria-label=\"Part size in words\" value=\"100\" min=\"50\" step=\"50\">\r\n      <label class=\"pth-sb-label\" style=\"display:flex;align-items:center;gap:8px;font-weight:600;margin-top:12px;\"><input type=\"checkbox\" id=\"pth-sb-fullctx\" style=\"width:auto;\"> Send the full story with each part (better continuity, uses more tokens)<\/label>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-preset\">Sample preset<\/label>\r\n      <select id=\"pth-sb-preset\" aria-label=\"Sample preset\">\r\n        <option value=\"horror\">Sinhala horror (Holman Katha)<\/option>\r\n        <option value=\"general\">General cinematic<\/option>\r\n      <\/select>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-lang\">Reading language (explanations)<\/label>\r\n      <select id=\"pth-sb-lang\" aria-label=\"Reading language for explanations\">\r\n        <option value=\"story\">Story language (default)<\/option>\r\n        <option value=\"Sinhala\">Sinhala<\/option>\r\n        <option value=\"English\">English<\/option>\r\n        <option value=\"Tamil\">Tamil<\/option>\r\n        <option value=\"Hindi\">Hindi<\/option>\r\n        <option value=\"Arabic\">Arabic<\/option>\r\n        <option value=\"Spanish\">Spanish<\/option>\r\n        <option value=\"French\">French<\/option>\r\n        <option value=\"German\">German<\/option>\r\n        <option value=\"Portuguese\">Portuguese<\/option>\r\n        <option value=\"Indonesian\">Indonesian<\/option>\r\n        <option value=\"Chinese\">Chinese<\/option>\r\n        <option value=\"Japanese\">Japanese<\/option>\r\n        <option value=\"Korean\">Korean<\/option>\r\n        <option value=\"Russian\">Russian<\/option>\r\n      <\/select>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-md\">AI instruction (system prompt)<\/label>\r\n      <textarea id=\"pth-sb-md\" aria-label=\"AI instruction system prompt\"><\/textarea>\r\n      <div class=\"pth-sb-keyrow\" style=\"margin-top:8px;\">\r\n        <input type=\"file\" id=\"pth-sb-mdfile\" aria-label=\"Upload instruction file\" accept=\".md,.txt\">\r\n        <button type=\"button\" id=\"pth-sb-mdreset\" class=\"pth-sb-btn\">Reset<\/button>\r\n      <\/div>\r\n      <p class=\"pth-sb-hint\">Uses the built-in visual-director instruction by default. Upload your own .md \/ .txt to override \u2014 it is saved for next time.<\/p>\r\n      <p class=\"pth-sb-hint\"><a id=\"pth-sb-mddl\" href=\"https:\/\/schoolict.net\/sampledata\/holman-katha-visual-director-gem-v4_0.md\" target=\"_blank\" rel=\"noopener\" download>\u2b07 Download the example instruction (.md)<\/a><\/p>\r\n\r\n      <label class=\"pth-sb-label\" for=\"pth-sb-context\">Running context \/ Character Bible<\/label>\r\n      <textarea id=\"pth-sb-context\" aria-label=\"Running context and Character Bible\"><\/textarea>\r\n      <p class=\"pth-sb-hint\">Auto-filled from each generated part to keep characters consistent. You can edit it.<\/p>\r\n    <\/aside>\r\n  <\/div>\r\n\r\n  <div id=\"pth-sb-toast\" class=\"pth-sb-toast\" role=\"status\" aria-live=\"polite\"><\/div>\r\n\r\n  <!-- Language Popup Modal -->\r\n  <div id=\"pth-lang-modal\">\r\n    <div class=\"pth-lang-box\">\r\n      <div class=\"pth-lang-header\">\r\n        <span>Choose a language<\/span>\r\n        <button id=\"pth-lang-close-btn\" class=\"pth-lang-close\" aria-label=\"Close\">\u00d7<\/button>\r\n      <\/div>\r\n      <div class=\"pth-lang-search-wrap\">\r\n        <input type=\"text\" class=\"pth-lang-search\" id=\"pth-lang-search\" placeholder=\"Search language...\" autocomplete=\"off\">\r\n      <\/div>\r\n      <div class=\"pth-lang-grid\" id=\"pth-lang-grid\"><\/div>\r\n    <\/div>\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  var APP = 'pthStoryboardApp';\r\n  if (window[APP] && window[APP].inited) { return; }\r\n  window[APP] = window[APP] || {};\r\n\r\n  var LS = {\r\n    provider: 'pthSB_provider',\r\n    key: 'pthSB_key_',\r\n    model: 'pthSB_model_',\r\n    md: 'pthSB_md',\r\n    preset: 'pthSB_preset',\r\n    story: 'pthSB_story',\r\n    custom: 'pthSB_custom',\r\n    context: 'pthSB_context',\r\n    charreq: 'pthSB_charreq',\r\n    saved: 'pthSB_saved',\r\n    seokw: 'pthSB_seokw',\r\n    seoplatform: 'pthSB_seoplatform',\r\n    aux: 'pthSB_aux',\r\n    fullctx: 'pthSB_fullctx',\r\n    colsplit: 'pthSB_colsplit',\r\n    font: 'pthSB_font',\r\n    lang: 'pthSB_lang'\r\n  };\r\n\r\n  var DEFAULT_MODELS = {\r\n    gemini: 'gemini-3.5-flash',\r\n    openai: 'gpt-4o-mini',\r\n    anthropic: 'claude-3-5-sonnet-latest'\r\n  };\r\n\r\n  var MODEL_DOCS = {\r\n    gemini: 'https:\/\/ai.google.dev\/gemini-api\/docs\/models',\r\n    openai: 'https:\/\/platform.openai.com\/docs\/models',\r\n    anthropic: 'https:\/\/docs.anthropic.com\/en\/docs\/about-claude\/models'\r\n  };\r\n\r\n  var DEFAULT_MD_URL = 'https:\/\/schoolict.net\/sampledata\/holman-katha-visual-director-gem-v4_0';\r\n\r\n  var DEFAULT_MD = [\r\n    'ROLE: You are an Expert AI Visual Director, Cinematographer, and Prompt Engineer for Sri Lankan cinematic horror and ghost stories. You turn story text into stable, consistent AI image and video prompts (for Google Flow \/ Veo and image-to-video tools like Grok), and propose the next beat in spoken Sinhala (katawahara).',\r\n    '',\r\n    'CORE PRINCIPLE: Suggest, do not show. Stability over ambition. Pick the safest shot, the fewest moving parts, and the most repeatable descriptions. The scariest horror is implied, not shown, and this also keeps every scene easy for the AI to generate.',\r\n    '',\r\n    'CHARACTER SETUP: On the first part, for each main character write a Character Reference Image Prompt with exact Sri Lankan features, hair, skin tone, age, build, and period clothing; neutral expression, front or 3\/4 view, full outfit, even studio light, plain WHITE background, EMPTY HANDS (no props). Keep a running Character Bible and reuse the exact names and looks in every later scene.',\r\n    '',\r\n    'EASY-SCENE TRICK TOOLBOX (apply when the raw action is hard for AI):',\r\n    '1. Suggest, do not show: show the reaction, a shadow, a hand at the edge of frame, a door opening, not the monster or the gore.',\r\n    '2. Frozen Moment: never prompt a complex action; show the moment just before or just after it.',\r\n    '3. One-Motion rule: keep the whole frame still except ONE moving element (lamp flame, curtain, blinking eyes, mist).',\r\n    '4. Silhouette and Shadow: lean into darkness with a single light source; it is stable and hides AI errors.',\r\n    '5. Close over Wide: prefer Close-Ups and Extreme Close-Ups over full-body wide shots.',\r\n    '6. The Entity is never animated: for a ghost use a still figure, silhouette, reflection or shadow; never morph or transform it.',\r\n    '7. POV and Insert shots for hard parts: a POV corridor push or an insert (flame, clock, rain on glass, bare feet) is almost impossible to get wrong.',\r\n    '8. Cut on darkness: end a clip on total black so continuity between clips need not match perfectly.',\r\n    '',\r\n    'FOR EACH SCENE OUTPUT EXACTLY THESE SECTIONS:',\r\n    'SCENE HEADER: Scene [number] | [Location] | [Time of day] | Characters: [names]. Keep location, time and weather consistent with earlier scenes in the same place (Continuity Anchor).',\r\n    '',\r\n    '1. CINEMATOGRAPHY STRATEGY & SCENE DIVISION',\r\n    '- Decide if the scene works as ONE shot or must be split. DIVIDE ONLY WHEN THE SCENE IS DIFFICULT (two characters both talking\/moving a lot, 3+ people, complex action, or gore needing Focus Out). Keep simple scenes as a single shot.',\r\n    '- When dividing, use one-sided focus: Shot A on Character 1 speaking, Shot B on Character 2 reacting.',\r\n    '- IMPLIED HORROR RULE: never show gore directly; Focus Out via a reaction shot or shadows. This also keeps the video ad-safe.',\r\n    '- Keep 1-2 characters in frame; crowds stay in the soft dark background.',\r\n    '- Safe Shots\/Angles: Eye Level, High Angle, Low Angle, MS, CU, ECU, Two Shot. Safe Movements: Dolly In\/Out, Pan, Tilt, Zoom In\/Out. AVOID Crane, Steadicam, Tracking, complex actions.',\r\n    '',\r\n    '2. IMAGE PROMPT (for AI image generators)',\r\n    '- If divided, give [Prompt A] and [Prompt B].',\r\n    '- Start with the chosen Shot and Angle. Describe the Sri Lankan environment, low-key lighting and eerie mood. Use 16:9 cinematic framing.',\r\n    '- Do NOT re-describe character features. Use ONLY the bolded character name and describe only their emotion and exact action.',\r\n    '- End with: Authentic 1970s cinematic film horror aesthetic, heavy film grain, volumetric lighting.',\r\n    '',\r\n    '3. VIDEO PROMPT (for image-to-video tools)',\r\n    '- If divided, give [Video Prompt A] and [Video Prompt B].',\r\n    '- Begin with a highlighted EXPECTED TIME line, e.g. EXPECTED TIME: ~6s or ~8s, based on the action and the length of the spoken dialogue.',\r\n    '- Keep motion MINIMALIST: only the one camera movement plus tiny micro-motions.',\r\n    '- DIALOGUE (full): if the scene has dialogue, embed the FULL exact Sinhala line: Character is talking, lips moving naturally speaking: \"[full exact Sinhala dialogue]\". If a line is long, extend the expected time or split across Shot A \/ Shot B.',\r\n    '- Append: Negative Prompt: morphing, sudden movement, complex motion, changing clothes, distorted face, changing lighting, over-exaggerated expressions, gore, extra fingers, deformed hands, extra limbs, text, watermark, subtitles.',\r\n    '',\r\n    '4. SINHALA DIALOGUE (for voiceover \/ TTS): the exact Sinhala sentences, clean to copy-paste, in authentic spoken Sinhala (katawahara). If none: dabas nometa.',\r\n    '',\r\n    '5. WHAT HAPPENS NEXT: propose the next logical beat in conversational spoken Sinhala (katawahara). Avoid formal literary words.'\r\n  ].join('\\n');\r\n\r\n  var SAMPLE_HORROR = [\r\n    'Nineteen-eighty. Gilimale village had no electricity, only kuppi lamps and the sound of the river.',\r\n    '',\r\n    'Nanda walked home late along the paddy bund. The jasmine smell was too strong for the season, and the dogs had stopped barking all at once.',\r\n    '',\r\n    'Ahead, a woman in a white cloth stood facing the water, perfectly still. Nanda called out to her. She did not turn.',\r\n    '',\r\n    'He took one more step. The lamp in his hand went out on its own.'\r\n  ].join('\\n');\r\n\r\n  var SAMPLE_GENERAL = [\r\n    'A weathered lighthouse keeper works the last light on a storm-battered cliff at the edge of the North Sea.',\r\n    '',\r\n    'For three nights a lantern has answered him from the black water, though no ship is listed as passing.',\r\n    '',\r\n    'On the fourth night he climbs down to the rocks to meet whoever is signalling back.'\r\n  ].join('\\n');\r\n\r\n  var els = {};\r\n  var state = { parts: [], results: [], cards: [], html: [], font: [], lang: [], shot: [] };\r\n  var auxStores = { setup: [], char: [], custom: [], tools: [] };\r\n  \r\n  \/\/ Modal Variables\r\n  var currentCardIndex = -1;\r\n  var currentCardElement = null;\r\n  var popupLanguages = [\r\n    { code: 'en', name: 'English (Original)', icon: '\ud83c\uddec\ud83c\udde7' },\r\n    { code: 'si', name: 'Sinhala (\u0dc3\u0dd2\u0d82\u0dc4\u0dbd)', icon: '\ud83c\uddf1\ud83c\uddf0' },\r\n    { code: 'hi', name: 'Hindi (\u0939\u093f\u0928\u094d\u0926\u0940)', icon: '\ud83c\uddee\ud83c\uddf3' },\r\n    { code: 'ta', name: 'Tamil (\u0ba4\u0bae\u0bbf\u0bb4\u0bcd)', icon: '\ud83c\uddee\ud83c\uddf3' },\r\n    { code: 'es', name: 'Espa\u00f1ol', icon: '\ud83c\uddea\ud83c\uddf8' },\r\n    { code: 'fr', name: 'Fran\u00e7ais', icon: '\ud83c\uddeb\ud83c\uddf7' },\r\n    { code: 'de', name: 'Deutsch', icon: '\ud83c\udde9\ud83c\uddea' },\r\n    { code: 'it', name: 'Italiano', icon: '\ud83c\uddee\ud83c\uddf9' },\r\n    { code: 'pt', name: 'Portugu\u00eas', icon: '\ud83c\uddf5\ud83c\uddf9' },\r\n    { code: 'zh-CN', name: '\u7b80\u4f53\u4e2d\u6587', icon: '\ud83c\udde8\ud83c\uddf3' },\r\n    { code: 'ja', name: '\u65e5\u672c\u8a9e', icon: '\ud83c\uddef\ud83c\uddf5' },\r\n    { code: 'ko', name: '\ud55c\uad6d\uc5b4', icon: '\ud83c\uddf0\ud83c\uddf7' },\r\n    { code: 'bn', name: '\u09ac\u09be\u0982\u09b2\u09be', icon: '\ud83c\udde7\ud83c\udde9' },\r\n    { code: 'id', name: 'Bahasa Indonesia', icon: '\ud83c\uddee\ud83c\udde9' },\r\n    { code: 'ms', name: 'Bahasa Melayu', icon: '\ud83c\uddf2\ud83c\uddfe' },\r\n    { code: 'th', name: '\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22', icon: '\ud83c\uddf9\ud83c\udded' },\r\n    { code: 'vi', name: 'Ti\u1ebfng Vi\u1ec7t', icon: '\ud83c\uddfb\ud83c\uddf3' },\r\n    { code: 'ru', name: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439', icon: '\ud83c\uddf7\ud83c\uddfa' },\r\n    { code: 'ar', name: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629', icon: '\ud83c\uddf8\ud83c\udde6' },\r\n    { code: 'tr', name: 'T\u00fcrk\u00e7e', icon: '\ud83c\uddf9\ud83c\uddf7' }\r\n  ];\r\n\r\n  \/\/ Per-part shot-type options. An empty value means \"no override\" -> the model\r\n  \/\/ follows the shot rules in the MD (system) instruction. A non-empty value is\r\n  \/\/ injected into the user message as a preferred primary shot type for that part.\r\n  var SHOT_TYPES = [\r\n    { value: '', label: 'Auto (use MD default)' },\r\n    { value: 'Wide \/ establishing shot', label: 'Wide \/ Establishing' },\r\n    { value: 'Medium Shot (MS)', label: 'Medium Shot (MS)' },\r\n    { value: 'Close-Up (CU)', label: 'Close-Up (CU)' },\r\n    { value: 'Extreme Close-Up (ECU)', label: 'Extreme Close-Up (ECU)' },\r\n    { value: 'Two-Shot with both characters in the frame', label: 'Two-Shot' },\r\n    { value: 'Over-the-Shoulder \/ Shot-Reverse-Shot', label: 'Over-the-Shoulder (OTS)' },\r\n    { value: 'POV shot', label: 'POV' },\r\n    { value: 'Insert \/ detail shot', label: 'Insert \/ Detail' },\r\n    { value: 'Reaction shot', label: 'Reaction shot' },\r\n    { value: 'Silhouette \/ shadow shot', label: 'Silhouette \/ Shadow' },\r\n    { value: 'High Angle', label: 'High Angle' },\r\n    { value: 'Low Angle', label: 'Low Angle' }\r\n  ];\r\n\r\n  function $(id) { return document.getElementById(id); }\r\n\r\n  function saveLocal(k, x) { try { window.localStorage.setItem(k, x); } catch (e) {} }\r\n  function readLocal(k) { try { return window.localStorage.getItem(k); } catch (e) { return null; } }\r\n\r\n  function toast(msg) {\r\n    var t = els.toast; if (!t) { return; }\r\n    t.textContent = msg;\r\n    t.className = 'pth-sb-toast pth-sb-show';\r\n    window.clearTimeout(t._tmo);\r\n    t._tmo = window.setTimeout(function () { t.className = 'pth-sb-toast'; }, 3000);\r\n  }\r\n\r\n  \/\/ --- Modal Logic ---\r\n  function openLangModal(index, card) {\r\n    currentCardIndex = index;\r\n    currentCardElement = card;\r\n    var modal = $('pth-lang-modal');\r\n    if (modal) {\r\n      modal.classList.add('show');\r\n      renderLanguages();\r\n      var search = $('pth-lang-search');\r\n      if (search) { search.value = ''; search.focus(); filterLanguages(); }\r\n    }\r\n  }\r\n\r\n  function closeLangModal() {\r\n    var modal = $('pth-lang-modal');\r\n    if (modal) { modal.classList.remove('show'); }\r\n  }\r\n\r\n  function filterLanguages() {\r\n    var search = $('pth-lang-search');\r\n    var query = search ? search.value.toLowerCase() : '';\r\n    var grid = $('pth-lang-grid');\r\n    if (!grid) return;\r\n    var items = grid.querySelectorAll('.pth-lang-item');\r\n    for (var i = 0; i < items.length; i++) {\r\n      if (items[i].textContent.toLowerCase().indexOf(query) > -1) {\r\n        items[i].style.display = 'flex';\r\n      } else {\r\n        items[i].style.display = 'none';\r\n      }\r\n    }\r\n  }\r\n\r\n  function renderLanguages() {\r\n    var grid = $('pth-lang-grid');\r\n    if (!grid) return;\r\n    grid.innerHTML = '';\r\n    for (var i = 0; i < popupLanguages.length; i++) {\r\n      (function(lang) {\r\n        var btn = document.createElement('div');\r\n        btn.className = 'pth-lang-item';\r\n        btn.innerHTML = '<span>' + lang.icon + '<\/span> <span>' + lang.name + '<\/span>';\r\n        btn.addEventListener('click', function() {\r\n          translatePart(currentCardElement, currentCardIndex, lang.code);\r\n          closeLangModal();\r\n        });\r\n        grid.appendChild(btn);\r\n      })(popupLanguages[i]);\r\n    }\r\n  }\r\n\r\n  function translatePart(card, idx, targetLang) {\r\n    var pre = card.querySelector('.pth-sb-pre');\r\n    var origText = state.results[idx];\r\n    if (!origText && pre) { origText = pre.innerText; }\r\n    if (!origText) { return; }\r\n\r\n    if (targetLang === 'en') {\r\n      if (state.html[idx]) { pre.innerHTML = state.html[idx]; }\r\n      else { pre.textContent = origText; }\r\n      return;\r\n    }\r\n\r\n    var backup = pre.innerHTML;\r\n    pre.textContent = \"Translating... \u0d9a\u0dbb\u0dd4\u0dab\u0dcf\u0d9a\u0dbb \u0dbb\u0dd0\u0db3\u0dd3 \u0dc3\u0dd2\u0da7\u0dd2\u0db1\u0dca\u0db1...\";\r\n\r\n    var url = 'https:\/\/translate.googleapis.com\/translate_a\/single?client=gtx&sl=en&tl=' + targetLang + '&dt=t';\r\n    var bodyParams = 'q=' + encodeURIComponent(origText);\r\n\r\n    window.fetch(url, {\r\n      method: 'POST',\r\n      headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\r\n      body: bodyParams\r\n    }).then(function(res) {\r\n      return res.json();\r\n    }).then(function(data) {\r\n      var translatedText = '';\r\n      if (data && data[0]) {\r\n        for (var j = 0; j < data[0].length; j++) {\r\n          if (data[0][j][0]) { translatedText += data[0][j][0]; }\r\n        }\r\n      }\r\n      pre.textContent = translatedText;\r\n    })['catch'](function(err) {\r\n      toast('Translation failed.');\r\n      pre.innerHTML = backup;\r\n    });\r\n  }\r\n\r\n  function currentProvider() { return els.provider ? els.provider.value : 'gemini'; }\r\n\r\n  function applyProviderDefaults(p) {\r\n    els.key.value = readLocal(LS.key + p) || '';\r\n    els.model.value = readLocal(LS.model + p) || DEFAULT_MODELS[p] || '';\r\n  }\r\n\r\n  function fetchDefaultMd(cb) {\r\n    if (!window.fetch) { cb(null); return; }\r\n    window.fetch(DEFAULT_MD_URL, { method: 'GET' }).then(function (r) {\r\n      if (!r.ok) { cb(null); return; }\r\n      return r.text().then(function (t) { cb(t); });\r\n    })['catch'](function () { cb(null); });\r\n  }\r\n\r\n  function loadSettings() {\r\n    var p = readLocal(LS.provider) || 'gemini';\r\n    els.provider.value = p;\r\n    applyProviderDefaults(p);\r\n    els.preset.value = readLocal(LS.preset) || 'horror';\r\n    els.story.value = readLocal(LS.story) || '';\r\n    els.custom.value = readLocal(LS.custom) || '';\r\n    els.charreq.value = readLocal(LS.charreq) || '';\r\n    els.context.value = readLocal(LS.context) || '';\r\n    els.seokw.value = readLocal(LS.seokw) || '';\r\n    els.seoplatform.value = readLocal(LS.seoplatform) || 'YouTube';\r\n    els.fullctx.checked = (readLocal(LS.fullctx) === '1');\r\n    els.colsplit.value = readLocal(LS.colsplit) || '50';\r\n    els.lang.value = readLocal(LS.lang) || 'story';\r\n    var md = readLocal(LS.md);\r\n    if (md && md.length) {\r\n      els.md.value = md;\r\n    } else {\r\n      els.md.value = DEFAULT_MD;\r\n      fetchDefaultMd(function (t) { if (t && !readLocal(LS.md)) { els.md.value = t; } });\r\n    }\r\n  }\r\n\r\n\r\n\r\n\/\/ Split text into parts of roughly chunkWords words, but break only at the\r\n\/\/ end of a complete sentence so no sentence is cut in half.\r\n\/\/ Works for English and Sinhala (sentence marks . ! ? are ASCII in both).\r\nfunction splitParts(text, chunkWords) {\r\n    if (!chunkWords || chunkWords < 50) { chunkWords = 100; }\r\n    if (!text || text.trim() === '') { return []; }\r\n\r\n    \/\/ Split on whitespace and drop empty tokens.\r\n    var words = text.split(\/\\s+\/).filter(function (w) { return w.length > 0; });\r\n    if (words.length === 0) { return [text]; }\r\n\r\n    var softLimit = chunkWords;                    \/\/ start looking for a sentence end here\r\n    var hardLimit = Math.round(chunkWords * 1.6);  \/\/ force a break if no sentence end by here\r\n\r\n    var parts = [];\r\n    var currentPart = [];\r\n    var wordCount = 0;\r\n\r\n    for (var i = 0; i < words.length; i++) {\r\n        currentPart.push(words[i]);\r\n        wordCount++;\r\n\r\n        var reachedSentenceEnd = (wordCount >= softLimit) && endsSentence(words[i]);\r\n        var reachedHardLimit = wordCount >= hardLimit;\r\n\r\n        if (reachedSentenceEnd || reachedHardLimit) {\r\n            parts.push(currentPart.join(' '));\r\n            currentPart = [];\r\n            wordCount = 0;\r\n        }\r\n    }\r\n\r\n    \/\/ Attach leftover words to the last part if it is only a small tail,\r\n    \/\/ otherwise keep them as a final part.\r\n    if (currentPart.length > 0) {\r\n        if (parts.length > 0 && currentPart.length < Math.round(chunkWords \/ 3)) {\r\n            parts[parts.length - 1] += ' ' + currentPart.join(' ');\r\n        } else {\r\n            parts.push(currentPart.join(' '));\r\n        }\r\n    }\r\n\r\n    if (parts.length === 0 && text) { parts.push(text); }\r\n    return parts;\r\n}\r\n\r\n\/\/ Return true when a word ends a sentence (last real character is . ! or ?),\r\n\/\/ ignoring trailing quotes or closing brackets such as .\" or !) or ?\"\r\nfunction endsSentence(word) {\r\n    if (!word) { return false; }\r\n    var trimmed = word;\r\n    while (trimmed.length > 0) {\r\n        var last = trimmed.charAt(trimmed.length - 1);\r\n        if (last === '\"' || last === \"'\" || last === ')' || last === ']' ||\r\n            last === '\\u201D' || last === '\\u2019' || last === '\\u00BB') {\r\n            trimmed = trimmed.substring(0, trimmed.length - 1);\r\n        } else {\r\n            break;\r\n        }\r\n    }\r\n    if (trimmed.length === 0) { return false; }\r\n    var lastChar = trimmed.charAt(trimmed.length - 1);\r\n    return lastChar === '.' || lastChar === '!' || lastChar === '?';\r\n}\r\n\r\n\/\/ Count words the same way splitParts does.\r\nfunction countWords(s) {\r\n    if (!s) { return 0; }\r\n    return s.split(\/\\s+\/).filter(function (w) { return w.length > 0; }).length;\r\n}\r\n\r\n  function buildUserMessage(part, idx, total, lang, shot) {\r\n    var ctxVal = els.context.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var ctx = ctxVal ? ('\\n\\nESTABLISHED CHARACTER, OBJECT & LOCATION BIBLE (already created earlier \u2014 reuse these EXACTLY, same names and looks):\\n' + ctxVal) : '';\r\n    var head;\r\n    if (idx === 0) {\r\n      if (ctxVal) {\r\n        head = 'This is PART 1 OF ' + total + ' of one continuous story. The characters, objects and locations are ALREADY established (see the bible below) \u2014 DO NOT recreate character reference prompts and DO NOT repeat the Part 0 setup. Begin with the establishing environment opener (Scene 0), then generate the scene prompts for THIS part, staying perfectly consistent with the bible.';\r\n      } else {\r\n        head = 'This is PART 1 OF ' + total + ' of one continuous story. Do the full setup from the system instruction (character reference prompts + key objects bible + location bible) AND the establishing opener, then the scene prompts for this part.';\r\n      }\r\n    } else {\r\n      head = 'This is PART ' + (idx + 1) + ' OF ' + total + ' of one continuous story. The characters and locations are ALREADY established (see the bible below). DO NOT recreate character reference prompts, and DO NOT repeat the Part 0 setup or the establishing opener. Generate ONLY the scene prompts for THIS part, staying perfectly consistent with the established bible. Only if a genuinely NEW character or location first appears in this part, add just that one new entry.';\r\n    }\r\n    var story = els.story.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var full = (els.fullctx && els.fullctx.checked && story) ? ('\\n\\nFULL STORY (reference for continuity across all parts \u2014 generate prompts ONLY for the current part shown at the end):\\n' + story) : '';\r\n    var strict = '\\n\\nSTRICT RULE: Use ONLY the events, characters and dialogue that actually appear in the story text. Do NOT invent, add, or propose any dialogue, events, characters or plot that is not written in the story. If a scene has no spoken dialogue in the story, write \"\\u0daf\\u0dd9\\u0db6\\u0dc3\\u0dca \\u0db1\\u0ddc\\u0db8\\u0dd0\\u0dad\" for the dialogue. Do not add a \"what happens next\" that is not already in the story.';\r\n    var shotNote = '';\r\n    if (shot) {\r\n      shotNote = '\\n\\nSHOT TYPE PREFERENCE FOR THIS PART: The creator has chosen a preferred primary shot type for this part: ' + shot + '. Use this as the main shot type \/ framing for the key beat(s) of this part wherever it fits the story, while still following all the other cinematography, division and stability rules in the system instruction. If the story genuinely cannot use it, choose the closest safe alternative. If no preference is given, follow the default shot rules in the system instruction.';\r\n    }\r\n    return head + ctx + full + strict + langNote(lang) + shotNote +\r\n      '\\n\\nAt the very end, output one line starting with \"CHARACTER BIBLE:\" listing every character established so far as: name - one-line fixed look, so it carries into the next part.' +\r\n      '\\n\\n=== CURRENT STORY PART ' + (idx + 1) + ' OF ' + total + ' (generate prompts for THIS part only) ===\\n' + part;\r\n  }\r\n\r\n  function extractBible(text) {\r\n    var m = text.match(\/CHARACTER BIBLE:\\s*([\\s\\S]*)$\/i);\r\n    if (m && m[1]) {\r\n      var b = m[1].replace(\/^\\s+|\\s+$\/g, '');\r\n      if (b && b.length >= (els.context.value || '').replace(\/^\\s+|\\s+$\/g, '').length) {\r\n        els.context.value = b;\r\n        saveLocal(LS.context, b);\r\n      }\r\n    }\r\n  }\r\n\r\n  function copyText(text) {\r\n    if (window.navigator && window.navigator.clipboard && window.navigator.clipboard.writeText) {\r\n      window.navigator.clipboard.writeText(text).then(function () { toast('Copied.'); }, function () { fallbackCopy(text); });\r\n    } else {\r\n      fallbackCopy(text);\r\n    }\r\n  }\r\n\r\n  function fallbackCopy(text) {\r\n    var ta = document.createElement('textarea');\r\n    ta.value = text;\r\n    ta.style.position = 'fixed';\r\n    ta.style.top = '-1000px';\r\n    document.body.appendChild(ta);\r\n    ta.select();\r\n    try { document.execCommand('copy'); toast('Copied.'); } catch (e) { toast('Copy failed - select manually.'); }\r\n    document.body.removeChild(ta);\r\n  }\r\n\r\n  function isModelError(msg) {\r\n    if (!msg) { return false; }\r\n    var m = ('' + msg).toLowerCase();\r\n    return m.indexOf('no longer available') > -1 ||\r\n      m.indexOf('not found') > -1 ||\r\n      m.indexOf('does not exist') > -1 ||\r\n      m.indexOf('is not available') > -1 ||\r\n      m.indexOf('deprecated') > -1 ||\r\n      m.indexOf('not supported') > -1 ||\r\n      m.indexOf('unsupported') > -1 ||\r\n      m.indexOf('unknown model') > -1 ||\r\n      m.indexOf('invalid model') > -1 ||\r\n      m.indexOf('no longer supported') > -1;\r\n  }\r\n\r\n  function modelDocUrl() { return MODEL_DOCS[currentProvider()] || MODEL_DOCS.gemini; }\r\n\r\n  function updateModelHelp() {\r\n    if (els.modelhelp) { els.modelhelp.setAttribute('href', modelDocUrl()); }\r\n  }\r\n\r\n  function renderModelHelp(card) {\r\n    if (card.querySelector('.pth-sb-modelfix')) { return; }\r\n    var box = document.createElement('div');\r\n    box.className = 'pth-sb-modelfix';\r\n    var p = document.createElement('p');\r\n    p.textContent = 'This model name looks outdated or unavailable. Models get retired over time. Open the current model list for this provider, copy a working model name into the Model field above, then click Generate again.';\r\n    box.appendChild(p);\r\n    var a = document.createElement('a');\r\n    a.className = 'pth-sb-btn';\r\n    a.setAttribute('href', modelDocUrl());\r\n    a.setAttribute('target', '_blank');\r\n    a.setAttribute('rel', 'noopener');\r\n    a.textContent = 'See current ' + currentProvider() + ' models \\u2197';\r\n    box.appendChild(a);\r\n    card.appendChild(box);\r\n  }\r\n\r\n  function callProvider(system, user, cb) {\r\n    var p = currentProvider();\r\n    var key = els.key.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var model = els.model.value.replace(\/^\\s+|\\s+$\/g, '') || DEFAULT_MODELS[p];\r\n    if (!key) { cb('Please enter your ' + p + ' API key in the settings panel.'); return; }\r\n\r\n    var url, opts;\r\n    if (p === 'gemini') {\r\n      url = 'https:\/\/generativelanguage.googleapis.com\/v1beta\/models\/' + encodeURIComponent(model) + ':generateContent?key=' + encodeURIComponent(key);\r\n      opts = {\r\n        method: 'POST',\r\n        headers: { 'Content-Type': 'application\/json' },\r\n        body: JSON.stringify({\r\n          system_instruction: { parts: [{ text: system }] },\r\n          contents: [{ role: 'user', parts: [{ text: user }] }]\r\n        })\r\n      };\r\n    } else if (p === 'openai') {\r\n      url = 'https:\/\/api.openai.com\/v1\/chat\/completions';\r\n      opts = {\r\n        method: 'POST',\r\n        headers: { 'Content-Type': 'application\/json', 'Authorization': 'Bearer ' + key },\r\n        body: JSON.stringify({\r\n          model: model,\r\n          messages: [{ role: 'system', content: system }, { role: 'user', content: user }]\r\n        })\r\n      };\r\n    } else {\r\n      url = 'https:\/\/api.anthropic.com\/v1\/messages';\r\n      opts = {\r\n        method: 'POST',\r\n        headers: {\r\n          'Content-Type': 'application\/json',\r\n          'x-api-key': key,\r\n          'anthropic-version': '2023-06-01',\r\n          'anthropic-dangerous-direct-browser-access': 'true'\r\n        },\r\n        body: JSON.stringify({\r\n          model: model,\r\n          max_tokens: 4096,\r\n          system: system,\r\n          messages: [{ role: 'user', content: user }]\r\n        })\r\n      };\r\n    }\r\n\r\n    window.fetch(url, opts).then(function (r) {\r\n      return r.text().then(function (t) { return { ok: r.ok, status: r.status, text: t }; });\r\n    }).then(function (res) {\r\n      var data = null;\r\n      try { data = JSON.parse(res.text); } catch (e) { data = null; }\r\n      if (!res.ok) {\r\n        var em = 'HTTP ' + res.status;\r\n        if (data && data.error) { em = (typeof data.error === 'string') ? data.error : (data.error.message || em); }\r\n        cb(em);\r\n        return;\r\n      }\r\n      var out = '', i, arr;\r\n      if (p === 'gemini') {\r\n        if (data && data.candidates && data.candidates[0] && data.candidates[0].content && data.candidates[0].content.parts) {\r\n          arr = data.candidates[0].content.parts;\r\n          for (i = 0; i < arr.length; i++) { if (arr[i].text) { out += arr[i].text; } }\r\n        }\r\n      } else if (p === 'openai') {\r\n        if (data && data.choices && data.choices[0] && data.choices[0].message) { out = data.choices[0].message.content || ''; }\r\n      } else {\r\n        if (data && data.content) { for (i = 0; i < data.content.length; i++) { if (data.content[i].text) { out += data.content[i].text; } } }\r\n      }\r\n      if (!out) { cb('Empty response from provider.'); return; }\r\n      cb(null, out);\r\n    })['catch'](function (err) {\r\n      cb('Network error: ' + (err && err.message ? err.message : 'request failed') + '. Check the API key, the model name, and that this provider allows direct browser requests.');\r\n    });\r\n  }\r\n\r\n  function addCollapseToggle(card) {\r\n    var head = card.querySelector('.pth-sb-cardhead'); if (!head) { return; }\r\n    if (head.querySelector('.pth-sb-togglebtn')) { return; }\r\n    var b = document.createElement('button');\r\n    b.type = 'button';\r\n    b.className = 'pth-sb-btn pth-sb-togglebtn';\r\n    b.textContent = 'Hide';\r\n    b.addEventListener('click', function () {\r\n      if (card.className.indexOf('pth-sb-collapsed') > -1) {\r\n        card.className = card.className.replace(\/\\s*pth-sb-collapsed\/, '');\r\n        b.textContent = 'Hide';\r\n      } else {\r\n        card.className += ' pth-sb-collapsed';\r\n        b.textContent = 'Show';\r\n      }\r\n    });\r\n    head.appendChild(b);\r\n  }\r\n\r\n  function setAllCollapsed(collapsed) {\r\n    var i, card, b;\r\n    for (i = 0; i < state.cards.length; i++) {\r\n      card = state.cards[i];\r\n      b = card.querySelector('.pth-sb-togglebtn');\r\n      if (collapsed) {\r\n        if (card.className.indexOf('pth-sb-collapsed') < 0) { card.className += ' pth-sb-collapsed'; }\r\n        if (b) { b.textContent = 'Show'; }\r\n      } else {\r\n        card.className = card.className.replace(\/\\s*pth-sb-collapsed\/, '');\r\n        if (b) { b.textContent = 'Hide'; }\r\n      }\r\n    }\r\n  }\r\n\r\n  function markSelection(card, idx) {\r\n    var sel = window.getSelection ? window.getSelection() : null;\r\n    if (!sel || sel.rangeCount === 0 || sel.isCollapsed) { toast('Select some text in this part first.'); return; }\r\n    var pre = card.querySelector('.pth-sb-pre');\r\n    var range = sel.getRangeAt(0);\r\n    if (!pre.contains(range.commonAncestorContainer)) { toast('Select text inside this part.'); return; }\r\n    var node = range.commonAncestorContainer;\r\n    var el = (node.nodeType === 1) ? node : node.parentNode;\r\n    while (el && el !== pre) {\r\n      if (el.className && ('' + el.className).indexOf('pth-sb-mark') > -1) {\r\n        var parent = el.parentNode;\r\n        while (el.firstChild) { parent.insertBefore(el.firstChild, el); }\r\n        parent.removeChild(el);\r\n        sel.removeAllRanges();\r\n        state.html[idx] = pre.innerHTML;\r\n        persistParts();\r\n        toast('Highlight removed.');\r\n        return;\r\n      }\r\n      el = el.parentNode;\r\n    }\r\n    try {\r\n      var m = document.createElement('mark');\r\n      m.className = 'pth-sb-mark';\r\n      range.surroundContents(m);\r\n      sel.removeAllRanges();\r\n      state.html[idx] = pre.innerHTML;\r\n      persistParts();\r\n      toast('Marked as to-do.');\r\n    } catch (e) { toast('Select text within a single line to mark it.'); }\r\n  }\r\n\r\n  function stepPartFont(card, i, delta) {\r\n    var x = state.font[i] || 14;\r\n    x += delta;\r\n    if (x < 10) { x = 10; }\r\n    if (x > 26) { x = 26; }\r\n    state.font[i] = x;\r\n    card.style.setProperty('--pth-font', x + 'px');\r\n    persistParts();\r\n  }\r\n\r\n  function addCardControls(card, i) {\r\n    var head = card.querySelector('.pth-sb-cardhead'); if (!head) { return; }\r\n    var fm = document.createElement('button');\r\n    fm.type = 'button'; fm.className = 'pth-sb-btn'; fm.textContent = 'A-';\r\n    fm.addEventListener('click', function () { stepPartFont(card, i, -1); });\r\n    var fp = document.createElement('button');\r\n    fp.type = 'button'; fp.className = 'pth-sb-btn'; fp.textContent = 'A+';\r\n    fp.addEventListener('click', function () { stepPartFont(card, i, 1); });\r\n    \r\n    var transBtn = document.createElement('button');\r\n    transBtn.type = 'button';\r\n    transBtn.className = 'pth-sb-btn';\r\n    transBtn.innerHTML = '\ud83c\udf10 Translate';\r\n    transBtn.addEventListener('click', function() { openLangModal(i, card); });\r\n    \r\n    head.appendChild(transBtn);\r\n    head.appendChild(fm); head.appendChild(fp);\r\n    if (state.font[i]) { card.style.setProperty('--pth-font', state.font[i] + 'px'); }\r\n  }\r\n\r\n  function makeCard(i) {\r\n    var card = document.createElement('div');\r\n    card.className = 'pth-sb-card';\r\n\r\n    var head = document.createElement('div');\r\n    head.className = 'pth-sb-cardhead';\r\n\r\n    var title = document.createElement('span');\r\n    title.className = 'pth-sb-cardtitle';\r\n    title.textContent = 'Part ' + (i + 1);\r\n    head.appendChild(title);\r\n\r\n    var stat = document.createElement('span');\r\n    stat.className = 'pth-sb-stat';\r\n    stat.textContent = 'Not generated';\r\n    head.appendChild(stat);\r\n\r\n    var genBtn = document.createElement('button');\r\n    genBtn.type = 'button';\r\n    genBtn.className = 'pth-sb-btn pth-sb-genbtn';\r\n    genBtn.textContent = 'Generate';\r\n    genBtn.addEventListener('click', function () { generatePart(i); });\r\n    head.appendChild(genBtn);\r\n\r\n    var copyBtn = document.createElement('button');\r\n    copyBtn.type = 'button';\r\n    copyBtn.className = 'pth-sb-btn pth-sb-copybtn';\r\n    copyBtn.textContent = 'Copy';\r\n    copyBtn.addEventListener('click', function () {\r\n      if (state.results[i]) { copyText(state.results[i]); } else { toast('Nothing to copy yet.'); }\r\n    });\r\n    head.appendChild(copyBtn);\r\n\r\n    var clrBtn = document.createElement('button');\r\n    clrBtn.type = 'button';\r\n    clrBtn.className = 'pth-sb-btn pth-sb-clrbtn';\r\n    clrBtn.textContent = 'Clear';\r\n    clrBtn.addEventListener('click', function () {\r\n      state.results[i] = '';\r\n      state.html[i] = '';\r\n      persistParts();\r\n      var p = card.querySelector('.pth-sb-pre');\r\n      if (p) { p.textContent = ''; }\r\n      var st = card.querySelector('.pth-sb-stat');\r\n      if (st) { st.textContent = 'Not generated'; }\r\n      var gb = card.querySelector('.pth-sb-genbtn');\r\n      if (gb) { gb.textContent = 'Generate'; }\r\n      toast('Part ' + (i + 1) + ' cleared.');\r\n    });\r\n    head.appendChild(clrBtn);\r\n\r\n    var markBtn = document.createElement('button');\r\n    markBtn.type = 'button';\r\n    markBtn.className = 'pth-sb-btn pth-sb-markbtn';\r\n    markBtn.textContent = '\ud83d\udd8d\ufe0f Mark';\r\n    markBtn.addEventListener('click', function () { markSelection(card, i); });\r\n    head.appendChild(markBtn);\r\n\r\n    card.appendChild(head);\r\n\r\n    var body = document.createElement('div');\r\n    body.className = 'pth-sb-cardbody';\r\n\r\n    var src = document.createElement('div');\r\n    src.className = 'pth-sb-source';\r\n    var srcLabel = document.createElement('div');\r\n    srcLabel.className = 'pth-sb-collabel';\r\n    srcLabel.textContent = 'Story part ' + (i + 1) + ' (editable)';\r\n\r\n    \/\/ Per-part shot-type selector. Empty value keeps the MD default behaviour.\r\n    var shotRow = document.createElement('div');\r\n    shotRow.className = 'pth-sb-shotrow';\r\n    var shotLabel = document.createElement('label');\r\n    shotLabel.className = 'pth-sb-shotlabel';\r\n    shotLabel.textContent = 'Shot type:';\r\n    shotLabel.setAttribute('for', 'pth-sb-shot-' + i);\r\n    var shotSel = document.createElement('select');\r\n    shotSel.className = 'pth-sb-shotsel';\r\n    shotSel.id = 'pth-sb-shot-' + i;\r\n    var si, opt;\r\n    for (si = 0; si < SHOT_TYPES.length; si++) {\r\n      opt = document.createElement('option');\r\n      opt.value = SHOT_TYPES[si].value;\r\n      opt.textContent = SHOT_TYPES[si].label;\r\n      shotSel.appendChild(opt);\r\n    }\r\n    shotSel.value = state.shot[i] || '';\r\n    shotSel.addEventListener('change', function () {\r\n      state.shot[i] = shotSel.value;\r\n      persistParts();\r\n    });\r\n    shotRow.appendChild(shotLabel);\r\n    shotRow.appendChild(shotSel);\r\n\r\n    \/\/ Editable story-part text. Edits are stored back into state.parts[i] and used\r\n    \/\/ as-is when the part is generated (or regenerated).\r\n    var srcText = document.createElement('textarea');\r\n    srcText.className = 'pth-sb-srcedit';\r\n    srcText.value = state.parts[i] || '';\r\n    srcText.setAttribute('aria-label', 'Editable text for story part ' + (i + 1));\r\n    srcText.addEventListener('input', function () {\r\n      state.parts[i] = srcText.value;\r\n    });\r\n    srcText.addEventListener('change', function () {\r\n      state.parts[i] = srcText.value;\r\n      persistParts();\r\n    });\r\n\r\n    src.appendChild(srcLabel);\r\n    src.appendChild(shotRow);\r\n    src.appendChild(srcText);\r\n\r\n    var outcol = document.createElement('div');\r\n    outcol.className = 'pth-sb-outcol';\r\n    var outLabel = document.createElement('div');\r\n    outLabel.className = 'pth-sb-collabel';\r\n    outLabel.textContent = 'Generated prompts';\r\n    var pre = document.createElement('pre');\r\n    pre.className = 'pth-sb-pre';\r\n    pre.textContent = '';\r\n    outcol.appendChild(outLabel);\r\n    outcol.appendChild(pre);\r\n\r\n    body.appendChild(src);\r\n    body.appendChild(outcol);\r\n    card.appendChild(body);\r\n\r\n    addCollapseToggle(card);\r\n    addProgressBar(card);\r\n    addCardControls(card, i);\r\n    return card;\r\n  }\r\n\r\n  function persistParts() {\r\n    try { window.localStorage.setItem(LS.saved, JSON.stringify({ parts: state.parts, results: state.results, html: state.html, font: state.font, lang: state.lang, shot: state.shot })); } catch (e) {}\r\n  }\r\n\r\n  function restoreParts() {\r\n    var raw = readLocal(LS.saved);\r\n    if (!raw) { return; }\r\n    var data;\r\n    try { data = JSON.parse(raw); } catch (e) { return; }\r\n    if (!data || !data.parts || !data.parts.length) { return; }\r\n    state.parts = data.parts;\r\n    state.results = data.results || [];\r\n    state.html = data.html || [];\r\n    state.font = data.font || [];\r\n    state.lang = data.lang || [];\r\n    state.shot = data.shot || [];\r\n    renderParts();\r\n    var i, card, gb;\r\n    for (i = 0; i < state.cards.length; i++) {\r\n      if (state.results[i]) {\r\n        card = state.cards[i];\r\n        if (state.html[i]) { card.querySelector('.pth-sb-pre').innerHTML = state.html[i]; }\r\n        else { card.querySelector('.pth-sb-pre').textContent = state.results[i]; }\r\n        card.querySelector('.pth-sb-stat').textContent = 'Done';\r\n        gb = card.querySelector('.pth-sb-genbtn');\r\n        if (gb) { gb.textContent = 'Regenerate'; }\r\n      }\r\n    }\r\n  }\r\n\r\n  function renderParts() {\r\n    els.output.textContent = '';\r\n    state.cards = [];\r\n    var bar = document.createElement('div');\r\n    bar.className = 'pth-sb-partbar';\r\n    var info = document.createElement('span');\r\n    info.className = 'pth-sb-partinfo';\r\n    info.textContent = state.parts.length + ' part(s) - generate them one by one.';\r\n    bar.appendChild(info);\r\n    var copyAll = document.createElement('button');\r\n    copyAll.type = 'button';\r\n    copyAll.className = 'pth-sb-btn';\r\n    copyAll.textContent = 'Copy all parts';\r\n    copyAll.addEventListener('click', function () {\r\n      var all = '', j;\r\n      for (j = 0; j < state.results.length; j++) {\r\n        if (state.results[j]) { all += '===== PART ' + (j + 1) + ' =====\\n' + state.results[j] + '\\n\\n'; }\r\n      }\r\n      if (all) { copyText(all); } else { toast('Generate at least one part first.'); }\r\n    });\r\n    bar.appendChild(copyAll);\r\n    var collapseAll = document.createElement('button');\r\n    collapseAll.type = 'button';\r\n    collapseAll.className = 'pth-sb-btn';\r\n    collapseAll.textContent = 'Collapse all';\r\n    collapseAll.addEventListener('click', function () { setAllCollapsed(true); });\r\n    bar.appendChild(collapseAll);\r\n    var expandAll = document.createElement('button');\r\n    expandAll.type = 'button';\r\n    expandAll.className = 'pth-sb-btn';\r\n    expandAll.textContent = 'Expand all';\r\n    expandAll.addEventListener('click', function () { setAllCollapsed(false); });\r\n    bar.appendChild(expandAll);\r\n    els.output.appendChild(bar);\r\n\r\n    var i, card;\r\n    for (i = 0; i < state.parts.length; i++) {\r\n      card = makeCard(i);\r\n      state.cards.push(card);\r\n      els.output.appendChild(card);\r\n    }\r\n  }\r\n\r\n  function rebuildAuxStore(storeName) {\r\n    var el = auxOutEl(storeName); if (!el) { return; }\r\n    var arr = [], kids = el.children, i, t, p;\r\n    for (i = 0; i < kids.length; i++) {\r\n      t = kids[i].querySelector('.pth-sb-cardtitle');\r\n      p = kids[i].querySelector('.pth-sb-pre');\r\n      if (t && p) { arr.push({ title: t.textContent, text: p.textContent }); }\r\n    }\r\n    arr.reverse();\r\n    auxStores[storeName] = arr;\r\n    persistAux();\r\n  }\r\n\r\n  function addAuxClear(card, storeName) {\r\n    var head = card.querySelector('.pth-sb-cardhead'); if (!head) { return; }\r\n    if (head.querySelector('.pth-sb-clrbtn')) { return; }\r\n    var b = document.createElement('button');\r\n    b.type = 'button';\r\n    b.className = 'pth-sb-btn pth-sb-clrbtn';\r\n    b.textContent = 'Clear';\r\n    b.addEventListener('click', function () {\r\n      if (card.parentNode) { card.parentNode.removeChild(card); }\r\n      rebuildAuxStore(storeName);\r\n      toast('Cleared.');\r\n    });\r\n    head.appendChild(b);\r\n  }\r\n\r\n  function persistAux() {\r\n    try { window.localStorage.setItem(LS.aux, JSON.stringify(auxStores)); } catch (e) {}\r\n  }\r\n\r\n  function auxOutEl(storeName) {\r\n    if (storeName === 'setup') { return els.setupout; }\r\n    if (storeName === 'char') { return els.charout; }\r\n    if (storeName === 'custom') { return els.customout; }\r\n    return els.toolsout;\r\n  }\r\n\r\n  function pushAux(storeName, title, text) {\r\n    var arr = auxStores[storeName]; if (!arr) { return; }\r\n    arr.push({ title: title, text: text });\r\n    if (arr.length > 30) { arr.shift(); }\r\n    persistAux();\r\n  }\r\n\r\n  function makeStaticCard(title, text) {\r\n    var card = document.createElement('div');\r\n    card.className = 'pth-sb-card';\r\n    var head = document.createElement('div');\r\n    head.className = 'pth-sb-cardhead';\r\n    var t = document.createElement('span');\r\n    t.className = 'pth-sb-cardtitle';\r\n    t.textContent = title;\r\n    head.appendChild(t);\r\n    var stat = document.createElement('span');\r\n    stat.className = 'pth-sb-stat';\r\n    stat.textContent = 'Done';\r\n    head.appendChild(stat);\r\n    var copyBtn = document.createElement('button');\r\n    copyBtn.type = 'button';\r\n    copyBtn.className = 'pth-sb-btn pth-sb-copybtn';\r\n    copyBtn.textContent = 'Copy';\r\n    head.appendChild(copyBtn);\r\n    var pre = document.createElement('pre');\r\n    pre.className = 'pth-sb-pre';\r\n    pre.textContent = text || '';\r\n    copyBtn.addEventListener('click', function () {\r\n      if (pre.textContent) { copyText(pre.textContent); } else { toast('Nothing to copy yet.'); }\r\n    });\r\n    card.appendChild(head);\r\n    card.appendChild(pre);\r\n    return card;\r\n  }\r\n\r\n  function restoreAux() {\r\n    var raw = readLocal(LS.aux); if (!raw) { return; }\r\n    var data; try { data = JSON.parse(raw); } catch (e) { return; }\r\n    if (!data) { return; }\r\n    var names = ['setup', 'char', 'custom', 'tools'], s, i, arr, el, card;\r\n    for (s = 0; s < names.length; s++) {\r\n      arr = data[names[s]] || [];\r\n      auxStores[names[s]] = arr;\r\n      el = auxOutEl(names[s]);\r\n      for (i = 0; i < arr.length; i++) {\r\n        card = makeStaticCard(arr[i].title || 'Result', arr[i].text || '');\r\n        addAuxClear(card, names[s]);\r\n        addCollapseToggle(card);\r\n        if (el.firstChild) { el.insertBefore(card, el.firstChild); } else { el.appendChild(card); }\r\n      }\r\n    }\r\n  }\r\n\r\n  function keyHelpCard() {\r\n    var steps = 'This tool runs on your OWN AI API key. It is never sent to any server - it stays in your browser.\\n\\n' +\r\n      'How to get a FREE Google Gemini key:\\n' +\r\n      '1. Open Google AI Studio (button below) and sign in with a Google account.\\n' +\r\n      '2. Click \"Create API key\" (or \"Get API key\") and copy the key.\\n' +\r\n      '3. Back here, keep Provider = Google Gemini (right-side panel).\\n' +\r\n      '4. Paste the key into the \"API key\" box.\\n' +\r\n      '5. If you see a \"model\" error, set the Model box to a current free model such as gemini-3.5-flash (or gemini-3.6-flash \/ gemini-3.5-flash-lite).\\n\\n' +\r\n      'Then click Generate again.';\r\n    var card = makeStaticCard('\ud83d\udd11 Add your API key to generate', steps);\r\n    card.querySelector('.pth-sb-stat').textContent = '';\r\n    var a = document.createElement('a');\r\n    a.className = 'pth-sb-btn';\r\n    a.setAttribute('href', 'https:\/\/aistudio.google.com\/apikey');\r\n    a.setAttribute('target', '_blank');\r\n    a.setAttribute('rel', 'noopener');\r\n    a.textContent = 'Get a free Gemini API key \\u2197';\r\n    a.style.margin = '0 14px 14px';\r\n    card.appendChild(a);\r\n    return card;\r\n  }\r\n\r\n  function requireKey(outEl) {\r\n    if (els.key.value.replace(\/^\\s+|\\s+$\/g, '')) { return true; }\r\n    var el = outEl || els.output;\r\n    var card = keyHelpCard();\r\n    if (el.firstChild) { el.insertBefore(card, el.firstChild); } else { el.appendChild(card); }\r\n    toast('Add your API key first (see the steps).');\r\n    return false;\r\n  }\r\n\r\n  function generatePart(i, done) {\r\n    var card = state.cards[i]; if (!card) { if (done) { done(false); } return; }\r\n    var btn = card.querySelector('.pth-sb-genbtn');\r\n    var stat = card.querySelector('.pth-sb-stat');\r\n    var pre = card.querySelector('.pth-sb-pre');\r\n    if (!requireKey(els.output)) { if (done) { done(false); } return; }\r\n    btn.disabled = true;\r\n    setLoading(card, true);\r\n    stat.textContent = 'Generating...';\r\n    pre.textContent = '';\r\n    var system = els.md.value || DEFAULT_MD;\r\n    var user = buildUserMessage(state.parts[i], i, state.parts.length, els.lang ? els.lang.value : 'story', state.shot[i] || '');\r\n    callProvider(system, user, function (err, text) {\r\n      btn.disabled = false;\r\n      setLoading(card, false);\r\n      if (err) {\r\n        stat.textContent = isModelError(err) ? 'Model error' : 'Error';\r\n        pre.textContent = err;\r\n        if (isModelError(err)) { renderModelHelp(card); }\r\n        toast('Part ' + (i + 1) + ' failed.');\r\n        if (done) { done(false); }\r\n        return;\r\n      }\r\n      stat.textContent = 'Done';\r\n      pre.textContent = text;\r\n      state.results[i] = text;\r\n      state.html[i] = '';\r\n      persistParts();\r\n      btn.textContent = 'Regenerate';\r\n      extractBible(text);\r\n      if (!done) {\r\n        if (i + 1 < state.parts.length) {\r\n          toast('Part ' + (i + 1) + ' done. Generate Part ' + (i + 2) + ' when ready.');\r\n        } else {\r\n          toast('All parts done.');\r\n        }\r\n      }\r\n      if (done) { done(true); }\r\n    });\r\n  }\r\n\r\n  function buildSetupMessage() {\r\n    var story = els.story.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var msg = 'STORY SETUP (this is PART 0). Do NOT generate any scenes. Provide, in order: ' +\r\n      '(1) Character Reference Image Prompts for EVERY main character (exact Sri Lankan features, 1970s clothing, neutral expression, front or 3\/4 view, plain WHITE background, EMPTY HANDS); ' +\r\n      '(2) a KEY OBJECTS \/ PROPS BIBLE listing every recurring object or prop that appears often in the story (e.g. lamps, cloths, ritual items, tools, vehicles) with a locked one-line look for consistency; ' +\r\n      '(3) the LOCATION BIBLE (each key location with a locked look). ' +\r\n      'At the very end, output ONE block starting with \"CHARACTER BIBLE:\" that lists every character, key object AND location with its locked one-line look, so it carries into every later part.';\r\n    if (story) { msg += '\\n\\nSTORY:\\n' + story; }\r\n    msg += langNote(els.lang.value);\r\n    return msg;\r\n  }\r\n\r\n  function onSetupGenerate() {\r\n    if (!requireKey(els.setupout)) { return; }\r\n    if (!els.story.value.replace(\/^\\s+|\\s+$\/g, '')) { toast('Paste your full story first.'); return; }\r\n    runIntoCard(makeResultCard('Part 0 Setup', 'characters + key objects + locations'), els.setupout, buildSetupMessage(), 'Part 0 setup', 'setup');\r\n  }\r\n\r\n  function buildCustomMessage(request) {\r\n    var ctx = els.context.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var story = els.story.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var msg = 'CUSTOM SCENE REQUEST. Output ONLY the scene prompt set (the numbered sections defined in the system instruction) for THIS specific scene \/ area. Do NOT re-output the Character Bible, the Location Bible, or the Phase 1 setup - assume they are already established (see the context below). Keep it fully consistent with the established story, characters and locations.';\r\n    if (ctx) { msg += '\\n\\nCONTEXT \/ CHARACTER & LOCATION BIBLE:\\n' + ctx; }\r\n    if (story) { msg += '\\n\\nSTORY (for reference \/ world consistency):\\n' + story; }\r\n    msg += '\\n\\n=== CUSTOM SCENE \/ AREA ===\\n' + request;\r\n    msg += langNote(els.lang.value);\r\n    return msg;\r\n  }\r\n\r\n  function buildCharMessage(request) {\r\n    var ctx = els.context.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var story = els.story.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var who = request ? ('for this character only: ' + request) : 'for ALL main characters in the story';\r\n    var msg = 'CHARACTER REFERENCE REQUEST. Output ONLY the Character Reference Image Prompt(s) ' + who + ', following the system instruction rules for character setup (exact Sri Lankan features, period 1970s clothing, neutral expression, front or 3\/4 view, full outfit, even soft studio lighting, plain WHITE background, EMPTY HANDS). Output NOTHING else - no scenes, no location bible, no video prompts, no dialogue, no ambient sound.';\r\n    if (ctx) { msg += '\\n\\nEXISTING CHARACTER BIBLE (stay consistent, do not contradict):\\n' + ctx; }\r\n    if (story) { msg += '\\n\\nSTORY (for reference):\\n' + story; }\r\n    if (request) { msg += '\\n\\n=== CHARACTER ===\\n' + request; }\r\n    msg += langNote(els.lang.value);\r\n    return msg;\r\n  }\r\n\r\n  function makeResultCard(prefix, request) {\r\n    var card = document.createElement('div');\r\n    card.className = 'pth-sb-card';\r\n    var head = document.createElement('div');\r\n    head.className = 'pth-sb-cardhead';\r\n    var title = document.createElement('span');\r\n    title.className = 'pth-sb-cardtitle';\r\n    var shortReq = request.length > 44 ? (request.substring(0, 44) + '...') : request;\r\n    title.textContent = prefix + ': ' + shortReq;\r\n    head.appendChild(title);\r\n    var stat = document.createElement('span');\r\n    stat.className = 'pth-sb-stat';\r\n    stat.textContent = 'Generating...';\r\n    head.appendChild(stat);\r\n    var copyBtn = document.createElement('button');\r\n    copyBtn.type = 'button';\r\n    copyBtn.className = 'pth-sb-btn pth-sb-copybtn';\r\n    copyBtn.textContent = 'Copy';\r\n    head.appendChild(copyBtn);\r\n    var pre = document.createElement('pre');\r\n    pre.className = 'pth-sb-pre';\r\n    pre.textContent = '';\r\n    copyBtn.addEventListener('click', function () {\r\n      if (pre.textContent) { copyText(pre.textContent); } else { toast('Nothing to copy yet.'); }\r\n    });\r\n    card.appendChild(head);\r\n    card.appendChild(pre);\r\n    return card;\r\n  }\r\n\r\n  function runIntoCard(card, outEl, userMsg, failLabel, storeName) {\r\n    if (outEl.firstChild) { outEl.insertBefore(card, outEl.firstChild); }\r\n    else { outEl.appendChild(card); }\r\n    if (storeName) { addAuxClear(card, storeName); }\r\n    addCollapseToggle(card);\r\n    addProgressBar(card);\r\n    setLoading(card, true);\r\n    var stat = card.querySelector('.pth-sb-stat');\r\n    var pre = card.querySelector('.pth-sb-pre');\r\n    var title = card.querySelector('.pth-sb-cardtitle').textContent;\r\n    var system = els.md.value || DEFAULT_MD;\r\n    callProvider(system, userMsg, function (err, text) {\r\n      setLoading(card, false);\r\n      if (err) {\r\n        stat.textContent = isModelError(err) ? 'Model error' : 'Error';\r\n        pre.textContent = err;\r\n        if (isModelError(err)) { renderModelHelp(card); }\r\n        toast(failLabel + ' failed.');\r\n        return;\r\n      }\r\n      stat.textContent = 'Done';\r\n      pre.textContent = text;\r\n      extractBible(text);\r\n      if (storeName) { pushAux(storeName, title, text); }\r\n      toast(failLabel + ' done.');\r\n    });\r\n  }\r\n\r\n  function onCustomGenerate() {\r\n    var req = els.custom.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    if (!req) { toast('Type a custom scene or area first.'); return; }\r\n    if (!requireKey(els.customout)) { return; }\r\n    runIntoCard(makeResultCard('Scene', req), els.customout, buildCustomMessage(req), 'Custom scene', 'custom');\r\n  }\r\n\r\n  function onCharGenerate() {\r\n    if (!requireKey(els.charout)) { return; }\r\n    var req = els.charreq.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var label = req ? req : 'all main characters';\r\n    runIntoCard(makeResultCard('Character', label), els.charout, buildCharMessage(req), 'Character', 'char');\r\n  }\r\n\r\n  function joinResults() {\r\n    var all = '', j;\r\n    for (j = 0; j < state.results.length; j++) {\r\n      if (state.results[j]) { all += 'PART ' + (j + 1) + ':\\n' + state.results[j] + '\\n\\n'; }\r\n    }\r\n    return all;\r\n  }\r\n\r\n  function prependToTools(card) {\r\n    if (els.toolsout.firstChild) { els.toolsout.insertBefore(card, els.toolsout.firstChild); }\r\n    else { els.toolsout.appendChild(card); }\r\n  }\r\n\r\n  function extractVoiceover() {\r\n    if (!state.results.length) { toast('Generate some parts first.'); return; }\r\n    var lines = [], i, r, m, re;\r\n    for (i = 0; i < state.results.length; i++) {\r\n      r = state.results[i]; if (!r) { continue; }\r\n      re = \/speaking:\\s*\"([^\"]+)\"\/g;\r\n      while ((m = re.exec(r)) !== null) {\r\n        var line = m[1].replace(\/^\\s+|\\s+$\/g, '');\r\n        if (line) { lines.push(line); }\r\n      }\r\n    }\r\n    var card = makeResultCard('Voiceover', 'all dialogue lines');\r\n    prependToTools(card);\r\n    addAuxClear(card, 'tools');\r\n    addCollapseToggle(card);\r\n    card.querySelector('.pth-sb-stat').textContent = 'Done';\r\n    var vtext = lines.length ? lines.join('\\n') : 'No spoken dialogue lines were found in the generated parts.';\r\n    card.querySelector('.pth-sb-pre').textContent = vtext;\r\n    pushAux('tools', card.querySelector('.pth-sb-cardtitle').textContent, vtext);\r\n    toast(lines.length + ' dialogue line(s) extracted.');\r\n  }\r\n\r\n  function downloadAll() {\r\n    var all = joinResults();\r\n    if (!all) { toast('Generate at least one part first.'); return; }\r\n    try {\r\n      var blob = new window.Blob([all], { type: 'text\/markdown;charset=utf-8' });\r\n      var url = window.URL.createObjectURL(blob);\r\n      var a = document.createElement('a');\r\n      a.href = url;\r\n      a.download = 'storyboard.md';\r\n      document.body.appendChild(a);\r\n      a.click();\r\n      document.body.removeChild(a);\r\n      window.setTimeout(function () { window.URL.revokeObjectURL(url); }, 1000);\r\n      toast('Downloaded storyboard.md');\r\n    } catch (e) { toast('Download is not supported in this browser.'); }\r\n  }\r\n\r\n  function genNextRemaining(from) {\r\n    var i;\r\n    for (i = from; i < state.parts.length; i++) { if (!state.results[i]) { break; } }\r\n    if (i >= state.parts.length) { toast('All parts generated.'); return; }\r\n    generatePart(i, function (ok) {\r\n      if (ok) { window.setTimeout(function () { genNextRemaining(i + 1); }, 700); }\r\n      else { toast('Stopped at Part ' + (i + 1) + '.'); }\r\n    });\r\n  }\r\n\r\n  function generateAllRemaining() {\r\n    if (!state.parts.length) { toast('Split the story into parts first.'); return; }\r\n    if (!requireKey(els.output)) { return; }\r\n    toast('Generating all remaining parts...');\r\n    genNextRemaining(0);\r\n  }\r\n\r\n  function buildShotListMessage() {\r\n    var joined = joinResults();\r\n    var msg = 'Produce a clean, numbered MASTER SHOT LIST for a video editor from the storyboard below. One line per shot in the format: Shot # | Part\/Scene | Location | Shot type | Expected time. Keep it concise and in order.';\r\n    if (joined) { msg += '\\n\\nSTORYBOARD:\\n' + joined; }\r\n    else { msg += '\\n\\nSTORY:\\n' + els.story.value.replace(\/^\\s+|\\s+$\/g, ''); }\r\n    return msg;\r\n  }\r\n\r\n  function onShotList() {\r\n    if (!requireKey(els.toolsout)) { return; }\r\n    runIntoCard(makeResultCard('Master Shot List', 'editor-ready'), els.toolsout, buildShotListMessage(), 'Shot list', 'tools');\r\n  }\r\n\r\n  function buildThumbMessage() {\r\n    var story = els.story.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var msg = 'Generate ONE gore-free, advertiser-friendly YouTube THUMBNAIL image prompt for this Sinhala horror story. Requirements: atmospheric implied dread only (absolutely NO blood, NO gore, NO sexual or suggestive content), one clear focal subject (a shadowy figure, a single scared face, or the village at night), 1970s cinematic aesthetic, heavy film grain, dark moody low-key lighting, and leave clear visual space on one side for large title text. 16:9. Output the English image prompt only.';\r\n    if (story) { msg += '\\n\\nSTORY:\\n' + story; }\r\n    return msg;\r\n  }\r\n\r\n  function onThumb() {\r\n    if (!requireKey(els.toolsout)) { return; }\r\n    runIntoCard(makeResultCard('Thumbnail prompt', 'gore-free'), els.toolsout, buildThumbMessage(), 'Thumbnail', 'tools');\r\n  }\r\n\r\n  function buildSeoMessage() {\r\n    var platform = els.seoplatform.value;\r\n    var kw = els.seokw.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var story = els.story.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    var msg = 'Generate SEO metadata for this Sinhala horror story video, optimized for ' + platform + '. Output clearly labelled: ' +\r\n      '(1) TITLE - mixed Sinhala + English, curiosity-driven, honest (no misleading claims), sized for ' + platform + '; ' +\r\n      '(2) DESCRIPTION - mixed Sinhala + English, first 1-2 lines punchy with the main keyword, then a short setup, then a like\/subscribe call-to-action; ' +\r\n      '(3) TAGS - 15-20 relevant tags on ONE single line separated by commas (ready to paste straight into YouTube), a mix of Sinhala + English; ' +\r\n      '(4) PROPOSED KEYWORDS - 5-8 additional keywords or hashtags worth targeting on ' + platform + '.';\r\n    if (kw) { msg += ' Use \"' + kw + '\" as the MAIN focus keyword throughout.'; }\r\n    else { msg += ' No focus keyword was given - propose the best main keyword yourself and build around it.'; }\r\n    if (story) { msg += '\\n\\nSTORY:\\n' + story; }\r\n    return msg;\r\n  }\r\n\r\n  function onSeo() {\r\n    if (!requireKey(els.toolsout)) { return; }\r\n    if (!els.story.value.replace(\/^\\s+|\\s+$\/g, '')) { toast('Paste your full story first.'); return; }\r\n    var kw = els.seokw.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    runIntoCard(makeResultCard('SEO ' + els.seoplatform.value, kw ? kw : 'auto keyword'), els.toolsout, buildSeoMessage(), 'SEO', 'tools');\r\n  }\r\n\r\n  function onGenerate() {\r\n    var story = els.story.value.replace(\/^\\s+|\\s+$\/g, '');\r\n    if (!story) { toast('Paste your full story first.'); return; }\r\n    var chunk = parseInt(els.chunk.value, 10);\r\n    if (!chunk || chunk < 50) { chunk = 100; els.chunk.value = 100; }\r\n    state.parts = splitParts(story, chunk);\r\n    state.results = [];\r\n    state.html = [];\r\n    state.font = [];\r\n    state.lang = [];\r\n    state.shot = [];\r\n    renderParts();\r\n    persistParts();\r\n    toast(state.parts.length + ' parts ready \u2014 click Generate on each part.');\r\n  }\r\n\r\n  function loadSample() {\r\n    els.story.value = (els.preset.value === 'general') ? SAMPLE_GENERAL : SAMPLE_HORROR;\r\n    toast('Sample story loaded.');\r\n  }\r\n\r\n  function clearAll() {\r\n    els.story.value = '';\r\n    els.context.value = '';\r\n    els.custom.value = '';\r\n    els.charreq.value = '';\r\n    els.customout.textContent = '';\r\n    els.charout.textContent = '';\r\n    els.setupout.textContent = '';\r\n    els.toolsout.textContent = '';\r\n    els.seokw.value = '';\r\n    els.output.textContent = '';\r\n    state.parts = []; state.results = []; state.cards = []; state.html = []; state.font = []; state.lang = []; state.shot = [];\r\n    auxStores = { setup: [], char: [], custom: [], tools: [] };\r\n    var keys = [LS.story, LS.custom, LS.context, LS.charreq, LS.saved, LS.seokw, LS.aux];\r\n    var i; for (i = 0; i < keys.length; i++) { try { window.localStorage.removeItem(keys[i]); } catch (e) {} }\r\n    toast('Cleared.');\r\n  }\r\n\r\n  function toggleFullWidth() {\r\n    var w = $('pth-storyboard-wrapper');\r\n    if (!w) { return; }\r\n    var on = w.className.indexOf('pth-sb-full') === -1;\r\n    w.className = on ? 'pth-sb pth-sb-full' : 'pth-sb';\r\n    if (document.body) {\r\n      if (on) { document.body.className += ' pth-sb-locked'; }\r\n      else { document.body.className = document.body.className.replace(\/\\s*pth-sb-locked\/g, ''); }\r\n    }\r\n    var btn = $('pth-sb-fullwidth');\r\n    if (btn) { btn.textContent = on ? '\\u26F6 Exit Full Width' : '\\u26F6 Full Width'; }\r\n  }\r\n\r\n  function onMdFile() {\r\n    var f = els.mdfile.files && els.mdfile.files[0];\r\n    if (!f) { return; }\r\n    var reader = new window.FileReader();\r\n    reader.onload = function (e) {\r\n      els.md.value = e.target.result || '';\r\n      saveLocal(LS.md, els.md.value);\r\n      toast('Custom instruction loaded and saved.');\r\n    };\r\n    reader.onerror = function () { toast('Could not read that file.'); };\r\n    reader.readAsText(f);\r\n  }\r\n\r\n  function addProgressBar(card) {\r\n    if (card.querySelector('.pth-sb-progress')) { return; }\r\n    var head = card.querySelector('.pth-sb-cardhead'); if (!head) { return; }\r\n    var p = document.createElement('div');\r\n    p.className = 'pth-sb-progress';\r\n    var inner = document.createElement('div');\r\n    inner.className = 'pth-sb-progress-inner';\r\n    p.appendChild(inner);\r\n    if (head.nextSibling) { card.insertBefore(p, head.nextSibling); } else { card.appendChild(p); }\r\n  }\r\n\r\n  function setLoading(card, on) {\r\n    if (on) { if (card.className.indexOf('pth-sb-loading') < 0) { card.className += ' pth-sb-loading'; } }\r\n    else { card.className = card.className.replace(\/\\s*pth-sb-loading\/, ''); }\r\n  }\r\n\r\n  function langNote(lang) {\r\n    var x = lang;\r\n    if (x === undefined) { x = els.lang ? els.lang.value : ''; }\r\n    if (!x || x === 'story' || x === 'global') { return ''; }\r\n    return '\\n\\nWrite ALL explanation text (the Sinhala Idea line, the strategy\/division reasoning, and ambient notes) in ' + x + '. Keep the actual image and video prompts in English.';\r\n  }\r\n\r\n  function applyFont() {\r\n    var x = parseInt(readLocal(LS.font), 10);\r\n    if (!x) { x = 14; }\r\n    if (x < 11) { x = 11; }\r\n    if (x > 24) { x = 24; }\r\n    var w = document.getElementById('pth-storyboard-wrapper');\r\n    if (w) { w.style.setProperty('--pth-font', x + 'px'); }\r\n    return x;\r\n  }\r\n\r\n  function stepFont(delta) {\r\n    var x = parseInt(readLocal(LS.font), 10);\r\n    if (!x) { x = 14; }\r\n    x += delta;\r\n    if (x < 11) { x = 11; }\r\n    if (x > 24) { x = 24; }\r\n    saveLocal(LS.font, '' + x);\r\n    applyFont();\r\n  }\r\n\r\n  function applyColSplit() {\r\n    var x = parseInt(els.colsplit.value, 10);\r\n    if (!x) { x = 50; }\r\n    var w = document.getElementById('pth-storyboard-wrapper');\r\n    if (w) {\r\n      w.style.setProperty('--pth-col-left', x + '%');\r\n      w.style.setProperty('--pth-col-right', (100 - x) + '%');\r\n    }\r\n  }\r\n\r\n  function init() {\r\n    els.provider = $('pth-sb-provider');\r\n    els.key = $('pth-sb-key');\r\n    els.model = $('pth-sb-model');\r\n    els.chunk = $('pth-sb-chunk');\r\n    els.preset = $('pth-sb-preset');\r\n    els.md = $('pth-sb-md');\r\n    els.mdfile = $('pth-sb-mdfile');\r\n    els.context = $('pth-sb-context');\r\n    els.story = $('pth-sb-story');\r\n    els.setupout = $('pth-sb-setupout');\r\n    els.charreq = $('pth-sb-charreq');\r\n    els.charout = $('pth-sb-charout');\r\n    els.custom = $('pth-sb-custom');\r\n    els.customout = $('pth-sb-customout');\r\n    els.output = $('pth-sb-output');\r\n    els.toolsout = $('pth-sb-toolsout');\r\n    els.seoplatform = $('pth-sb-seoplatform');\r\n    els.seokw = $('pth-sb-seokw');\r\n    els.fullctx = $('pth-sb-fullctx');\r\n    els.colsplit = $('pth-sb-colsplit');\r\n    els.lang = $('pth-sb-lang');\r\n    els.toast = $('pth-sb-toast');\r\n    els.modelhelp = $('pth-sb-modelhelp');\r\n\r\n    if (!els.story) { return; }\r\n\r\n    loadSettings();\r\n    updateModelHelp();\r\n    applyColSplit();\r\n    applyFont();\r\n    restoreParts();\r\n    restoreAux();\r\n\r\n    \/\/ Event Listeners for the Language Modal\r\n    var closeBtn = $('pth-lang-close-btn');\r\n    if (closeBtn) { closeBtn.addEventListener('click', closeLangModal); }\r\n    var modalEl = $('pth-lang-modal');\r\n    if (modalEl) {\r\n      modalEl.addEventListener('click', function(e) {\r\n        if (e.target === modalEl) { closeLangModal(); }\r\n      });\r\n    }\r\n    var searchEl = $('pth-lang-search');\r\n    if (searchEl) { searchEl.addEventListener('keyup', filterLanguages); }\r\n\r\n    $('pth-sb-generate').addEventListener('click', onGenerate);\r\n    $('pth-sb-setupgen').addEventListener('click', onSetupGenerate);\r\n    $('pth-sb-voiceover').addEventListener('click', extractVoiceover);\r\n    $('pth-sb-downloadall').addEventListener('click', downloadAll);\r\n    $('pth-sb-genall').addEventListener('click', generateAllRemaining);\r\n    $('pth-sb-shotlist').addEventListener('click', onShotList);\r\n    $('pth-sb-thumb').addEventListener('click', onThumb);\r\n    $('pth-sb-seogen').addEventListener('click', onSeo);\r\n    els.seokw.addEventListener('input', function () { saveLocal(LS.seokw, els.seokw.value); });\r\n    els.seoplatform.addEventListener('change', function () { saveLocal(LS.seoplatform, els.seoplatform.value); });\r\n    els.fullctx.addEventListener('change', function () { saveLocal(LS.fullctx, els.fullctx.checked ? '1' : '0'); });\r\n    els.colsplit.addEventListener('input', function () { saveLocal(LS.colsplit, els.colsplit.value); applyColSplit(); });\r\n    els.lang.addEventListener('change', function () { saveLocal(LS.lang, els.lang.value); });\r\n    $('pth-sb-fontminus').addEventListener('click', function () { stepFont(-1); });\r\n    $('pth-sb-fontplus').addEventListener('click', function () { stepFont(1); });\r\n    $('pth-sb-customgen').addEventListener('click', onCustomGenerate);\r\n    $('pth-sb-chargen').addEventListener('click', onCharGenerate);\r\n    $('pth-sb-sample').addEventListener('click', loadSample);\r\n    $('pth-sb-fullwidth').addEventListener('click', toggleFullWidth);\r\n    $('pth-sb-clear').addEventListener('click', clearAll);\r\n\r\n    els.provider.addEventListener('change', function () {\r\n      saveLocal(LS.provider, els.provider.value);\r\n      applyProviderDefaults(els.provider.value);\r\n      updateModelHelp();\r\n    });\r\n    els.key.addEventListener('input', function () { saveLocal(LS.key + currentProvider(), els.key.value); });\r\n    els.model.addEventListener('input', function () { saveLocal(LS.model + currentProvider(), els.model.value); });\r\n    els.md.addEventListener('input', function () { saveLocal(LS.md, els.md.value); });\r\n    els.preset.addEventListener('change', function () { saveLocal(LS.preset, els.preset.value); });\r\n    els.story.addEventListener('input', function () { saveLocal(LS.story, els.story.value); });\r\n    els.custom.addEventListener('input', function () { saveLocal(LS.custom, els.custom.value); });\r\n    els.charreq.addEventListener('input', function () { saveLocal(LS.charreq, els.charreq.value); });\r\n    els.context.addEventListener('input', function () { saveLocal(LS.context, els.context.value); });\r\n    els.mdfile.addEventListener('change', onMdFile);\r\n    $('pth-sb-mdreset').addEventListener('click', function () {\r\n      try { window.localStorage.removeItem(LS.md); } catch (e) {}\r\n      els.md.value = DEFAULT_MD;\r\n      fetchDefaultMd(function (t) { if (t) { els.md.value = t; } });\r\n      toast('Instruction reset to the default.');\r\n    });\r\n  }\r\n\r\n  function boot() {\r\n    if (window[APP].inited) { return; }\r\n    if (!document.getElementById('pth-storyboard-wrapper')) { return; }\r\n    window[APP].inited = true;\r\n    init();\r\n  }\r\n\r\n  boot();\r\n  if (document.addEventListener) { document.addEventListener('DOMContentLoaded', boot); }\r\n  window.addEventListener('load', function () { window.setTimeout(boot, 50); });\r\n  if (window.MutationObserver) {\r\n    var mo = new window.MutationObserver(function () {\r\n      boot();\r\n      if (window[APP].inited) { mo.disconnect(); }\r\n    });\r\n    try { mo.observe(document.documentElement || document.body, { childList: true, subtree: true }); } catch (e) {}\r\n    window.setTimeout(function () { try { mo.disconnect(); } catch (e2) {} }, 10000);\r\n  }\r\n})();\r\n<\/script>\r\n<!-- \/noptimize -->\n\n\n\n<style>\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);transition:all .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.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-feature-wrap\">\n  <div class=\"pth-feat-box\">\n    <p class=\"pth-feat-title\"><span aria-hidden=\"true\">\ud83d\udfe2<\/span> One Story In, a Whole Shot List Out<\/p>\n    <p class=\"pth-feat-text\">Paste a full script and the tool splits it into ordered parts, then turns each into a self-contained prompt built for text-to-video models \u2014 subject, setting, camera, lighting and action written the way the models actually respond to. You go from a wall of prose to a numbered sequence you can feed a generator shot by shot, in the right order.<\/p>\n  <\/div>\n  <div class=\"pth-feat-box\">\n    <p class=\"pth-feat-title\"><span aria-hidden=\"true\">\ud83d\udd35<\/span> A Character Bible That Keeps Faces Consistent<\/p>\n    <p class=\"pth-feat-text\">The hardest problem in AI video is that the model forgets what your character looks like between clips. This tool maintains a running <strong>Character Bible<\/strong> \u2014 a locked one-line description of each character and setting that is carried into every later scene, so the same person keeps the same face, clothes and features across the whole story instead of mutating shot to shot.<\/p>\n  <\/div>\n  <div class=\"pth-feat-box\">\n    <p class=\"pth-feat-title\"><span aria-hidden=\"true\">\ud83d\udfe3<\/span> Voiceover, Shot List and SEO Too<\/p>\n    <p class=\"pth-feat-text\">Beyond scene prompts it extracts a clean <strong>voiceover script<\/strong>, builds a <strong>master shot list<\/strong>, drafts a <strong>thumbnail prompt<\/strong>, and generates <strong>SEO<\/strong> titles, description and tags for the finished upload. Platform presets for YouTube, Facebook, TikTok and Instagram set the right aspect ratio and pacing for where the video is going.<\/p>\n  <\/div>\n<\/div>\n \n<div class=\"pth-steps-wrap\">\n  <p class=\"pth-steps-title\">How to Use the Story-to-Storyboard Generator<\/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\">Connect Your Key<\/p>\n      <p class=\"pth-step-desc\">Pick your AI provider \u2014 Gemini, OpenAI or Claude \u2014 and paste your own API key. Gemini and Groq-class free tiers make this workable at no cost for most creators.<\/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\">Run Part 0 First<\/p>\n      <p class=\"pth-step-desc\">Paste your story, choose a platform and preset, then run the setup step. This establishes the Character Bible and running context that every later scene will draw from.<\/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\">Split and Generate<\/p>\n      <p class=\"pth-step-desc\">Split the story into parts, generate character references, then produce scene prompts \u2014 one at a time or all remaining at once. Add a custom scene for any moment you want handled specially.<\/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 the Package<\/p>\n      <p class=\"pth-step-desc\">Pull the voiceover, master shot list, thumbnail prompt and SEO, then download everything. Take the prompts to your video model and the voiceover to your narration workflow.<\/p>\n    <\/div>\n  <\/div>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Last updated: Octomber 2026<\/p>\n\n\n\n<h2 id=\"\ud83d\udd34-the-real-problem-your-character-changes-every-shot\" class=\"wp-block-heading\">\ud83d\udd34&nbsp;<strong>The Real Problem: Your Character Changes Every Shot<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Anyone who has tried to make a multi-scene video with AI tools has hit the same wall. You generate a clip of your protagonist \u2014 a woman in a red coat, dark hair, sharp features. It looks great. You generate the next clip, and she has blonde hair, a different face, and the coat is now brown. Text-to-video models have no memory between generations. Each clip is produced fresh, and unless you feed the model the exact same description every time, it invents a new person.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the single biggest thing standing between a nice one-off clip and an actual watchable story. A story needs the same character to be recognisably the same across a dozen shots, and the models will not do that on their own. Solving it is not about a better single prompt \u2014 it is about&nbsp;<em>consistency management<\/em>&nbsp;across the whole sequence, and that is what this tool is built around.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The mechanism is the&nbsp;<strong>Character Bible<\/strong>. When you run the setup step, the tool reads your whole story and locks down a short, fixed description of each recurring character and key setting \u2014 one line each, deliberately terse so it can be repeated verbatim. Every scene prompt it then generates carries those exact descriptions forward. The model sees the same &#8220;woman, late 20s, shoulder-length black hair, red wool coat&#8221; in shot 1 and shot 11, so it draws the same person both times. It is the same trick a human show-runner uses with a series bible, applied to a machine that has no memory.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe1-why-split-a-story-into-parts-at-all\" class=\"wp-block-heading\">\ud83d\udfe1&nbsp;<strong>Why Split a Story Into Parts At All?<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Current AI video tools generate short clips \u2014 a handful of seconds each. You cannot hand one a five-minute story and get a film back. So the real production workflow is: break the story into short beats, generate each as its own clip, and edit them together. The splitting is not busywork; it mirrors how the video actually has to be made.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Doing that well by hand is tedious and easy to get wrong. Each beat needs to be the right length for one clip, needs a prompt that stands on its own, and needs to carry the visual continuity from the last shot. The tool handles the decomposition \u2014 you set a target part size, and it divides the narrative into scene-sized pieces, each emerging as a complete prompt with the subject, setting, camera framing, lighting and action spelled out. A neat detail baked into the guidance it follows: ending a clip on black makes continuity between clips far more forgiving, so the seams do not show when you cut them together.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If instead of a whole story you just want to nail&nbsp;<em>one<\/em>&nbsp;perfect shot \u2014 dialing in the lens, the grade, the film stock \u2014 that is a different job, and the companion&nbsp;AI Cinematic Prompt Generator&nbsp;is built for exactly that, with a structured vocabulary of film and photography terms and a shot-list tab that holds one look steady across variations. Use this Story-to-Storyboard tool to break a narrative into a consistent sequence; use that one to perfect an individual frame.<\/p>\n\n\n\n<h2 id=\"\ud83d\udd34-an-honest-word-this-tool-sends-your-story-to-an-ai\" class=\"wp-block-heading\">\ud83d\udd34&nbsp;<strong>An Honest Word: This Tool Sends Your Story to an AI<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most tools on this site run entirely in your browser with nothing leaving your machine. This one cannot, and it would be wrong to imply otherwise. Turning a story into scene prompts is a job only a large language model can do, and those models run on the provider&#8217;s servers. When you generate, your story text is sent over the internet to whichever provider you chose.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So the accurate privacy statement is specific: your API key and your story go directly from your browser to the AI provider you selected \u2014 Google, OpenAI or Anthropic \u2014 and never pass through a schoolict server. What the provider does with that text is governed by&nbsp;<em>its<\/em>&nbsp;data policy, not ours. If your script is unpublished or commercially sensitive, read the data-retention terms of the provider you pick before pasting it in. This is the same honest trade covered for the&nbsp;<a href=\"https:\/\/schoolict.net\/free-web-tools-by-category\/\">other browser-based AI tools in the collection<\/a>: bring-your-own-key removes the middleman, it does not make the work local.<\/p>\n\n\n\n<h3 id=\"\ud83d\udfe2-bring-your-own-key-pick-your-provider\" class=\"wp-block-heading\">\ud83d\udfe2&nbsp;<strong>Bring Your Own Key, Pick Your Provider<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You supply the API key, which means you are on your own free or paid tier with the provider directly, and there is no subscription to this tool. Each provider has a personality worth matching to the task: the free Gemini tier is fast and capable and the sensible default; OpenAI&#8217;s smaller models are strong all-rounders; and Claude tends to hold a long, complex story in mind well, which matters when the Character Bible has to stay coherent across many scenes. Because model names change often, the tool ships current defaults with the option to override \u2014 do not be surprised when the exact version shifts over time.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe1-built-with-sinhala-horror-creators-in-mind\" class=\"wp-block-heading\">\ud83d\udfe1&nbsp;<strong>Built With Sinhala Horror Creators in Mind<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One of the built-in presets is tuned for Sinhala horror storytelling \u2014 the Holman Katha style familiar to Sri Lankan YouTube audiences. Horror is a genre where character and setting consistency matters more than most: the dread depends on the same house, the same figure, the same darkness recurring and building. A protagonist whose face changes halfway through breaks the spell entirely. The preset leans the prompts toward the mood, pacing and framing that suit the genre, so a creator working in Sinhala horror starts far closer to the right output than a generic cinematic setting would allow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It is a small example of a larger point: local creators are not well served by tools built only around English-language, Western-genre assumptions, and a preset that understands a specific storytelling tradition saves a great deal of manual prompt-wrangling. The general cinematic preset remains there for everything else.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe2-everything-the-upload-needs-not-just-the-visuals\" class=\"wp-block-heading\">\ud83d\udfe2&nbsp;<strong>Everything the Upload Needs, Not Just the Visuals<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A finished video is more than its shots, and the tool produces the surrounding pieces so you are not assembling them by hand afterwards. The&nbsp;<strong>voiceover extraction<\/strong>&nbsp;pulls a clean narration script out of the story, separated from the visual directions, ready to record or feed to a text-to-speech step. The&nbsp;<strong>master shot list<\/strong>&nbsp;gives you the whole sequence at a glance for planning and editing. The&nbsp;<strong>thumbnail prompt<\/strong>&nbsp;drafts a strong key image for the click. And the&nbsp;<strong>SEO generator<\/strong>&nbsp;writes the title, description and tags the platform will index.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Taken together, that turns the tool from a prompt toy into a pre-production assistant: story in, and out comes an ordered prompt sequence with locked characters, a narration script, a shot list, a thumbnail concept and the metadata for the upload. The generation still happens in your video model of choice and the edit still happens in your editor \u2014 but the planning, the part that usually eats an evening, is done. The rest of the site&#8217;s creator and developer utilities are indexed in the&nbsp;<a href=\"https:\/\/schoolict.net\/school-ict-tools-directory\/\">tools directory<\/a>.<\/p>\n\n\n\n<style>\n.pth-faq-section{margin-top:50px;margin-bottom:40px;font-family:inherit}\n.pth-faq-header{font-size:1.8rem;font-weight:800;color:#0f172a;margin-bottom:25px;border-bottom:2px solid #e2e8f0;padding-bottom:10px;display:flex;align-items:center;gap:10px}\n.pth-faq-grid{display:grid;grid-template-columns:100%;gap:20px}\n@media(min-width:768px){.pth-faq-grid{grid-template-columns:repeat(2,1fr)}}\n@media(min-width:1024px){.pth-faq-grid{grid-template-columns:repeat(3,1fr)}}\n.pth-faq-card{background:#f8fafc;padding:24px;border-radius:12px;border:1px solid #e2e8f0;transition:transform .2s ease;break-inside:avoid}\n.pth-faq-card:hover{transform:translateY(-3px);box-shadow:0 4px 12px rgba(0,0,0,.05)}\n.pth-faq-q{color:#0f172a;font-size:1rem;font-weight:700;margin:0 0 12px;line-height:1.4}\n.pth-faq-a{margin:0;font-size:.95rem;color:#1e293b;line-height:1.6;font-weight:500}\n<\/style>\n<div class=\"pth-faq-section\">\n  <div class=\"pth-faq-header\"><span aria-hidden=\"true\">\u2753<\/span> Frequently Asked Questions<\/div>\n  <div class=\"pth-faq-grid\">\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Does this tool make the video for me?<\/p>\n      <p class=\"pth-faq-a\">No. It produces the prompts, voiceover script, shot list and SEO. You take the scene prompts to a text-to-video model (Sora, Runway, Kling and the like) to generate the actual clips, then edit them together yourself.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">How does it keep my character looking the same?<\/p>\n      <p class=\"pth-faq-a\">Through a running Character Bible: a fixed one-line description of each character and setting, established up front and carried verbatim into every scene prompt, so the model draws the same person each time instead of inventing a new one.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Is it free?<\/p>\n      <p class=\"pth-faq-a\">The tool is free with no subscription. It uses your own AI API key, and providers like Gemini offer a free tier with no card, so most creators pay nothing. Heavier use may draw on a paid tier with your provider directly.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Is my story kept private?<\/p>\n      <p class=\"pth-faq-a\">Your story goes directly from your browser to the AI provider you choose, never through a schoolict server. But it does leave your machine \u2014 it has to reach the model. For sensitive scripts, check the provider&#8217;s data-retention policy first.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Why must I run Part 0 before anything else?<\/p>\n      <p class=\"pth-faq-a\">Part 0 reads the whole story and builds the Character Bible and running context. Every later scene depends on it. Skip it and the tool has no locked descriptions to carry forward, so consistency breaks down.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Which video tools do the prompts work with?<\/p>\n      <p class=\"pth-faq-a\">The prompts are written in the plain, structured style that text-to-video models respond to, so they suit the major generators. You may still tweak wording for a specific tool, but the subject, camera and lighting detail transfers well.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">What is the difference from the Cinematic Prompt Generator?<\/p>\n      <p class=\"pth-faq-a\">This tool breaks a whole story into a consistent sequence of scene prompts. The Cinematic Prompt Generator perfects a single shot from a controlled film vocabulary. Use this for a narrative, that one for one frame.<\/p>\n    <\/div>\n    <div class=\"pth-faq-card\">\n      <p class=\"pth-faq-q\">Can it handle Sinhala horror stories?<\/p>\n      <p class=\"pth-faq-a\">Yes \u2014 there is a preset tuned for Sinhala horror (Holman Katha) storytelling, which leans the prompts toward the mood and pacing of the genre. A general cinematic preset is there for everything else.<\/p>\n    <\/div>\n  <\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>AI Story-to-Storyboard Generator Turn a full story or script into sequential, shot-by-shot prompts for AI video tools. Keeps characters consistent across every scene with a running Character Bible, extracts voiceover, builds a master shot list , and generates SEO. Bring your own key (Gemini, OpenAI or Claude). \ud83d\udfe2 One Story In, a Whole Shot List &#8230; <a title=\"AI Storyboard Generator\" class=\"read-more\" href=\"https:\/\/schoolict.net\/tools\/ai-storyboard-generator\/\" aria-label=\"Read more about AI Storyboard Generator\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-7063","page","type-page","status-publish"],"_links":{"self":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/7063","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=7063"}],"version-history":[{"count":4,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/7063\/revisions"}],"predecessor-version":[{"id":7129,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/7063\/revisions\/7129"}],"wp:attachment":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media?parent=7063"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}