diff --git a/CHANGELOG.md b/CHANGELOG.md index c48a039..dce9311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.20.36] — 2026-08-15 + +### Added +- **Character image workflow is now directly editable.** Click a character avatar or profile image to open a large editor with local upload, online-image download, and regeneration controls. Concept art opens in a wider preview/editor with the same three actions. +- **Voice review is now faster in character profiles.** A compact blue play/stop control sits beside the assigned voice, and the adjacent circular regenerate control opens the editable Voice Design prompt before creating a replacement voice. +- **Character table view now mirrors the Voice Library’s review data.** Each assigned voice has its own editable star rating and note, plus a Voice Design Prompt column sourced from the voice metadata (with the character-sheet prompt as fallback). +- **Book context is generated during character-sheet casting when it is missing.** Genre, setting, era and language are shared with the profile, image, concept-art and voice-prompt workflows. +- **Selective character casting has a larger, readable chooser.** The action is now labelled **“Select characters to cast or recast roles”**, and its list keeps names and line counts aligned. + +### Changed +- **Voice design prompts now use audible identity constraints.** They are concise English descriptions capped at 480 characters for CosyVoice compatibility, with explicit gender, age, native-language delivery, pitch/timbre/pace and distinctive vocal identity. Species and roles are translated into practical sound cues (for example Orc, vampire, elf, warrior, blacksmith and mage) rather than passed as lore labels alone. +- **Concept-art prompt construction better anchors profile facts.** Gender, species, physical features, clothing, signature props and the book context are carried into a consistent front/side/back character-reference prompt. +- **ElevenLabs voice imports remain on Get a Voice Online after importing**, enabling several voices to be collected in succession rather than forcing a jump back to the voice library. + +### Fixed +- **Studio casting controls could disappear completely.** The Studio toolbar used to hide its own controls after borrowing the casting footer; an asynchronous footer rebuild could then leave an empty bar. The real Identify / Cast Characters / Cast submenu bar is now relocated only after it has fully rendered, while the simple fallback controls remain visible until then. +- **Selective casting could generate the wrong character.** Alias matching could resolve the selected roster name to another character’s profile. The selected sidebar name is now authoritative when a profile is generated and saved. +- **Editing a Voice Design prompt could leave the character detail page blank.** The detail page now re-renders from the saved character record after generation instead of rebuilding from stale in-memory data. +- **Google image generation reports provider failures instead of silently falling back**, and character-image generation skips sparse profiles rather than producing generic placeholder portraits. + ## [1.20.8] — 2026-08-12 ### Added diff --git a/VERSION b/VERSION index 4261a51..381d42e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.35 +1.20.36 diff --git a/routes/conversation.py b/routes/conversation.py index f0e3a19..f783819 100644 --- a/routes/conversation.py +++ b/routes/conversation.py @@ -685,7 +685,7 @@ def _charsheets_prepare(data: dict) -> dict: "- mannerisms: habitual gestures, tics, body language, habits and quirks\n" "- communication_style: how they communicate socially — blunt, formal, warm, guarded, sarcastic, etc.\n" "- voice_pattern: speech style — accent, pacing, vocabulary, register and verbal tics (for voice casting)\n" - "- voice_design_prompt: concise English Qwen voice-design prompt (15-45 words). Include age impression, gender/androgyny if inferable, pitch, timbre, pace, accent/register, emotional baseline and suitability for audiobook dialogue. " + "- voice_design_prompt: concise English voice-design prompt, maximum 480 characters. Use only objective, audible traits; no lore-only labels or repeated synonyms. Include gender as a hard audible constraint, age impression, pitch, timbre, pace, accent/register, emotion and audiobook use. Translate species/role into audible cues (orc: chest-led/gravel-edged; vampire: cool/velvety/controlled; elf: airy/precise/adult; demon: dark/subtly otherworldly; warrior: disciplined command cadence; blacksmith: work-worn/practical; mage: measured/precise). Give one distinctive vocal fingerprint. " + ("State the accent explicitly as an authentic native " + language + " accent — never American-accented English, even though the prompt itself is written in English. " if language and language.lower() != "english" else "State the accent explicitly as a neutral British or international English accent — never American/US-accented. ") + "Do NOT mention plot spoilers.\n" "- image_prompt: detailed English image-generation prompt for this character. Include face, age impression, build, hair/eyes/skin if known, clothing, posture, props, mood, genre/style, and visible symbols. Mark inferred traits with '*'.\n" @@ -861,6 +861,86 @@ async def character_sheets(request: Request): return _charsheets_parse(raw) +@router.post("/api/book-profile/generate") +async def generate_book_profile(request: Request): + """Infer missing book context from the source text and identified cast. + + The result is deliberately a small, grounded metadata object rather than + another character-sheet response. Existing user-entered fields win. + """ + data = await request.json() + book = str(data.get("book") or "").strip() + text = str(data.get("text") or "").strip() + roster = [str(x).strip() for x in (data.get("known_characters") or []) if str(x).strip()] + if not book or not text: + raise HTTPException(400, "book and text are required") + + from core.database import state_get, state_put + key = "book_profile::" + book.lower() + current = state_get(key, {}) or {} + fields = ("genre", "setting", "era", "language") + missing = [f for f in fields if not str(current.get(f) or "").strip()] + if not missing: + return {"ok": True, "profile": {f: str(current.get(f) or "").strip() for f in fields}, "generated": False} + + settings = _load_settings() + llm_url = (data.get("llm_url") or settings.get("llm_url") or "http://localhost:11434/v1").rstrip("/") + model = str(data.get("model") or settings.get("llm_model") or "").strip() + language_hint = str(data.get("language") or "").strip() + roster_text = ", ".join(roster[:120]) or "(no cast roster available)" + prompt = f"""You are preparing production metadata for an audiobook and its character profiles. +Infer ONLY the missing book-context fields from the source excerpt and the recognized cast. +Do not invent a setting merely because a character name sounds familiar. Prefer evidence in the text. +This is the German fantasy series Das Jahr des Greifen, but still use the text as the authority. + +Missing fields: {', '.join(missing)} +Recognized cast: {roster_text} +Language hint: {language_hint or '(detect from the source)'} + +Return ONLY one JSON object with exactly these keys: +{{"genre":"...","setting":"...","era":"...","language":"..."}} +Use concise values (at most a short phrase per field). Write metadata in the source language, +except that language should be a plain language name such as German. Leave a field empty if the +text does not support a reliable answer. Do not describe individual characters and do not include +markdown or explanation. + +SOURCE TEXT: +{text[:120000]}""" + payload = {"messages": [ + {"role": "system", "content": "Return valid JSON only. Ground every field in the supplied book text."}, + {"role": "user", "content": prompt}, + ], "temperature": 0.1, "max_tokens": 300} + if model: + payload["model"] = model + timeout = _request_timeout_seconds(data.get("timeout_seconds"), 300.0) + try: + if not await asyncio.to_thread(_attribution_llm_lock.acquire, True, timeout): + raise HTTPException(503, "Attribution engine busy") + try: + resp = await asyncio.to_thread( + _post_llm_chat_completion, llm_url, payload, + {"Authorization": f"Bearer {settings.get('llm_api_key') or 'sk-dummy-key'}"}, timeout, + ) + finally: + _attribution_llm_lock.release() + resp.raise_for_status() + msg = resp.json()["choices"][0]["message"] + raw = (msg.get("content") or _reasoning_text(msg) or "").strip() + parsed = json.loads(_extract_json_block(raw)) + except HTTPException: + raise + except Exception as e: + raise HTTPException(502, f"Book context generation failed: {e}") + + generated = {f: str(parsed.get(f) or "").strip()[:200 if f != "language" else 60] for f in fields} + merged = {f: str(current.get(f) or "").strip() for f in fields} + for f in missing: + if generated[f]: + merged[f] = generated[f] + state_put(key, merged) + return {"ok": True, "profile": merged, "generated": True} + + @router.post("/api/character-sheets/stream") async def character_sheets_stream(request: Request): """Same job as /api/character-sheets, but streams the LLM's raw output @@ -1187,9 +1267,23 @@ async def character_generate_prompts(request: Request): preamble = ( "You are a prompt engineer who turns a fiction character's profile into ready-to-paste prompts for " "other tools. Use ONLY details present in the profile below; mark anything you must reasonably infer " - f"with a trailing '*'. Never invent plot spoilers not implied by the profile.{lang_note}\n\n" + f"with a trailing '*'. Never invent plot spoilers not implied by the profile.{lang_note}\n" + "CONTENT VS LOOK RULE: The profile is the sole source of truth for who this character is and what they " + "visually possess. The prompt controls how that content is presented: sheet purpose, composition, camera, " + "lighting, rendering medium, palette treatment, background, annotation policy, and output ratio. Never let " + "a chosen art style silently change the character's gender, age, anatomy, species, face, clothing, colors, " + "weapons, or props. Never add lifestyle items, underwear, cosmetics, brands, or extra gear unless the profile " + "explicitly establishes them.\n\n" ) user = f"BOOK: {book or 'unspecified'}\n\nCHARACTER PROFILE:\n{profile or name}\n\n" + visual_identity_rule = ( + "VISUAL IDENTITY LOCK: Treat the profile as a production design bible. " + "Gender is mandatory and must be stated explicitly; never reinterpret a male character as female or vice versa. " + "Extract every visual fact available in the profile: gender, age impression, species, face shape, skin tone, " + "hair, eyes, facial hair, scars or distinguishing marks, height/build, clothing layers, armor, colors, " + "jewelry, weapons, tools, and signature props. Use one consistent face identity anchor across the portrait, " + "turnaround, expression sheet, and concept art. Do not add visual details that contradict the profile.\n\n" + ) # One independent call per field (not one four-field JSON object, nor even # the two-field pairing this used briefly) — the UI now has a Generate @@ -1221,6 +1315,7 @@ async def character_generate_prompts(request: Request): ), "image_prompt": ( preamble + + visual_identity_rule + "Produce exactly this field:\n" "- image_prompt: a detailed English image-generation prompt for a full CHARACTER REFERENCE SHEET " "(a single composite image, like a game/animation production turnaround), not just one portrait. " @@ -1238,7 +1333,9 @@ async def character_generate_prompts(request: Request): "expression sheet with 3-5 headshots showing this character's typical emotional range " "(calm/determined/etc. — pick expressions that fit their personality), (4) a color palette swatch " "panel for hair/eyes/outfit, (5) callouts for their signature props, tools, or clothing details " - "with short labels. Include age impression, build, hair/eyes/skin if known, and clothing " + "with short labels. Include the explicit gender and every established visual trait: age impression, species, " + "face shape, build, hair, eyes, skin, facial hair, distinguishing marks, clothing, armor, colors, " + "jewelry, weapons, tools, and signature props when present. " "appropriate to the established setting. Specify a clean production-design/concept-art layout " "with a plain neutral background, and explicitly note this is an ORIGINAL character, not based on " "any copyrighted character. One dense paragraph, comma-separated descriptors are fine.\n\n" @@ -1247,33 +1344,35 @@ async def character_generate_prompts(request: Request): "silly_tavern_prompt": preamble + _silly_tavern_prompt_instruction(sheet), "concept_art_prompt": ( preamble + + visual_identity_rule + "Produce exactly this field:\n" "- concept_art_prompt: an English image-generation prompt for a production-ready character/NPC " "reference sheet, written as labelled clauses in this EXACT order — Task, Subject, Context, Style, " "Composition, Lighting, Constraints, Output — each a single sentence, all as one dense paragraph " - "(not a list). This mirrors a well-tested prompt-engineering pattern for these sheets; follow it " - "precisely rather than writing free-form:\n" - " Task: name the sheet type, e.g. 'Generate a character/NPC design sheet.'\n" - " Subject: 'an original adult [role/archetype from the profile]' plus its 3-6 most visually " - "distinctive, ALREADY-ESTABLISHED traits (skin/hair, signature garment layers, and — only if the " - "profile actually gives this character a prop, weapon, or tool — its exact count, e.g. 'exactly one " - "quiver' or 'exactly two throwing knives'; omit props entirely if the profile has none).\n" - " Context: one clause on what the sheet is for and the story's genre/setting/era — infer this from " - "the book/profile the same way you would for a portrait (a fantasy Admiral is NOT a real-world 20th-" - "century Admiral) and state it explicitly, since an image model defaults to generic modern imagery " - "otherwise.\n" - " Style: a concept-art style matching that genre/setting (painterly game concept art / detailed " - "semi-realistic concept art / hand-painted concept art — pick what fits), grounded materials, clear " - "shape language.\n" - " Composition: full-body front, side, and back views across the top; below them, ONE clean row of " - "isolated callouts for this character's established props/costume components ONLY (skip this row " - "entirely if the profile establishes no distinct props/costume pieces worth separating out) — state " - "the exact number of callouts and name each one.\n" - " Lighting: neutral studio/museum-style light, no cinematic color cast.\n" - " Constraints: face/silhouette/garment/prop consistency across every view; the exact prop count " - "restated; no duplicate gear, no extra limbs, no readable text/logos/watermark, no real-world/" - "franchise references, this is an ORIGINAL character not based on any copyrighted one.\n" - " Output: one production-ready 3:2 reference sheet.\n\n" + "(not a list). Use the following two reference layouts as the structural model, but replace EVERY " + "subject fact with the current character profile:\n" + " Task: choose the best sheet purpose for the profile — a three-view character turnaround with " + "accessory callouts, or a character props/accessories callout sheet.\n" + " Subject: write an original adult [EXACT GENDER] [role/archetype] using ONLY the profile's established " + "content. Include every relevant visual fact from the profile: age, species, face, skin, hair, eyes, " + "facial hair, marks, build, clothing layers, colors, armor, jewelry, weapons, tools, and signature " + "accessories. For each profile-established prop/accessory, state its exact count; omit anything not in " + "the profile. Never copy the herbalist/cartographer examples.\n" + " Context: state the book's profile-derived genre, setting, era, and the production purpose of the sheet.\n" + " Style: retain the reference style principles — detailed hand-painted or painterly game concept art, " + "grounded leather/metal/cloth/wood materials, clear readable shape language, and coherent wear — but " + "choose the visual medium and palette treatment appropriate to this book's profile.\n" + " Composition: choose either (A) aligned full-body front, side, and back views across the top with a " + "dialogue/action pose and one clean accessory callout row below, or (B) one full-body three-quarter " + "hero figure at left with exactly one isolated callout for each profile-established accessory at right. " + "Use clean leader lines, generous spacing, safe margins, and keep every figure and accessory fully visible.\n" + " Lighting: use warm-neutral or neutral studio light that reveals material texture, with soft contact " + "shadows and no cinematic color cast unless the profile or book context explicitly requires one.\n" + " Constraints: same exact gender, face identity anchor, body proportions, silhouette, garment seams, " + "colors, wear, accessory scale, attachment points, and prop count in every view; each accessory appears " + "once and matches the carried version; no duplicate gear, disassembled fragments, floating objects, extra " + "limbs, readable text, logos, real-world emblems, franchise references, or watermark.\n" + " Output: one organized, production-ready 3:2 character reference sheet.\n\n" 'Respond with STRICT JSON only: {"concept_art_prompt":""}/no-think' ), } @@ -1637,11 +1736,22 @@ def _character_generate_image_sync(provider: str, model: str, prompt: str, keys: if not api_key: raise HTTPException(400, "Google API key not set (Settings > Engines > Image Generation)") gmodel = model or "gemini-2.5-flash-image" + # Google retired the preview alias; keep older saved settings usable + # instead of turning a previously working Google selection into a 404. + if gmodel == "gemini-2.5-flash-image-preview": + gmodel = "gemini-2.5-flash-image" try: resp = requests.post( - f"https://generativelanguage.googleapis.com/v1beta/models/{gmodel}:generateContent", - params={"key": api_key}, - json={"contents": [{"parts": [{"text": prompt}]}]}, + f"https://generativelanguage.googleapis.com/v1/models/{gmodel}:generateContent", + headers={"x-goog-api-key": api_key, "Content-Type": "application/json"}, + # Gemini image models do not return image bytes unless image + # output is explicitly requested. The old payload only sent + # the prompt, so Google successfully returned text (or a + # safety response) and the client reported "no image data". + json={ + "contents": [{"parts": [{"text": prompt}]}], + "generationConfig": {"responseModalities": ["IMAGE"]}, + }, timeout=120, ) if not resp.ok: @@ -1651,10 +1761,15 @@ def _character_generate_image_sync(provider: str, model: str, prompt: str, keys: except Exception: pass raise HTTPException(resp.status_code, f"Google image generation failed: {detail}") - parts = (resp.json().get("candidates") or [{}])[0].get("content", {}).get("parts", []) - inline = next((p.get("inlineData") for p in parts if p.get("inlineData")), None) + payload = resp.json() + parts = (payload.get("candidates") or [{}])[0].get("content", {}).get("parts", []) + inline = next((p.get("inlineData") or p.get("inline_data") for p in parts if p.get("inlineData") or p.get("inline_data")), None) if not inline: - raise HTTPException(502, "Google returned no image data — try again or rephrase the prompt") + # Preserve Google's useful finish/safety text in the error; + # this makes quota, blocked prompts, and unavailable models + # distinguishable from a client parsing bug. + finish = (payload.get("candidates") or [{}])[0].get("finishReason") or "unknown response" + raise HTTPException(502, f"Google returned no image data ({finish}) — check the selected image model, quota, or prompt") mime = inline.get("mimeType", "image/png") return {"image": f"data:{mime};base64,{inline['data']}"} except HTTPException: diff --git a/static/dist/main.min.js b/static/dist/main.min.js index f466c1e..4ce649e 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -1173,13 +1173,13 @@ ${lines.trim()} - `,confirmOverlay.addEventListener("click",ce=>ce.stopPropagation()),confirmOverlay.querySelector("#btn-cancel-del").addEventListener("click",ce=>{ce.stopPropagation(),confirmOverlay.remove()}),confirmOverlay.querySelector("#btn-confirm-del").addEventListener("click",async ce=>{ce.stopPropagation(),confirmOverlay.innerHTML='';try{if(!(await fetch(`${READER_API}/${el.dataset.id}`,{method:"DELETE"})).ok)throw new Error("Failed to delete book");toast("Book deleted","success"),readerRenderLibrary()}catch(err){toast(err.message,"error"),confirmOverlay.remove()}}),el.appendChild(confirmOverlay)}),el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.style.borderColor="var(--accent)")}),el.addEventListener("dragleave",()=>{el.style.borderColor=""}),el.addEventListener("drop",async e=>{var _a2;if(!(e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")))return;e.preventDefault(),e.stopPropagation(),el.style.borderColor="";const f=(_a2=e.dataTransfer.files)==null?void 0:_a2[0];if(!f||!f.type.startsWith("image/")){toast("Please drop an image file","error");return}try{const id=el.dataset.id,r=await fetch(`${READER_API}/${id}/cover`,{method:"PUT",body:f});if(!r.ok)throw new Error(r.statusText);await fetch(`${READER_API}/${id}/progress`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({updated:new Date().toISOString()})}),toast("Cover image updated","success"),readerRenderLibrary()}catch(err){toast("Failed to upload cover: "+(err.message||err),"error")}})}),list.querySelectorAll(".reader-book-del").forEach(btn=>btn.addEventListener("click",e=>{e.stopPropagation(),confirm("Delete this saved document and its audio?")&&readerDeleteLibraryDoc(btn.dataset.del)}))}(_Kb=$("reader-file-input"))==null||_Kb.addEventListener("change",function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];this.value="",readerImportFile(f)}),(_Lb=$("reader-paste-load"))==null||_Lb.addEventListener("click",readerImportPastedText),(_Mb=$("reader-paste-text"))==null||_Mb.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="Enter"&&(e.preventDefault(),readerImportPastedText())}),(_Nb=$("reader-fetch-voices-btn"))==null||_Nb.addEventListener("click",readerFetchVoices),(_Ob=$("reader-backend-select"))==null||_Ob.addEventListener("change",()=>{const sel=$("reader-voice-select");sel&&(sel.innerHTML=''),window.VoicePicker&&(VoicePicker.upgrade("reader-voice-select"),VoicePicker.populate("reader-voice-select",[])),readerUpdateBackendHint()}),(_Pb=$("reader-chunk-mode"))==null||_Pb.addEventListener("change",function(){readerRechunk(this.value)}),(_Qb=$("reader-normalize"))==null||_Qb.addEventListener("change",function(){readerState.normalize=this.checked}),(_Rb=$("reader-play"))==null||_Rb.addEventListener("click",()=>{readerState.playing?readerPause():readerPlay()}),(_Sb=$("reader-stop"))==null||_Sb.addEventListener("click",()=>{readerStopPlayback(),readerState.idx=0,readerSaveResume(),readerUpdateProgress()}),(_Tb=$("reader-prev"))==null||_Tb.addEventListener("click",()=>readerJumpTo(readerState.idx-1)),(_Ub=$("reader-next"))==null||_Ub.addEventListener("click",()=>readerJumpTo(readerState.idx+1)),(_Vb=$("reader-speed"))==null||_Vb.addEventListener("input",function(){readerState.speed=parseFloat(this.value)||1;const lbl=$("reader-speed-label");lbl&&(lbl.textContent=readerState.speed.toFixed(2).replace(/0$/,"")+"\xD7"),readerState.currentSource&&(readerState.currentSource.playbackRate.value=readerState.speed)}),(_Wb=$("reader-zoom-fitw"))==null||_Wb.addEventListener("click",()=>readerApplyZoom("fit-width")),(_Xb=$("reader-zoom-fith"))==null||_Xb.addEventListener("click",()=>readerApplyZoom("fit-height")),(_Yb=$("reader-zoom-two"))==null||_Yb.addEventListener("click",()=>readerApplyZoom("two")),(_Zb=$("reader-zoom-in"))==null||_Zb.addEventListener("click",()=>{readerState.scale=Math.min(4,readerState.scale*1.2),readerApplyZoom("custom")}),(__b=$("reader-zoom-out"))==null||__b.addEventListener("click",()=>{readerState.scale=Math.max(.2,readerState.scale/1.2),readerApplyZoom("custom")});let _readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,_readerSearchTimer=null;const READER_SEARCH_MAX_HITS=2500;function readerSearchTextMap(words){var _a2;let text="";const spans=[];for(let i=0;isp.end>pos&&sp.startsp.wordIndex);idxs.length&&groups.push(idxs),pos=Math.max(pos+1,end)}return groups}function readerRemoveSearchHighlights(){document.querySelectorAll(".reader-search-hit").forEach(el=>el.remove()),document.querySelectorAll(".reader-word.search-hit, .reader-word.search-current").forEach(el=>{el.classList.remove("search-hit","search-current")})}function readerSetSearchStatus(text){const st=$("reader-search-status");st&&(st.textContent=text||"")}function readerClearSearch({clearInput=!1}={}){if(_readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,readerRemoveSearchHighlights(),readerSetSearchStatus(""),clearInput){const inp=$("reader-search");inp&&(inp.value="")}}function readerCollectSearchHits(term){var _a2,_b2,_c2;const hits=[],sents=readerState.sentences||[];for(let si=0;si=READER_SEARCH_MAX_HITS)return hits}}return hits}function readerPaintSearchHighlights(){if(readerRemoveSearchHighlights(),!_readerSearchTerm||!_readerSearchHits.length)return;const scale=readerState.scale||1;_readerSearchHits.forEach((hit,hi)=>{var _a2;const s=readerState.sentences[hit.sentenceIndex];if(s){hit.nodes=[];for(const wi of hit.wordIndexes){const w=(_a2=s.words)==null?void 0:_a2[wi];if(w)if(readerState.mode==="pdf"){const pg=readerState.pages[w.page];if(!(pg!=null&&pg.overlay))continue;const el=document.createElement("div");el.className="reader-search-hit"+(hi===_readerSearchCurrent?" search-current":""),Object.assign(el.style,{left:w.x*scale+"px",top:w.top*scale+"px",width:Math.max(w.w,2)*scale+"px",height:Math.max(w.h,2)*scale+"px"}),pg.overlay.appendChild(el),hit.nodes.push(el)}else w.el&&(w.el.classList.add("search-hit"),hi===_readerSearchCurrent&&w.el.classList.add("search-current"),hit.nodes.push(w.el))}}})}function readerUpdateSearchStatus(){var _a2,_b2;if(!_readerSearchTerm){readerSetSearchStatus("");return}if(!((_a2=readerState.sentences)!=null&&_a2.length)){readerSetSearchStatus("No text loaded");return}if(!_readerSearchHits.length){readerSetSearchStatus("0/0");return}const current=_readerSearchCurrent>=0?_readerSearchCurrent+1:1,hit=_readerSearchHits[Math.max(0,_readerSearchCurrent)],capped=_readerSearchHits.length>=READER_SEARCH_MAX_HITS?"+":"",page=readerState.mode==="pdf"?" \xB7 p."+(((_b2=hit==null?void 0:hit.page)!=null?_b2:0)+1):"";readerSetSearchStatus(current+"/"+_readerSearchHits.length+capped+page)}function readerJumpToSearchHit(idx){var _a2;if(!_readerSearchHits.length)return;_readerSearchCurrent=(idx+_readerSearchHits.length)%_readerSearchHits.length,readerPaintSearchHighlights(),readerUpdateSearchStatus();const hit=_readerSearchHits[_readerSearchCurrent],node=(_a2=hit==null?void 0:hit.nodes)==null?void 0:_a2[0];if(node)readerEnsureVisible(node);else if(readerState.mode==="pdf"){const pg=readerState.pages[hit.page];pg!=null&&pg.pageDiv&&(readerRenderPage(hit.page),pg.pageDiv.scrollIntoView({behavior:"smooth",block:"center"}))}}function readerSearchApply(term,{jump=!0}={}){if(term=String(term||"").trim(),!term){readerClearSearch();return}_readerSearchTerm=term,_readerSearchHits=readerCollectSearchHits(term),_readerSearchCurrent=_readerSearchHits.length?0:-1,readerPaintSearchHighlights(),readerUpdateSearchStatus(),jump&&_readerSearchHits.length&&readerJumpToSearchHit(0)}function readerSearchStep(direction){var _a2;const term=((_a2=$("reader-search"))==null?void 0:_a2.value.trim())||_readerSearchTerm;if(!_readerSearchHits.length||term!==_readerSearchTerm){readerSearchApply(term,{jump:!0});return}readerJumpToSearchHit(_readerSearchCurrent+(direction===-1?-1:1))}(_$b=$("reader-search"))==null||_$b.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),readerSearchStep(e.shiftKey?-1:1)),e.key==="Escape"&&readerClearSearch({clearInput:!0})}),(_ac=$("reader-search"))==null||_ac.addEventListener("input",e=>{clearTimeout(_readerSearchTimer);const term=e.target.value.trim();if(!term){readerClearSearch();return}_readerSearchTimer=setTimeout(()=>readerSearchApply(term,{jump:!0}),160)}),(_bc=$("reader-search-prev"))==null||_bc.addEventListener("click",()=>readerSearchStep(-1)),(_cc=$("reader-search-next"))==null||_cc.addEventListener("click",()=>readerSearchStep(1)),(_dc=$("reader-search-clear"))==null||_dc.addEventListener("click",()=>readerClearSearch({clearInput:!0})),(_ec=$("reader-save-lib"))==null||_ec.addEventListener("click",readerSaveLibrary),(_fc=$("reader-export-btn"))==null||_fc.addEventListener("click",()=>{var _a2;return readerExport(((_a2=$("reader-export-mode"))==null?void 0:_a2.value)||"page")}),(_gc=$("reader-synth-all"))==null||_gc.addEventListener("click",readerSynthAll),(_hc=$("reader-synth-cancel"))==null||_hc.addEventListener("click",()=>{readerState.synthCancel=!0}),(_ic=$("reader-select-toggle"))==null||_ic.addEventListener("click",()=>{readerState.selecting?readerSetSelecting(!1):(readerClearSelection(),readerSetSelecting(!0))}),(_jc=$("reader-sel-clear"))==null||_jc.addEventListener("click",readerClearSelection),document.addEventListener("keydown",e=>{e.key==="Escape"&&readerState.selecting&&readerSetSelecting(!1)}),(_kc=$("reader-page-from"))==null||_kc.addEventListener("change",readerUpdateScopeLabel),(_lc=$("reader-page-to"))==null||_lc.addEventListener("change",readerUpdateScopeLabel),(_mc=$("reader-doc"))==null||_mc.addEventListener("click",e=>{if(!readerState.selecting||readerState.mode!=="pdf")return;const stat=e.target.closest(".reader-stat");if(!stat)return;const si=parseInt(stat.dataset.si);isNaN(si)||readerPickSentence(si)}),(_nc=$("reader-doc"))==null||_nc.addEventListener("scroll",function(){let t=null;return()=>{t||(t=setTimeout(()=>{t=null,readerRenderVisible()},120))}}()),window.addEventListener("resize",function(){let t=null;return()=>{readerState.mode!=="pdf"||readerState.zoomMode==="custom"||(clearTimeout(t),t=setTimeout(()=>readerApplyZoom(readerState.zoomMode),200))}}()),function(){["reader-dropzone","reader-doc"].map(id=>$(id)).filter(Boolean).forEach(el=>{el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.add("dragover"))}),el.addEventListener("dragleave",()=>el.classList.remove("dragover")),el.addEventListener("drop",e=>{var _a2;e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.remove("dragover"),readerImportFile((_a2=e.dataTransfer.files)==null?void 0:_a2[0]))})})}(),window.showReaderView=function(view){const main=$("reader-main-view"),cast=$("reader-audiobook-panel"),chars=$("reader-charsheets-panel"),lib=$("reader-library-card");main&&(main.hidden=view!=="main"),cast&&(cast.hidden=view!=="cast",view!=="cast"&&(cast.style.display="",cast.style.flexDirection="",cast.style.minHeight=""),view==="cast"&&!cast.innerHTML.trim()&&(cast.innerHTML=`

No active casting session

Open a document in the Reader tab and click "Cast as audiobook" to start extracting characters.


`,cast.className="ab-castpanel-inline card")),chars&&(chars.hidden=view!=="chars",view==="chars"&&!chars.innerHTML.trim()&&(chars.innerHTML=`
Character sheets

This stage will show the live passage-by-passage extraction view, prompt editor, and generated sheets once you start a cast.

`,chars.className="")),lib&&(lib.hidden=view!=="library",view==="library"&&(lib.classList.remove("card"),lib.style.boxShadow="none",lib.style.border="none",lib.style.background="transparent",readerRenderLibrary())),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs(view==="cast"?"cast":view==="chars"?"chars":"source")},window.navReaderView=function(view){window.showReaderView(view)},window.readerJumpToPage=function(pageNum){const pg=parseInt(pageNum,10);if(!pg)return Promise.resolve(!1);typeof navTo=="function"&&navTo("s-reader"),window.showReaderView("main");let tries=0;const waitFrame=()=>new Promise(resolve=>requestAnimationFrame(resolve)),jump=async()=>{var _a2;await waitFrame();const pageState=(_a2=readerState.pages)==null?void 0:_a2[pg-1],pageDiv=pageState==null?void 0:pageState.pageDiv;return pageDiv&&!pageDiv.hidden?(await readerRenderPage(pg-1),pageDiv.scrollIntoView({behavior:"smooth",block:"start",inline:"nearest"}),readerRenderVisible(),!0):tries++<20?(await new Promise(resolve=>setTimeout(resolve,100)),jump()):(typeof toast=="function"&&toast("Source page is not loaded in Reader yet","info"),!1)};return jump()},window.readerOnShow=async function(){const sel=$("reader-backend-select");if(sel&&(!sel.value||sel.options.length<=1)){if(typeof availableTtsBackends=="function"&&!availableTtsBackends().length&&typeof refreshTtsBackendAvailability=="function")try{await refreshTtsBackendAvailability()}catch{}typeof availableTtsBackends=="function"&&availableTtsBackends().length&&(sel.innerHTML=ttsBackendOptions(sel.value),sel.disabled=!1)}readerUpdateBackendHint(),window.VoicePicker&&VoicePicker.upgrade("reader-voice-select"),readerRenderLibrary(),window._readerStartView&&(window.showReaderView(window._readerStartView),window._readerStartView=null),_readerEnsureImportVisible()};function _readerEnsureImportVisible(){if(readerState.sentences.length)return;const card=document.getElementById("reader-config-card"),body=card?card.querySelector(".card-col-body"):null;body&&body.hidden&&(body.hidden=!1,card.classList.remove("card-col-closed"))}const AUDIOBOOK_CHUNK_CHARS=3e3,AUDIOBOOK_WARMUP_TIMEOUT_MS=24e4,AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS=6e5,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS=3e5,AUDIOBOOK_RECAST_TIMEOUT_MS=24e4,AUDIOBOOK_RECAST_CONTEXT_CHARS=4200,AUDIOBOOK_RECAST_TARGETS_PER_CALL=6,AUDIOBOOK_DRAFT_AUTOSAVE_MS=5*60*1e3,_audiobook={running:!1,cancel:!1};window._audiobook=_audiobook;let _abDraftAutosaveTimer=null,_abLastServerDraftErrorAt=0;async function audiobookFetchWithTimeout(url,options={},timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const parentSignal=options.signal,ac=new AbortController;let timedOut=!1;const timer=setTimeout(()=>{timedOut=!0,ac.abort()},timeoutMs),onParentAbort=()=>ac.abort(parentSignal==null?void 0:parentSignal.reason);parentSignal&&(parentSignal.aborted?onParentAbort():parentSignal.addEventListener("abort",onParentAbort,{once:!0}));try{return await fetch(url,{...options,signal:ac.signal})}catch(err){if(timedOut){const timeoutErr=new Error(`Timed out after ${Math.ceil(timeoutMs/1e3)}s`);throw timeoutErr.name="TimeoutError",timeoutErr}throw err}finally{clearTimeout(timer),parentSignal&&parentSignal.removeEventListener("abort",onParentAbort)}}function audiobookTimeoutSeconds(timeoutMs){return Math.max(5,Math.round(timeoutMs/1e3))}async function audiobookAttributeStream(body,view,outerSignal,idleTimeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/attribute-dialogue/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify({...body,want_reasoning:!0})});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(` + `,confirmOverlay.addEventListener("click",ce=>ce.stopPropagation()),confirmOverlay.querySelector("#btn-cancel-del").addEventListener("click",ce=>{ce.stopPropagation(),confirmOverlay.remove()}),confirmOverlay.querySelector("#btn-confirm-del").addEventListener("click",async ce=>{ce.stopPropagation(),confirmOverlay.innerHTML='';try{if(!(await fetch(`${READER_API}/${el.dataset.id}`,{method:"DELETE"})).ok)throw new Error("Failed to delete book");toast("Book deleted","success"),readerRenderLibrary()}catch(err){toast(err.message,"error"),confirmOverlay.remove()}}),el.appendChild(confirmOverlay)}),el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.style.borderColor="var(--accent)")}),el.addEventListener("dragleave",()=>{el.style.borderColor=""}),el.addEventListener("drop",async e=>{var _a2;if(!(e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")))return;e.preventDefault(),e.stopPropagation(),el.style.borderColor="";const f=(_a2=e.dataTransfer.files)==null?void 0:_a2[0];if(!f||!f.type.startsWith("image/")){toast("Please drop an image file","error");return}try{const id=el.dataset.id,r=await fetch(`${READER_API}/${id}/cover`,{method:"PUT",body:f});if(!r.ok)throw new Error(r.statusText);await fetch(`${READER_API}/${id}/progress`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({updated:new Date().toISOString()})}),toast("Cover image updated","success"),readerRenderLibrary()}catch(err){toast("Failed to upload cover: "+(err.message||err),"error")}})}),list.querySelectorAll(".reader-book-del").forEach(btn=>btn.addEventListener("click",e=>{e.stopPropagation(),confirm("Delete this saved document and its audio?")&&readerDeleteLibraryDoc(btn.dataset.del)}))}(_Kb=$("reader-file-input"))==null||_Kb.addEventListener("change",function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];this.value="",readerImportFile(f)}),(_Lb=$("reader-paste-load"))==null||_Lb.addEventListener("click",readerImportPastedText),(_Mb=$("reader-paste-text"))==null||_Mb.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="Enter"&&(e.preventDefault(),readerImportPastedText())}),(_Nb=$("reader-fetch-voices-btn"))==null||_Nb.addEventListener("click",readerFetchVoices),(_Ob=$("reader-backend-select"))==null||_Ob.addEventListener("change",()=>{const sel=$("reader-voice-select");sel&&(sel.innerHTML=''),window.VoicePicker&&(VoicePicker.upgrade("reader-voice-select"),VoicePicker.populate("reader-voice-select",[])),readerUpdateBackendHint()}),(_Pb=$("reader-chunk-mode"))==null||_Pb.addEventListener("change",function(){readerRechunk(this.value)}),(_Qb=$("reader-normalize"))==null||_Qb.addEventListener("change",function(){readerState.normalize=this.checked}),(_Rb=$("reader-play"))==null||_Rb.addEventListener("click",()=>{readerState.playing?readerPause():readerPlay()}),(_Sb=$("reader-stop"))==null||_Sb.addEventListener("click",()=>{readerStopPlayback(),readerState.idx=0,readerSaveResume(),readerUpdateProgress()}),(_Tb=$("reader-prev"))==null||_Tb.addEventListener("click",()=>readerJumpTo(readerState.idx-1)),(_Ub=$("reader-next"))==null||_Ub.addEventListener("click",()=>readerJumpTo(readerState.idx+1)),(_Vb=$("reader-speed"))==null||_Vb.addEventListener("input",function(){readerState.speed=parseFloat(this.value)||1;const lbl=$("reader-speed-label");lbl&&(lbl.textContent=readerState.speed.toFixed(2).replace(/0$/,"")+"\xD7"),readerState.currentSource&&(readerState.currentSource.playbackRate.value=readerState.speed)}),(_Wb=$("reader-zoom-fitw"))==null||_Wb.addEventListener("click",()=>readerApplyZoom("fit-width")),(_Xb=$("reader-zoom-fith"))==null||_Xb.addEventListener("click",()=>readerApplyZoom("fit-height")),(_Yb=$("reader-zoom-two"))==null||_Yb.addEventListener("click",()=>readerApplyZoom("two")),(_Zb=$("reader-zoom-in"))==null||_Zb.addEventListener("click",()=>{readerState.scale=Math.min(4,readerState.scale*1.2),readerApplyZoom("custom")}),(__b=$("reader-zoom-out"))==null||__b.addEventListener("click",()=>{readerState.scale=Math.max(.2,readerState.scale/1.2),readerApplyZoom("custom")});let _readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,_readerSearchTimer=null;const READER_SEARCH_MAX_HITS=2500;function readerSearchTextMap(words){var _a2;let text="";const spans=[];for(let i=0;isp.end>pos&&sp.startsp.wordIndex);idxs.length&&groups.push(idxs),pos=Math.max(pos+1,end)}return groups}function readerRemoveSearchHighlights(){document.querySelectorAll(".reader-search-hit").forEach(el=>el.remove()),document.querySelectorAll(".reader-word.search-hit, .reader-word.search-current").forEach(el=>{el.classList.remove("search-hit","search-current")})}function readerSetSearchStatus(text){const st=$("reader-search-status");st&&(st.textContent=text||"")}function readerClearSearch({clearInput=!1}={}){if(_readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,readerRemoveSearchHighlights(),readerSetSearchStatus(""),clearInput){const inp=$("reader-search");inp&&(inp.value="")}}function readerCollectSearchHits(term){var _a2,_b2,_c2;const hits=[],sents=readerState.sentences||[];for(let si=0;si=READER_SEARCH_MAX_HITS)return hits}}return hits}function readerPaintSearchHighlights(){if(readerRemoveSearchHighlights(),!_readerSearchTerm||!_readerSearchHits.length)return;const scale=readerState.scale||1;_readerSearchHits.forEach((hit,hi)=>{var _a2;const s=readerState.sentences[hit.sentenceIndex];if(s){hit.nodes=[];for(const wi of hit.wordIndexes){const w=(_a2=s.words)==null?void 0:_a2[wi];if(w)if(readerState.mode==="pdf"){const pg=readerState.pages[w.page];if(!(pg!=null&&pg.overlay))continue;const el=document.createElement("div");el.className="reader-search-hit"+(hi===_readerSearchCurrent?" search-current":""),Object.assign(el.style,{left:w.x*scale+"px",top:w.top*scale+"px",width:Math.max(w.w,2)*scale+"px",height:Math.max(w.h,2)*scale+"px"}),pg.overlay.appendChild(el),hit.nodes.push(el)}else w.el&&(w.el.classList.add("search-hit"),hi===_readerSearchCurrent&&w.el.classList.add("search-current"),hit.nodes.push(w.el))}}})}function readerUpdateSearchStatus(){var _a2,_b2;if(!_readerSearchTerm){readerSetSearchStatus("");return}if(!((_a2=readerState.sentences)!=null&&_a2.length)){readerSetSearchStatus("No text loaded");return}if(!_readerSearchHits.length){readerSetSearchStatus("0/0");return}const current=_readerSearchCurrent>=0?_readerSearchCurrent+1:1,hit=_readerSearchHits[Math.max(0,_readerSearchCurrent)],capped=_readerSearchHits.length>=READER_SEARCH_MAX_HITS?"+":"",page=readerState.mode==="pdf"?" \xB7 p."+(((_b2=hit==null?void 0:hit.page)!=null?_b2:0)+1):"";readerSetSearchStatus(current+"/"+_readerSearchHits.length+capped+page)}function readerJumpToSearchHit(idx){var _a2;if(!_readerSearchHits.length)return;_readerSearchCurrent=(idx+_readerSearchHits.length)%_readerSearchHits.length,readerPaintSearchHighlights(),readerUpdateSearchStatus();const hit=_readerSearchHits[_readerSearchCurrent],node=(_a2=hit==null?void 0:hit.nodes)==null?void 0:_a2[0];if(node)readerEnsureVisible(node);else if(readerState.mode==="pdf"){const pg=readerState.pages[hit.page];pg!=null&&pg.pageDiv&&(readerRenderPage(hit.page),pg.pageDiv.scrollIntoView({behavior:"smooth",block:"center"}))}}function readerSearchApply(term,{jump=!0}={}){if(term=String(term||"").trim(),!term){readerClearSearch();return}_readerSearchTerm=term,_readerSearchHits=readerCollectSearchHits(term),_readerSearchCurrent=_readerSearchHits.length?0:-1,readerPaintSearchHighlights(),readerUpdateSearchStatus(),jump&&_readerSearchHits.length&&readerJumpToSearchHit(0)}function readerSearchStep(direction){var _a2;const term=((_a2=$("reader-search"))==null?void 0:_a2.value.trim())||_readerSearchTerm;if(!_readerSearchHits.length||term!==_readerSearchTerm){readerSearchApply(term,{jump:!0});return}readerJumpToSearchHit(_readerSearchCurrent+(direction===-1?-1:1))}(_$b=$("reader-search"))==null||_$b.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),readerSearchStep(e.shiftKey?-1:1)),e.key==="Escape"&&readerClearSearch({clearInput:!0})}),(_ac=$("reader-search"))==null||_ac.addEventListener("input",e=>{clearTimeout(_readerSearchTimer);const term=e.target.value.trim();if(!term){readerClearSearch();return}_readerSearchTimer=setTimeout(()=>readerSearchApply(term,{jump:!0}),160)}),(_bc=$("reader-search-prev"))==null||_bc.addEventListener("click",()=>readerSearchStep(-1)),(_cc=$("reader-search-next"))==null||_cc.addEventListener("click",()=>readerSearchStep(1)),(_dc=$("reader-search-clear"))==null||_dc.addEventListener("click",()=>readerClearSearch({clearInput:!0})),(_ec=$("reader-save-lib"))==null||_ec.addEventListener("click",readerSaveLibrary),(_fc=$("reader-export-btn"))==null||_fc.addEventListener("click",()=>{var _a2;return readerExport(((_a2=$("reader-export-mode"))==null?void 0:_a2.value)||"page")}),(_gc=$("reader-synth-all"))==null||_gc.addEventListener("click",readerSynthAll),(_hc=$("reader-synth-cancel"))==null||_hc.addEventListener("click",()=>{readerState.synthCancel=!0}),(_ic=$("reader-select-toggle"))==null||_ic.addEventListener("click",()=>{readerState.selecting?readerSetSelecting(!1):(readerClearSelection(),readerSetSelecting(!0))}),(_jc=$("reader-sel-clear"))==null||_jc.addEventListener("click",readerClearSelection),document.addEventListener("keydown",e=>{e.key==="Escape"&&readerState.selecting&&readerSetSelecting(!1)}),(_kc=$("reader-page-from"))==null||_kc.addEventListener("change",readerUpdateScopeLabel),(_lc=$("reader-page-to"))==null||_lc.addEventListener("change",readerUpdateScopeLabel),(_mc=$("reader-doc"))==null||_mc.addEventListener("click",e=>{if(!readerState.selecting||readerState.mode!=="pdf")return;const stat=e.target.closest(".reader-stat");if(!stat)return;const si=parseInt(stat.dataset.si);isNaN(si)||readerPickSentence(si)}),(_nc=$("reader-doc"))==null||_nc.addEventListener("scroll",function(){let t=null;return()=>{t||(t=setTimeout(()=>{t=null,readerRenderVisible()},120))}}()),window.addEventListener("resize",function(){let t=null;return()=>{readerState.mode!=="pdf"||readerState.zoomMode==="custom"||(clearTimeout(t),t=setTimeout(()=>readerApplyZoom(readerState.zoomMode),200))}}()),function(){["reader-dropzone","reader-doc"].map(id=>$(id)).filter(Boolean).forEach(el=>{el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.add("dragover"))}),el.addEventListener("dragleave",()=>el.classList.remove("dragover")),el.addEventListener("drop",e=>{var _a2;e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.remove("dragover"),readerImportFile((_a2=e.dataTransfer.files)==null?void 0:_a2[0]))})})}(),window.showReaderView=function(view){const main=$("reader-main-view"),cast=$("reader-audiobook-panel"),chars=$("reader-charsheets-panel"),lib=$("reader-library-card");main&&(main.hidden=view!=="main"),cast&&(cast.hidden=view!=="cast",view!=="cast"&&(cast.style.display="",cast.style.flexDirection="",cast.style.minHeight=""),view==="cast"&&!cast.innerHTML.trim()&&(cast.innerHTML=`

No active casting session

Open a document in the Reader tab and click "Cast as audiobook" to start extracting characters.


`,cast.className="ab-castpanel-inline card")),chars&&(chars.hidden=view!=="chars",view==="chars"&&!chars.innerHTML.trim()&&(chars.innerHTML=`
Character sheets

This stage will show the live passage-by-passage extraction view, prompt editor, and generated sheets once you start a cast.

`,chars.className="")),lib&&(lib.hidden=view!=="library",view==="library"&&(lib.classList.remove("card"),lib.style.boxShadow="none",lib.style.border="none",lib.style.background="transparent",readerRenderLibrary())),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs(view==="cast"?"cast":view==="chars"?"chars":"source")},window.navReaderView=function(view){window.showReaderView(view)},window.readerJumpToPage=function(pageNum){const pg=parseInt(pageNum,10);if(!pg)return Promise.resolve(!1);typeof navTo=="function"&&navTo("s-reader"),window.showReaderView("main");let tries=0;const waitFrame=()=>new Promise(resolve=>requestAnimationFrame(resolve)),jump=async()=>{var _a2;await waitFrame();const pageState=(_a2=readerState.pages)==null?void 0:_a2[pg-1],pageDiv=pageState==null?void 0:pageState.pageDiv;return pageDiv&&!pageDiv.hidden?(await readerRenderPage(pg-1),pageDiv.scrollIntoView({behavior:"smooth",block:"start",inline:"nearest"}),readerRenderVisible(),!0):tries++<20?(await new Promise(resolve=>setTimeout(resolve,100)),jump()):(typeof toast=="function"&&toast("Source page is not loaded in Reader yet","info"),!1)};return jump()},window.readerOnShow=async function(){const sel=$("reader-backend-select");if(sel&&(!sel.value||sel.options.length<=1)){if(typeof availableTtsBackends=="function"&&!availableTtsBackends().length&&typeof refreshTtsBackendAvailability=="function")try{await refreshTtsBackendAvailability()}catch{}typeof availableTtsBackends=="function"&&availableTtsBackends().length&&(sel.innerHTML=ttsBackendOptions(sel.value),sel.disabled=!1)}readerUpdateBackendHint(),window.VoicePicker&&VoicePicker.upgrade("reader-voice-select"),readerRenderLibrary(),window._readerStartView&&(window.showReaderView(window._readerStartView),window._readerStartView=null),_readerEnsureImportVisible()};function _readerEnsureImportVisible(){if(readerState.sentences.length)return;const card=document.getElementById("reader-config-card"),body=card?card.querySelector(".card-col-body"):null;body&&body.hidden&&(body.hidden=!1,card.classList.remove("card-col-closed"))}const AUDIOBOOK_CHUNK_CHARS=3e3,AUDIOBOOK_WARMUP_TIMEOUT_MS=24e4,AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS=6e5,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS=3e5,AUDIOBOOK_RECAST_TIMEOUT_MS=24e4,AUDIOBOOK_RECAST_CONTEXT_CHARS=4200,AUDIOBOOK_RECAST_TARGETS_PER_CALL=6,AUDIOBOOK_DRAFT_AUTOSAVE_MS=5*60*1e3,_audiobook={running:!1,cancel:!1};function _abHideCastingControl(){document.querySelectorAll("#ab-cv-cancel").forEach(btn=>{btn.hidden=!0,btn.disabled=!1,btn.innerHTML=' Cancel'})}window._abDelegatedCancelHandler||(window._abDelegatedCancelHandler=!0,document.addEventListener("click",event=>{var _a2,_b2;const btn=(_b2=(_a2=event.target).closest)==null?void 0:_b2.call(_a2,"#ab-cv-cancel");!btn||btn.hidden||(_audiobook.running?(_audiobook.cancel=!0,btn.disabled=!0,btn.innerHTML=' Stopping...',typeof _audiobook.abort=="function"&&_audiobook.abort()):_abHideCastingControl())})),window._audiobook=_audiobook;let _abDraftAutosaveTimer=null,_abLastServerDraftErrorAt=0;async function audiobookFetchWithTimeout(url,options={},timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const parentSignal=options.signal,ac=new AbortController;let timedOut=!1;const timer=setTimeout(()=>{timedOut=!0,ac.abort()},timeoutMs),onParentAbort=()=>ac.abort(parentSignal==null?void 0:parentSignal.reason);parentSignal&&(parentSignal.aborted?onParentAbort():parentSignal.addEventListener("abort",onParentAbort,{once:!0}));try{return await fetch(url,{...options,signal:ac.signal})}catch(err){if(timedOut){const timeoutErr=new Error(`Timed out after ${Math.ceil(timeoutMs/1e3)}s`);throw timeoutErr.name="TimeoutError",timeoutErr}throw err}finally{clearTimeout(timer),parentSignal&&parentSignal.removeEventListener("abort",onParentAbort)}}function audiobookTimeoutSeconds(timeoutMs){return Math.max(5,Math.round(timeoutMs/1e3))}async function audiobookAttributeStream(body,view,outerSignal,idleTimeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/attribute-dialogue/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify({...body,want_reasoning:!0})});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(` `))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&(view!=null&&view.thinking)&&view.thinking(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}catch(err){throw(err==null?void 0:err.name)==="AbortError"&&!(outerSignal&&outerSignal.aborted)?new Error("stream idle timeout"):err}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}function audiobookDialogueKey(text){return String(text||"").normalize("NFKC").replace(/[»«„“”"‘’'`]+/g,"").replace(/\s+/g," ").trim().toLowerCase()}function audiobookSegmentText(seg){return seg?seg.type==="narration"||!seg.speaker||/^Unknown|Unbekannt/i.test(seg.speaker)?seg.text||"":`"${seg.text||""}"`:""}function _abWordRangeAtPoint(x,y){let node=null,offset=0;if(document.caretRangeFromPoint){const r=document.caretRangeFromPoint(x,y);if(!r)return null;node=r.startContainer,offset=r.startOffset}else if(document.caretPositionFromPoint){const p=document.caretPositionFromPoint(x,y);if(!p)return null;node=p.offsetNode,offset=p.offset}else return null;if(!node||node.nodeType!==3)return null;const text=node.nodeValue||"",isW=ch=>ch!=null&&/[\p{L}\p{N}'’-]/u.test(ch);if(offset>=text.length&&(offset=text.length-1),!isW(text[offset]))if(offset>0&&isW(text[offset-1]))offset--;else return null;let a=offset,b=offset;for(;a>0&&isW(text[a-1]);)a--;for(;b+1{var _a3;return String(((_a3=segs[i])==null?void 0:_a3.text)||"").replace(/\s+/g," ").trim()},out=new Set;for(let i=0;i=minScore&&out.add(i)}return out}window._abSuspectNarration=_abSuspectNarration;function audiobookRecastGroups(unknownIdxs,segs){const groups=[];let cur=[];for(const idx of unknownIdxs){const prev=cur.length?cur[cur.length-1]:-1/0,shortGap=idx<=prev+2&&segs.slice(prev+1,idx).every(s=>!s||s.type==="narration"||/^Unknown|Unbekannt/i.test(s.speaker||""));!cur.length||shortGap&&cur.lengthsegs.slice(start,end).map(audiobookSegmentText).join(" ").trim();let text=render();for(;text.length>AUDIOBOOK_RECAST_CONTEXT_CHARS&&(starttargetEnd)&&(end>targetEnd&&end--,text=render(),!(text.length<=AUDIOBOOK_RECAST_CONTEXT_CHARS));)start=end?"after":"near"}: ${s.speaker}: ${(s.text||"").slice(0,100)}`)}return lines.slice(-12).join(` `)}function audiobookFindReturnedSegment(targetSeg,returned,used){const targetKey=audiobookDialogueKey(targetSeg==null?void 0:targetSeg.text);if(!targetKey)return null;let loose=null;for(let i=0;i=18&&(candKey.includes(targetKey)||targetKey.includes(candKey))&&(loose=loose||{idx:i,seg:cand})}return loose}function audiobookFindReturnedSegmentSequence(targetSeg,returned,used){const targetKey=audiobookDialogueKey(targetSeg==null?void 0:targetSeg.text);if(!targetKey)return null;for(let i=0;ireturned[k])};if(key.length>targetKey.length*1.35+40)break}}return null}function _abStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):String(v)}const _AB_DRAFT_KEY="ttsvc_ab_draft";function _abBookId(){return window.readerState&&readerState.savedId||_audiobook.bookId||null}function _abDraftKey(bookId){return bookId?_AB_DRAFT_KEY+"_"+bookId:_AB_DRAFT_KEY}function _abTextId(text){const s=(text.slice(0,300)+text.slice(-300)).replace(/\s+/g,"");let h=5381;for(let i=0;i>>0;return h.toString(36)+"_"+text.length}function _abSafeFilename(name,fallback="cast"){return(String(name||fallback||"cast").replace(/[\\/:*?"<>|]+/g,"_").replace(/\s+/g,"_").replace(/^_+|_+$/g,"")||fallback||"cast").slice(0,120)}function _abCastMarkdown(data){const payload=data||{},segments=Array.isArray(payload.segments)?payload.segments:[],speakers=[...new Set(segments.filter(s=>(s==null?void 0:s.type)==="dialogue"&&s.speaker).map(s=>String(s.speaker)))].sort(),lines=[`# ${payload.title||"Cast Script"}`,""],metaBits=[];payload.savedAt&&metaBits.push("exported "+new Date(payload.savedAt).toISOString().slice(0,16).replace("T"," ")),metaBits.push(`${speakers.length} character${speakers.length!==1?"s":""}`),metaBits.push(`${segments.length} segment${segments.length!==1?"s":""}`),lines.push(`*${metaBits.join(" \xB7 ")}*`,""),speakers.length&&lines.push(`**Characters:** ${speakers.join(", ")}`,"");let lastPage=null;for(const seg of segments){const text=String((seg==null?void 0:seg.text)||"").trim();if(text){if(seg.page!=null&&seg.page!==lastPage&&(lines.push("---","",`## Page ${seg.page}`,""),lastPage=seg.page),seg.type==="dialogue"){const speaker=String(seg.speaker||"Narrator"),tag=seg.emotion?` *(${seg.emotion})*`:"";lines.push(`**${speaker.toUpperCase()}**${tag}: "${text}"`)}else lines.push(text);lines.push("")}}return lines.join(` `)}function _abStampSegmentPages(segs,pageMarks,text){if(!Array.isArray(segs)||!segs.length||segs.some(s=>s&&s.page!=null))return;const marks=(pageMarks||[]).slice().sort((a,b)=>(a.offset||0)-(b.offset||0)),src=text||"";if(!marks.length||!src)return;let markIdx=0,searchPos=0,cur=null;marks[0].offset<=2&&(cur=marks[0].page+1,markIdx=1);for(const s of segs){const probe=String(s.text||"").trim().slice(0,24),at=probe?src.indexOf(probe,searchPos):-1,pos=at>=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx=marks[markIdx].offset;)cur=marks[markIdx].page+1,markIdx++;cur!=null&&(s.page=cur)}}let _abLastDraftSaveAt=0;const AB_DRAFT_SAVE_THROTTLE_MS=4e3;function _abSaveDraft(segs,roster,text,done,total){var _a2;const isFinal=total>0&&done>=total,now=Date.now();if(!isFinal&&now-_abLastDraftSaveAt{if(!r.ok)throw new Error(r.statusText||`HTTP ${r.status}`)}).catch(err=>{console.warn("[audiobook] server draft autosave failed:",err);const now2=Date.now();now2-_abLastServerDraftErrorAt>6e4&&(_abLastServerDraftErrorAt=now2,typeof toast=="function"&&toast("Server autosave failed. Browser draft is still saved.","error"))})}let _abExportBusy=!1;async function audiobookExportCastMd(){var _a2;if(_abExportBusy)return;_abExportBusy=!0,setTimeout(()=>{_abExportBusy=!1},2e3);const segs=_audiobook.segments||[];if(!segs.length){toast("No cast to export","error");return}const bookId=_abBookId();_audiobook.lastText&&_abSaveDraft(segs,_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0);const title=((_a2=window.readerState)==null?void 0:_a2.title)||_audiobook.title||"audiobook";if(bookId)try{const r=await fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast/export`);if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const blob2=await r.blob(),match=(r.headers.get("Content-Disposition")||"").match(/filename="([^"]+)"/i),name=(match==null?void 0:match[1])||`${_abSafeFilename(title)}_cast.zip`;if(typeof readerDownload=="function")readerDownload(blob2,name);else{const a=document.createElement("a");a.href=URL.createObjectURL(blob2),a.download=name,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),500)}toast("Exported cast + character sheets (.zip)","success");return}catch{toast("Server export failed, downloading local cast copy","error")}const payload={bookId,title,textId:_audiobook.lastText?_abTextId(_audiobook.lastText):"",segments:segs,roster:_audiobook.roster||[],pageMarks:_audiobook.pageMarks||[],rehId:_audiobook.rehId||null,done:_audiobook.completedChunks||0,total:_audiobook.completedTotal||0,savedAt:Date.now()},blob=new Blob([_abCastMarkdown(payload)],{type:"text/markdown;charset=utf-8"});if(typeof readerDownload=="function")readerDownload(blob,`${_abSafeFilename(title)}_cast.md`);else{const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=`${_abSafeFilename(title)}_cast.md`,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),500)}}function _abStartDraftAutosave(saveNow){if(_abStopDraftAutosave(),typeof saveNow!="function")return;_abDraftAutosaveTimer=setInterval(saveNow,AUDIOBOOK_DRAFT_AUTOSAVE_MS);const saveOnLeave=()=>saveNow();_audiobook._draftSaveOnLeave=saveOnLeave,window.addEventListener("pagehide",saveOnLeave),window.addEventListener("beforeunload",saveOnLeave)}function _abStopDraftAutosave(){_abDraftAutosaveTimer&&clearInterval(_abDraftAutosaveTimer),_abDraftAutosaveTimer=null,_audiobook._draftSaveOnLeave&&(window.removeEventListener("pagehide",_audiobook._draftSaveOnLeave),window.removeEventListener("beforeunload",_audiobook._draftSaveOnLeave),_audiobook._draftSaveOnLeave=null)}async function _abEnsureLibraryBook(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2;const rs=window.readerState;if(!rs||rs.savedId||!((_a2=rs.sentences)!=null&&_a2.length))return!!(rs!=null&&rs.savedId);if(rs.mode==="pdf"&&!rs.fileBlob)return!1;const meta={title:rs.title||"Untitled",kind:rs.mode,idx:rs.idx,voice:((_b2=$("reader-voice-select"))==null?void 0:_b2.value)||"",backend:((_c2=$("reader-backend-select"))==null?void 0:_c2.value)||"",speed:rs.speed,instruct:((_d2=$("reader-instruct"))==null?void 0:_d2.value.trim())||"",chunkMode:rs.chunkMode,seed:((_e2=$("reader-seed"))==null?void 0:_e2.value.trim())||"",temperature:((_f2=$("reader-temp"))==null?void 0:_f2.value.trim())||"",tts_speed:parseFloat((_g2=$("reader-tts-speed"))==null?void 0:_g2.value)||1,normalize:rs.normalize,sentenceCount:rs.sentences.length,pageCount:((_h2=rs.pages)==null?void 0:_h2.length)||0,synthCount:0,updated:new Date().toISOString()};try{const r=await fetch("/api/reader/docs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(meta)});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);rs.savedId=(await r.json()).id,_audiobook.bookId=rs.savedId;const ext=rs.mode==="pdf"?"pdf":"txt",body=rs.mode==="pdf"?rs.fileBlob:new Blob([rs.docText||audiobookScopeText()||""],{type:"text/plain"});return(await fetch(`/api/reader/docs/${encodeURIComponent(rs.savedId)}/source?ext=${ext}`,{method:"PUT",body})).ok&&(rs.sourceUploaded=!0),typeof readerRenderLibrary=="function"&&readerRenderLibrary(),!0}catch(err){return console.warn("[audiobook] could not create reader-library autosave target:",err),!1}}async function _abLoadDraftServer(bookId){if(!bookId)return null;for(let attempt=0;attempt<2;attempt++)try{const r=await fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`);if(!r.ok){if(attempt===0&&(r.status===404||r.status>=500)){await new Promise(resolve=>setTimeout(resolve,650));continue}return null}const d=await r.json();return d&&Array.isArray(d.segments)&&d.segments.length?d:null}catch{attempt===0&&await new Promise(resolve=>setTimeout(resolve,650))}return null}function _abLoadDraft(text){var _a2;const bookId=_abBookId(),textId=_abTextId(text),title=String(((_a2=window.readerState)==null?void 0:_a2.title)||"").trim().toLowerCase();try{if(bookId){const raw2=localStorage.getItem(_abDraftKey(bookId));if(raw2){const d=JSON.parse(raw2);if(d&&Array.isArray(d.segments)&&d.segments.length)return d}}const raw=localStorage.getItem(_AB_DRAFT_KEY);if(raw){const d=JSON.parse(raw);if(d&&Array.isArray(d.segments)&&d.segments.length&&d.textId===textId)return d}let best=null;for(let i=0;ibest.score)&&(best={score,draft:cand})}catch{}}if(best!=null&&best.draft)return best.draft}catch{return null}return null}function _abClearDraft(){const bookId=_abBookId();try{localStorage.removeItem(_abDraftKey(bookId))}catch{}bookId&&fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`,{method:"DELETE"}).catch(()=>{})}const AB_DIALOGUE_RE=/[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/;function audiobookHasDialogue(t){return AB_DIALOGUE_RE.test(t||"")}function audiobookDehyphenate(t){return(t||"").replace(/([a-zäöüß])-\s+(?=[a-zäöüßA-ZÄÖÜ])/g,"$1")}const AB_SPEECH_VERBS="(?:sagte|fragte|rief|antwortete|erwiderte|entgegnete|meinte|fl\xFCsterte|wisperte|raunte|murmelte|brummte|knurrte|br\xFCllte|schrie|stammelte|fauchte|zischte|seufzte|lachte|kicherte|befahl|wiederholte|fuhr\\s+fort|grunzte|versetzte|unterbrach|dr\xE4ngte|herrschte|keuchte|hauchte|\xE4chzte|st\xF6hnte|jammerte|spottete|h\xF6hnte|warf\\s+ein|setzte\\s+hinzu|erkl\xE4rte|verk\xFCndete|widersprach|beharrte|gestand|best\xE4tigte|erinnerte|dachte|\xFCberlegte|sinnierte|said|asked|replied|answered|whispered|murmured|muttered|shouted|cried|called|exclaimed|added|continued)",AB_NOTNAME=new Set(["Der","Die","Das","Den","Dem","Ein","Eine","Einen","Er","Sie","Es","Ich","Du","Wir","Ihr","Man","Und","Aber","Da","Dann","Doch","So","Nun","Jetzt","The","He","She","It","They","A","An","And","But","Then","Now","Sofort","Pl\xF6tzlich","Endlich","Schlie\xDFlich","Stille","Schweigen","Stimme","Stimmen","Frage","Antwort","Gel\xE4chter","Wieder","Gleich","Sogleich","Langsam","Leise","Laut","Kaum","Vielleicht","Nat\xFCrlich","Wirklich","Ja","Nein","Komm","Warte","Halt","Geh","Hier","Dort","Oben","Unten","Schon","Noch","Auch","Nur","Immer","Nie"]),_AB_NAME="([A-Z\xC4\xD6\xDC][A-Za-z\xE4\xF6\xFC\xDF'\\-]+)",AB_PERSON_NOUNS=new Set(["Mann","Frau","Junge","M\xE4dchen","Alte","Alter","Fremde","Fremder","Krieger","Kriegerin","W\xE4chter","Wache","Soldat","Hauptmann","Ork","Ritter","Magier","Magierin","Zwerg","Elf","Elfe","H\xE4ndler","Wirt","Wirtin","Bauer","Priester","Priesterin","Nachbar","Nachbarin","Sklave","Sklavin","Verweser","Inquisitor","General","K\xF6nig","K\xF6nigin","Prinz","Prinzessin","F\xFCrst","F\xFCrstin","Baron","Baronin","Bote","Diener","Dienerin","Knabe","Kind","Reiter","Reiterin","Bogensch\xFCtze","Schmied","Schmiedin","Heiler","Heilerin","Gelehrte","Gelehrter","Kapit\xE4n","Anf\xFChrer","Anf\xFChrerin"]);function audiobookResolveUnknowns(segs,prevTail,roster){var _a2,_b2;const isUnknown=s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)),isNamed=s=>(s==null?void 0:s.type)==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt|Narrator$/i.test(s.speaker),names=(roster||[]).filter(n=>n&&!/^(Narrator|Unknown|Unbekannt)/i.test(n)).sort((a,b)=>b.length-a.length),lastNameIn=text=>{let best=null,bestAt=-1;for(const n of names){const at=text.lastIndexOf(n);at>bestAt&&(bestAt=at,best=n)}if(best)return best;const clause=text.match(new RegExp(_AB_NAME+"[^.!?:]{0,80}\\b(?:und\\s+)?(?:"+AB_SPEECH_VERBS+")[^:]{0,60}:\\s*$"));if(clause&&!AB_NOTNAME.has(clause[1])&&!AB_PERSON_NOUNS.has(clause[1]))return clause[1];const m=[...text.matchAll(/\b(?:[Dd]er|[Dd]ie|[Dd]en|[Dd]em|[Ee]in|[Ee]ine)\s+([A-ZÄÖÜ][a-zäöüß]{2,})\b/g)].map(x=>x[1]).filter(n=>AB_PERSON_NOUNS.has(n));return m.length?m[m.length-1]:null},all=[...prevTail||[],...segs],offset=(prevTail||[]).length,resolved=[];for(let i=offset;i=0&&j>=i-6&&prevDialogues.length<2&&!(((_a2=all[j])==null?void 0:_a2.page)!=null&&s.page!=null&&all[j].page!==s.page);j--)if(((_b2=all[j])==null?void 0:_b2.type)==="dialogue"){if(!isNamed(all[j])){prevDialogues.length=0;break}prevDialogues.push(all[j].speaker)}prevDialogues.length===2&&prevDialogues[0]!==prevDialogues[1]&&(who=prevDialogues[1])}who&&_nextIsInquitText&&next&&next.type!=="narration"&&(next.type="narration",next.speaker="Narrator",next.emotion=""),who&&!/^(Narrator|Unknown|Unbekannt)$/i.test(who)&&(s.speaker=who,resolved.push(who))}return resolved}const _AB_TAG_NAME="[A-Z\xC4\xD6\xDC][A-Za-z\xE4\xF6\xFC\xDF'\\-]+",AB_SPEECH_TAG_ONLY_RE=new RegExp("^\\s*[,.;:!?\u2013-]*\\s*(?:(?:"+AB_SPEECH_VERBS+")\\b\\s+(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|"+_AB_TAG_NAME+")|(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|"+_AB_TAG_NAME+")\\s+(?:"+AB_SPEECH_VERBS+")\\b)(?:\\s+(?:mit|in|leise|laut|kalt|heiser|erstickt|ruhig|zornig|w\xFCtend|\xE4ngstlich|sp\xF6ttisch|ver\xE4chtlich|fragend|fl\xFCsternd|schrill|dumpf|slowly|coldly|quietly|softly|angrily|hoarsely)\\b[\\s\\S]*)?[.!?\u2026]*\\s*$","i");function audiobookIsSpeechTagOnly(text){const t=String(text||"").trim();return!t||t.length>180||/[»«„“”"‟‚‘’『「]/.test(t)?!1:AB_SPEECH_TAG_ONLY_RE.test(t)}function audiobookGuessSpeaker(after,before){let m;const ok=n=>n&&!AB_NOTNAME.has(n)?n:null;if(m=new RegExp("^[\\s,;\u2013-]*"+AB_SPEECH_VERBS+"\\s+(?:der|die|das|ein|eine)?\\s*"+_AB_NAME).exec(after||"")){const r=ok(m[1]);if(r)return r}if(m=new RegExp("^[\\s,;\u2013-]*"+_AB_NAME+"\\s+"+AB_SPEECH_VERBS).exec(after||"")){const r=ok(m[1]);if(r)return r}if(m=new RegExp(_AB_NAME+"\\s+"+AB_SPEECH_VERBS+"[\\s:,\u2013-]*$").exec(before||"")){const r=ok(m[1]);if(r)return r}return null}const AB_QUOTE_PAIRS={"\xBB":"\xAB","\xAB":"\xBB","\u201E":"\u201C","\u201C":"\u201D",'"':'"',"\u300C":"\u300D","\u300E":"\u300F","\u2018":"\u2019","\u201A":"\u2018"},_AB_UNCLOSED_TAG_RE=new RegExp("^\\s*(?:[,;:\u2013-]\\s*)?(?:"+AB_SPEECH_VERBS+"|(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|I|we|you)\\s+"+AB_SPEECH_VERBS+")\\b","i");function _abUnclosedQuoteEnd(raw,closeIdx){const limit=closeIdx>=0?closeIdx:raw.length;for(let i=0;i40)return m[1].length}return closeIdx>=0?closeIdx:raw.length}function _abOrphanClosingQuoteSpan(text,closeIdx,minStart){var _a2;if(!"\xAB\u201D\u2019\u2039\u300D\u300F".includes(text[closeIdx]))return null;const before=text.slice(minStart,closeIdx),prev=((_a2=before.match(/\S(?=\s*$)/))==null?void 0:_a2[0])||"";if(!/[.!?]/.test(prev))return null;const trimmedLen=before.trimEnd().length;let localStart=0;const boundaryRe=/[\n\r]|[.!?:]\s+/g;let m;for(;m=boundaryRe.exec(before);){const next=m.index+m[0].length;nextch&&(AB_QUOTE_OPENERS+AB_QUOTE_CLOSERS).includes(ch);for(let i=0;is.text.trim())}function _audiobookMergeAdjacentSameSpeaker(segments){const out=[];for(const s of segments){const last=out[out.length-1];if(last&&last.type===s.type&&last.page===s.page){const lastSpeaker=(last.speaker||"Narrator").trim().toLowerCase(),curSpeaker=(s.speaker||"Narrator").trim().toLowerCase(),sameEmotion=s.type!=="dialogue"||(last.emotion||"").toLowerCase()===(s.emotion||"").toLowerCase(),unknownSpeaker=!lastSpeaker||lastSpeaker==="unknown"||lastSpeaker==="unbekannt";if(lastSpeaker===curSpeaker&&sameEmotion&&!(s.type==="dialogue"&&unknownSpeaker)){const lastText=last.text.trimEnd(),curText=s.text.trimStart();last.text=/[.!?…»«”"’']$/.test(lastText)?lastText+` -`+curText:lastText+" "+curText;continue}}out.push(Object.assign({},s))}return out}const _AB_TITLE_PREFIX=/^(?:der|die|das|den|dem|ein|eine|einer|herr|frau|oberst|obrist|graf|gräfin|ritter|hauptmann|kommandant|meister|bruder|schwester|prinz|prinzessin|könig|königin|alter|alte|junger|junge|blonder|blonde)\s+/i;function _abIdentityTokens(name){let n=String(name||"").trim().toLowerCase(),prev=null;for(;prev!==n;)prev=n,n=n.replace(_AB_TITLE_PREFIX,"");return n.split(/[\s\-]+/).filter(Boolean)}const _AB_NOT_A_CHARACTER=new Set(["inneren","innere","herz","herzen","hand","h\xE4nde","haenden","kopf","stimme","stimmen","wind","feuer","flamme","flammen","dunkelheit","stille","schatten","licht","nacht","angst","schmerz","gedanke","gedanken","traum","tr\xE4ume","geliebten","geliebter","geliebte","blondschopf","antwort","echo","ferne","n\xE4he","naehe","tiefe","wasser","regen","donner","erde","himmel","sonne","mond","tod","leben","seele","geist der","wahrheit","zweifel","hoffnung","erinnerung","vergangenheit"]);function _abIsImplausibleSpeaker(name){const n=String(name||"").trim().toLowerCase().replace(/^(?:der|die|das|den|dem|ein|eine|einer|einem)\s+/,"");return n?_AB_NOT_A_CHARACTER.has(n):!1}function _audiobookDropPhantomSpeakers(segments){let dropped=0;const names=new Set;for(const s of segments)(s==null?void 0:s.type)==="dialogue"&&_abIsImplausibleSpeaker(s.speaker)&&(names.add(s.speaker),s.speaker="Unknown",dropped++);return{dropped,names:[...names]}}window._audiobookDropPhantomSpeakers=_audiobookDropPhantomSpeakers;function _audiobookConsolidateSpeakerAliases(segments){const count=new Map;for(const s of segments){if((s==null?void 0:s.type)!=="dialogue")continue;const sp=String(s.speaker||"").trim();!sp||/^(unknown|unbekannt|narrator)/i.test(sp)||count.set(sp,(count.get(sp)||0)+1)}const names=[...count.keys()],tokens=new Map(names.map(n=>[n,new Set(_abIdentityTokens(n))])),mapping=new Map;for(const a of names){const ta=tokens.get(a);if(!ta.size)continue;let best=null;for(const b of names){if(a===b)continue;const tb=tokens.get(b);if(!tb.size||tb.size<=ta.size)continue;let subset=!0;for(const t of ta)if(!tb.has(t)){subset=!1;break}subset&&(!best||count.get(b)>count.get(best)||count.get(b)===count.get(best)&&b.length>best.length)&&(best=b)}best&&mapping.set(a,best)}let moved=0;if(mapping.size)for(const s of segments){if((s==null?void 0:s.type)!=="dialogue")continue;const to=mapping.get(String(s.speaker||"").trim());to&&(s.speaker=to,moved++)}return{merged:mapping.size,moved,mapping}}window._audiobookConsolidateSpeakerAliases=_audiobookConsolidateSpeakerAliases;function _audiobookMergeSplitSentences(segments){const out=[];let merged=0;for(const s of segments){const prev=out[out.length-1],cur=String((s==null?void 0:s.text)||"").trim();if(prev&&prev.type==="narration"&&(s==null?void 0:s.type)==="narration"&&cur){const prevText=String(prev.text||"").trimEnd(),endsOpen=prevText&&!/[.!?:;…»"'\)\]]$/.test(prevText),startsLower=/^[a-zäöüß]/.test(cur),prevIsInquit=new RegExp(_AB_INQUIT_VERBS+"[^.!?]{0,40},\\s*$","i").test(prevText);if(endsOpen&&startsLower&&!prevIsInquit){prev.text=prevText+" "+cur,merged++;continue}}out.push(s)}return{segments:out,merged}}function _audiobookDedupNearbyDuplicates(segments){const QUOTE_CHARS='\xBB\xAB\u201E"\u2018\u2019\u203A\u2039',stripRe=new RegExp(`(^[${QUOTE_CHARS}\\s]+)|([${QUOTE_CHARS}\\s]+$)`,"g"),norm=t=>String(t||"").replace(stripRe,"").toLowerCase().replace(/\s+/g," ").trim(),WINDOW=30,MIN_LEN=20,out=[];let removed=0;for(const s of segments){const nt=norm(s==null?void 0:s.text);if(nt.length>=MIN_LEN){let isDup=!1;for(let k=out.length-1;k>=0&&out.length-k<=WINDOW;k--)if(out[k].type===s.type&&norm(out[k].text)===nt){isDup=!0;break}if(isDup){removed++;continue}}out.push(s)}return{segments:out,removed}}function audiobookSplitByQuotes(text){const spans=[];for(let i=0;i0&&!/[\s([{—–-]/.test(text[i-1]))continue;const raw=text.slice(i+1),closeIdx=raw.indexOf(close),endInRaw=_abUnclosedQuoteEnd(raw,closeIdx),quote=raw.slice(0,endInRaw).trim();if(!quote)continue;const consumedClose=closeIdx>=0&&endInRaw===closeIdx;spans.push({start:i,end:i+1+endInRaw+(consumedClose?1:0),quote}),i=spans[spans.length-1].end-1}if(!spans.length)return[{speaker:"Narrator",type:"narration",text,emotion:""}];const out=[];let last=0;for(let k=0;k{});const rehModel=$("reh-llm-model");return rehModel&&cleanModel&&[...rehModel.options].some(o=>o.value===cleanModel)&&(rehModel.value=cleanModel),{url:cleanUrl,model:cleanModel}}function audiobookScopeText(){var _a2,_b2,_c2;if(_audiobook.pageMarks=[],typeof readerScopeIndices!="function"||!((_a2=readerState==null?void 0:readerState.sentences)!=null&&_a2.length))return"";const idxs=readerScopeIndices(),anchors=[];let lastPage=null;for(const i of idxs){const u=readerState.sentences[i],pg=(_c2=(_b2=u==null?void 0:u.words)==null?void 0:_b2[0])==null?void 0:_c2.page;pg!=null&&pg!==lastPage&&(anchors.push({page:pg,anchor:(u.text||"").trim().slice(0,40)}),lastPage=pg)}let raw="";for(const i of idxs){const u=readerState.sentences[i];u!=null&&u.text&&(raw+=raw?(u.paraStart?` +`+curText:lastText+" "+curText;continue}}out.push(Object.assign({},s))}return out}const _AB_TITLE_PREFIX=/^(?:der|die|das|den|dem|ein|eine|einer|herr|frau|oberst|obrist|graf|gräfin|ritter|hauptmann|kommandant|meister|bruder|schwester|prinz|prinzessin|könig|königin|alter|alte|junger|junge|blonder|blonde)\s+/i;function _abIdentityTokens(name){let n=String(name||"").trim().toLowerCase(),prev=null;for(;prev!==n;)prev=n,n=n.replace(_AB_TITLE_PREFIX,"");return n.split(/[\s\-]+/).filter(Boolean)}const _AB_NOT_A_CHARACTER=new Set(["hinter","vor","neben","\xFCber","unter","zwischen","gegen\xFCber","keiner","keine","niemand","jemand","alle","beide","einige","viele","andere","anderen","jeder","jede","man","von","zu","bei","nach","aus","mit","ihm","ihr","ihnen","dort","hier","drau\xDFen","drinnen","oben","unten","pl\xF6tzlich","endlich","schlie\xDFlich","inneren","innere","herz","herzen","hand","h\xE4nde","haenden","kopf","stimme","stimmen","wind","feuer","flamme","flammen","dunkelheit","stille","schatten","licht","nacht","angst","schmerz","gedanke","gedanken","traum","tr\xE4ume","geliebten","geliebter","geliebte","blondschopf","antwort","echo","ferne","n\xE4he","naehe","tiefe","wasser","regen","donner","erde","himmel","sonne","mond","tod","leben","seele","geist der","wahrheit","zweifel","hoffnung","erinnerung","vergangenheit"]);function _abIsImplausibleSpeaker(name){const n=String(name||"").trim().toLowerCase().replace(/^(?:der|die|das|den|dem|ein|eine|einer|einem)\s+/,"");return n?_AB_NOT_A_CHARACTER.has(n):!1}function _audiobookDropPhantomSpeakers(segments){let dropped=0;const names=new Set;for(const s of segments)(s==null?void 0:s.type)==="dialogue"&&_abIsImplausibleSpeaker(s.speaker)&&(names.add(s.speaker),s.speaker="Unknown",dropped++);return{dropped,names:[...names]}}window._audiobookDropPhantomSpeakers=_audiobookDropPhantomSpeakers;function _audiobookConsolidateSpeakerAliases(segments){const count=new Map;for(const s of segments){if((s==null?void 0:s.type)!=="dialogue")continue;const sp=String(s.speaker||"").trim();!sp||/^(unknown|unbekannt|narrator)/i.test(sp)||count.set(sp,(count.get(sp)||0)+1)}const names=[...count.keys()],tokens=new Map(names.map(n=>[n,new Set(_abIdentityTokens(n))])),mapping=new Map;for(const a of names){const ta=tokens.get(a);if(!ta.size)continue;let best=null;for(const b of names){if(a===b)continue;const tb=tokens.get(b);if(!tb.size||tb.size<=ta.size)continue;let subset=!0;for(const t of ta)if(!tb.has(t)){subset=!1;break}subset&&(!best||count.get(b)>count.get(best)||count.get(b)===count.get(best)&&b.length>best.length)&&(best=b)}best&&mapping.set(a,best)}let moved=0;if(mapping.size)for(const s of segments){if((s==null?void 0:s.type)!=="dialogue")continue;const to=mapping.get(String(s.speaker||"").trim());to&&(s.speaker=to,moved++)}return{merged:mapping.size,moved,mapping}}window._audiobookConsolidateSpeakerAliases=_audiobookConsolidateSpeakerAliases;function _audiobookMergeSplitSentences(segments){const out=[];let merged=0;for(const s of segments){const prev=out[out.length-1],cur=String((s==null?void 0:s.text)||"").trim();if(prev&&prev.type==="narration"&&(s==null?void 0:s.type)==="narration"&&cur){const prevText=String(prev.text||"").trimEnd(),endsOpen=prevText&&!/[.!?:;…»"'\)\]]$/.test(prevText),startsLower=/^[a-zäöüß]/.test(cur),prevIsInquit=new RegExp(_AB_INQUIT_VERBS+"[^.!?]{0,40},\\s*$","i").test(prevText);if(endsOpen&&startsLower&&!prevIsInquit){prev.text=prevText+" "+cur,merged++;continue}}out.push(s)}return{segments:out,merged}}function _audiobookDedupNearbyDuplicates(segments){const QUOTE_CHARS='\xBB\xAB\u201E"\u2018\u2019\u203A\u2039',stripRe=new RegExp(`(^[${QUOTE_CHARS}\\s]+)|([${QUOTE_CHARS}\\s]+$)`,"g"),norm=t=>String(t||"").replace(stripRe,"").toLowerCase().replace(/\s+/g," ").trim(),WINDOW=30,MIN_LEN=20,out=[];let removed=0;for(const s of segments){const nt=norm(s==null?void 0:s.text);if(nt.length>=MIN_LEN){let isDup=!1;for(let k=out.length-1;k>=0&&out.length-k<=WINDOW;k--)if(out[k].type===s.type&&norm(out[k].text)===nt){isDup=!0;break}if(isDup){removed++;continue}}out.push(s)}return{segments:out,removed}}function audiobookSplitByQuotes(text){const spans=[];for(let i=0;i0&&!/[\s([{—–-]/.test(text[i-1]))continue;const raw=text.slice(i+1),closeIdx=raw.indexOf(close),endInRaw=_abUnclosedQuoteEnd(raw,closeIdx),quote=raw.slice(0,endInRaw).trim();if(!quote)continue;const consumedClose=closeIdx>=0&&endInRaw===closeIdx;spans.push({start:i,end:i+1+endInRaw+(consumedClose?1:0),quote}),i=spans[spans.length-1].end-1}if(!spans.length)return[{speaker:"Narrator",type:"narration",text,emotion:""}];const out=[];let last=0;for(let k=0;k{});const rehModel=$("reh-llm-model");return rehModel&&cleanModel&&[...rehModel.options].some(o=>o.value===cleanModel)&&(rehModel.value=cleanModel),{url:cleanUrl,model:cleanModel}}function audiobookScopeText(){var _a2,_b2,_c2;if(_audiobook.pageMarks=[],typeof readerScopeIndices!="function"||!((_a2=readerState==null?void 0:readerState.sentences)!=null&&_a2.length))return"";const idxs=readerScopeIndices(),anchors=[];let lastPage=null;for(const i of idxs){const u=readerState.sentences[i],pg=(_c2=(_b2=u==null?void 0:u.words)==null?void 0:_b2[0])==null?void 0:_c2.page;pg!=null&&pg!==lastPage&&(anchors.push({page:pg,anchor:(u.text||"").trim().slice(0,40)}),lastPage=pg)}let raw="";for(const i of idxs){const u=readerState.sentences[i];u!=null&&u.text&&(raw+=raw?(u.paraStart?` `:" ")+u.text:u.text)}raw=raw.replace(/[ \t]+/g," ").replace(/\n{3,}/g,` @@ -1197,7 +1197,7 @@ ${lines.trim()} ${avatar} ${escHtml(c.name)} ${lineCount} - `},render=()=>{const q=filter.trim().toLowerCase(),items=existingChars.filter(c=>!q||(c.name||"").toLowerCase().includes(q)).sort(sort==="alpha"?(a,b)=>(a.name||"").localeCompare(b.name||""):(a,b)=>{var _a2,_b2;return(((_a2=b.sheet)==null?void 0:_a2.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)}),list=ov.querySelector(".ab-recast-select-list");list&&(list.innerHTML=items.length?items.map(rowHtml).join(""):'
No matches
',list.querySelectorAll(".ab-recast-cb").forEach(cb=>{cb.addEventListener("change",()=>{cb.checked?selected.add(cb.value):selected.delete(cb.value)})}))};ov.innerHTML=`
Cast selected character roles
Only re-reads the passages that mention the characters you pick below, including known aliases, plus a little surrounding context. Everyone else's sheet stays as-is.
`,document.body.appendChild(ov),render();const sortSel=ov.querySelector("#ab-recast-sort");sortSel.value=sort,sortSel.addEventListener("change",()=>{sort=sortSel.value;try{localStorage.setItem("ttsvc_ab_recast_sort",sort)}catch{}render()});let _searchT=null;ov.querySelector("#ab-recast-search").addEventListener("input",e=>{clearTimeout(_searchT);const v=e.target.value;_searchT=setTimeout(()=>{filter=v,render()},120)}),ov.querySelector("#ab-recast-cancel").addEventListener("click",()=>ov.remove()),ov.querySelector("#ab-recast-select-all").addEventListener("click",e=>{const allSelected=existingChars.length>0&&existingChars.every(c=>selected.has(c.name));allSelected?selected.clear():existingChars.forEach(c=>selected.add(c.name)),e.currentTarget.textContent=allSelected?"Select all":"Select none",render()}),ov.querySelector("#ab-recast-confirm").addEventListener("click",async()=>{const names=[...selected];if(!names.length){toast("Select at least one character","error");return}ov.remove(),typeof window.audiobookRecastSelectedCharacters=="function"?await window.audiobookRecastSelectedCharacters(names):typeof window.csForReaderSelective=="function"&&await window.csForReaderSelective(names)})}async function audiobookRecastSelectedCharacters(selectedNames){return typeof window.csForReaderSelective=="function"?window.csForReaderSelective(selectedNames):(toast("Character-sheet recast is unavailable right now","error"),null)}window.audiobookRecastSelectedCharacters=audiobookRecastSelectedCharacters;function _abScrollIntoView(el,opts){el&&(el.scrollIntoView(Object.assign({behavior:"auto"},opts)),requestAnimationFrame(()=>requestAnimationFrame(()=>{el.scrollIntoView(Object.assign({behavior:"smooth"},opts))})))}function audiobookCastView(total,llmUrl,defaultModel,isIdle=!1){var _a2,_b2,_c2,_d2,_e2,_f2,_g2;const panel=document.getElementById("reader-audiobook-panel");if(!panel)return;if(typeof window.navReaderView=="function")window.navReaderView("cast");else if(typeof window.showReaderView=="function")window.showReaderView("cast");else{const mainView=document.getElementById("reader-main-view");mainView&&(mainView.hidden=!0),panel.hidden=!1}panel.className="ab-castpanel-inline card",panel.style.display="",panel.style.flexDirection="",panel.style.minHeight="",panel.innerHTML=` + `},render=()=>{const q=filter.trim().toLowerCase(),items=existingChars.filter(c=>!q||(c.name||"").toLowerCase().includes(q)).sort(sort==="alpha"?(a,b)=>(a.name||"").localeCompare(b.name||""):(a,b)=>{var _a2,_b2;return(((_a2=b.sheet)==null?void 0:_a2.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)}),list=ov.querySelector(".ab-recast-select-list");list&&(list.innerHTML=items.length?items.map(rowHtml).join(""):'
No matches
',list.querySelectorAll(".ab-recast-cb").forEach(cb=>{cb.addEventListener("change",()=>{cb.checked?selected.add(cb.value):selected.delete(cb.value)})}))};ov.innerHTML=`
Select characters to cast or recast roles
Only re-reads the passages that mention the characters you pick below, including known aliases, plus a little surrounding context. Everyone else's sheet stays as-is.
`,document.body.appendChild(ov),render();const sortSel=ov.querySelector("#ab-recast-sort");sortSel.value=sort,sortSel.addEventListener("change",()=>{sort=sortSel.value;try{localStorage.setItem("ttsvc_ab_recast_sort",sort)}catch{}render()});let _searchT=null;ov.querySelector("#ab-recast-search").addEventListener("input",e=>{clearTimeout(_searchT);const v=e.target.value;_searchT=setTimeout(()=>{filter=v,render()},120)}),ov.querySelector("#ab-recast-cancel").addEventListener("click",()=>ov.remove()),ov.querySelector("#ab-recast-select-all").addEventListener("click",e=>{const allSelected=existingChars.length>0&&existingChars.every(c=>selected.has(c.name));allSelected?selected.clear():existingChars.forEach(c=>selected.add(c.name)),e.currentTarget.textContent=allSelected?"Select all":"Select none",render()}),ov.querySelector("#ab-recast-confirm").addEventListener("click",async()=>{const names=[...ov.querySelectorAll(".ab-recast-cb:checked")].map(cb=>cb.value).filter(Boolean);if(!names.length){toast("Select at least one character","error");return}ov.remove(),typeof window.audiobookRecastSelectedCharacters=="function"?await window.audiobookRecastSelectedCharacters(names):typeof window.csForReaderSelective=="function"&&await window.csForReaderSelective(names)})}async function audiobookRecastSelectedCharacters(selectedNames){return typeof window.csForReaderSelective=="function"?window.csForReaderSelective(selectedNames):(toast("Character-sheet recast is unavailable right now","error"),null)}window.audiobookRecastSelectedCharacters=audiobookRecastSelectedCharacters;function _abScrollIntoView(el,opts){el&&(el.scrollIntoView(Object.assign({behavior:"auto"},opts)),requestAnimationFrame(()=>requestAnimationFrame(()=>{el.scrollIntoView(Object.assign({behavior:"smooth"},opts))})))}function audiobookCastView(total,llmUrl,defaultModel,isIdle=!1){var _a2,_b2,_c2,_d2,_e2,_f2,_g2;const panel=document.getElementById("reader-audiobook-panel");if(!panel)return;if(typeof window.navReaderView=="function")window.navReaderView("cast");else if(typeof window.showReaderView=="function")window.showReaderView("cast");else{const mainView=document.getElementById("reader-main-view");mainView&&(mainView.hidden=!0),panel.hidden=!1}panel.className="ab-castpanel-inline card",panel.style.display="",panel.style.flexDirection="",panel.style.minHeight="",panel.innerHTML=`
Casting audiobook @@ -1457,7 +1457,7 @@ ${newRules}`}return/gehören IMMER zum Dialog-Segment/.test(p)||(p=p.replace("-

           
- `,this._procRow.querySelector(".ab-cv-expand-btn").addEventListener("click",function(){const split=this.closest(".ab-cv-llm-row").querySelector(".ab-cv-llm-split"),open=this.getAttribute("aria-expanded")==="true";this.setAttribute("aria-expanded",String(!open)),split.hidden=open;const icon=this.querySelector("span");icon&&(icon.className=open?"mdi mdi-chevron-right":"mdi mdi-chevron-down")}),_abPage().appendChild(this._procRow),trim()},thinking(delta){if(!this._procRow||!delta||this._thinkDone)return;this._thinkRaw=(this._thinkRaw||"")+delta;const think=this._procRow.querySelector(".ab-cv-think"),label=this._procRow.querySelector(".ab-cv-think-label");if(!think)return;const openAt=this._thinkRaw.indexOf("");let shown,isReal=!1,closeAt=-1;if(openAt>=0){const afterOpen=this._thinkRaw.slice(openAt+7);closeAt=afterOpen.indexOf(""),shown=closeAt>=0?afterOpen.slice(0,closeAt):afterOpen,isReal=!0}else shown=this._thinkRaw;label&&(label.innerHTML=isReal?' LLM Thinking\u2026':' Live Output (raw \u2014 no reasoning exposed)'),think.textContent=shown.trim().slice(-2e4),think.scrollTop=think.scrollHeight,closeAt>=0&&(this._thinkDone=!0)},clearProcessing(){this._procRow&&(this._procRow.remove(),this._procRow=null)},addSegments(segs){var _a3,_b3;this.clearProcessing(),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove();const makeRow=s=>{const{speakerName}=_abRowSpeaker(s);return colorFor(speakerName),roster.get(speakerName).count++,_abRowFromSegment(s)},CHUNK=80,first=segs.slice(0,CHUNK),frag0=document.createDocumentFragment();if(first.forEach(s=>frag0.appendChild(makeRow(s))),_abPage().appendChild(frag0),renderRoster(),segs.length>CHUNK){let i=CHUNK;const next=()=>{if(i>=segs.length){trim(),renderRoster();return}const batch=document.createDocumentFragment(),end=Math.min(i+CHUNK,segs.length);for(;i=0?prevIdx+1:0),segsAll=_audiobook.segments||[],gapTo=Math.min(segsAll.length-1,nextIdx>=0?nextIdx-1:segsAll.length-1),gapCount=Math.max(0,gapTo-gapFrom+1);r.innerHTML=`\u22EF ${gapCount>0?gapCount+" line"+(gapCount!==1?"s":"")+" hidden \u2014 ":""}click to expand`,r.title="Click to reveal the text between these passages",r.addEventListener("click",()=>{const gap=segsAll.slice(gapFrom,gapTo+1);if(!gap.length){r.remove();return}const PEEK=20,show=gap.slice(0,PEEK),rest=gap.slice(PEEK),frag=document.createDocumentFragment();for(const s of show){const isNarr=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator",spk=isNarr?"Narrator":s.speaker,c=colorFor(spk),row=document.createElement("div");row.className="ab-cv-row ab-cv-ctx-row"+(isNarr?" is-narr":""),row.__seg=s,row.innerHTML=`${escHtml(spk)}${s.emotion?' ('+escHtml(s.emotion)+")":""}${_abMarkQuotes(highlightText2(_abDisplayText(s)))}`,frag.appendChild(row)}if(rest.length){const next=document.createElement("div");next.className="ab-cv-divider ab-cv-divider-expand",next.innerHTML=`\u22EF ${rest.length} line${rest.length!==1?"s":""} hidden \u2014 click to expand`,next.title="Click to reveal more";const restSegs=rest;next.addEventListener("click",function(){const f2=document.createDocumentFragment();for(const s of restSegs){const isNarr=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator",spk=isNarr?"Narrator":s.speaker,c=colorFor(spk),row=document.createElement("div");row.className="ab-cv-row ab-cv-ctx-row"+(isNarr?" is-narr":""),row.__seg=s,row.innerHTML=`${escHtml(spk)}${s.emotion?' ('+escHtml(s.emotion)+")":""}${_abMarkQuotes(highlightText2(_abDisplayText(s)))}`,f2.appendChild(row)}next.replaceWith(f2)}),frag.appendChild(next)}r.replaceWith(frag)}),_abPage().appendChild(r),trim()},pagemark(pageNum){this.clearProcessing(),_abNewPage("Page "+pageNum,pageNum),trim()},complete(summary,onOpen,onRecast,onRecastUnknown){var _a3,_b3;count&&(count.hidden=!0);const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!0);const completedSegments=Array.isArray(_audiobook.segments)&&_audiobook.segments.length?_audiobook.segments:typeof allSegments!="undefined"&&allSegments.length?allSegments:null;completedSegments&&_abRedrawSegmentsChunked(completedSegments);const cancelBtn=panel.querySelector("#ab-cv-cancel");cancelBtn&&(cancelBtn.hidden=!0);const foot=panel.querySelector("#ab-cv-foot");foot.hidden=!1;const sideEl=panel.querySelector(".ab-cv-side");if(sideEl){let doneEl=sideEl.querySelector(".ab-cv-side-done");doneEl||(doneEl=document.createElement("div"),doneEl.className="ab-cv-side-done",sideEl.appendChild(doneEl)),doneEl.innerHTML=` ${escHtml(summary)}`}const continueBtnHtml=_audiobook.completedChunks>0&&_audiobook.completedTotal>0&&_audiobook.completedChunks<_audiobook.completedTotal?``:"";foot.innerHTML=`${continueBtnHtml}`;const verificationPrompt=`Du bist ein Qualit\xE4tspr\xFCfer f\xFCr die Sprecherzuordnung eines bereits analysierten deutschen H\xF6rbuch-Textes. Eine erste KI hat jedem Segment bereits einen Sprecher zugewiesen ('Narrator' oder einen Charakternamen, ggf. 'Unknown'). Du bekommst diese Zuweisung NICHT \u2014 du siehst nur den Text und musst selbst neu urteilen, ob die Zeile zu Narration oder zu gesprochener Rede eines Charakters geh\xF6rt, und falls Dialog: zu welchem. Das ist eine Gegenprobe, keine Neuklassifizierung von Grund auf: dein Job ist nicht "wer k\xF6nnte das gesagt haben", sondern "ist diese Zeile wirklich Narration, oder wurde hier gesprochene Rede in den Erz\xE4hltext hineingezogen?". + `,this._procRow.querySelector(".ab-cv-expand-btn").addEventListener("click",function(){const split=this.closest(".ab-cv-llm-row").querySelector(".ab-cv-llm-split"),open=this.getAttribute("aria-expanded")==="true";this.setAttribute("aria-expanded",String(!open)),split.hidden=open;const icon=this.querySelector("span");icon&&(icon.className=open?"mdi mdi-chevron-right":"mdi mdi-chevron-down")}),_abPage().appendChild(this._procRow),trim()},thinking(delta){if(!this._procRow||!delta||this._thinkDone)return;this._thinkRaw=(this._thinkRaw||"")+delta;const think=this._procRow.querySelector(".ab-cv-think"),label=this._procRow.querySelector(".ab-cv-think-label");if(!think)return;const openAt=this._thinkRaw.indexOf("");let shown,isReal=!1,closeAt=-1;if(openAt>=0){const afterOpen=this._thinkRaw.slice(openAt+7);closeAt=afterOpen.indexOf(""),shown=closeAt>=0?afterOpen.slice(0,closeAt):afterOpen,isReal=!0}else shown=this._thinkRaw;label&&(label.innerHTML=isReal?' LLM Thinking\u2026':' Live Output (raw \u2014 no reasoning exposed)'),think.textContent=shown.trim().slice(-2e4),think.scrollTop=think.scrollHeight,closeAt>=0&&(this._thinkDone=!0)},clearProcessing(){this._procRow&&(this._procRow.remove(),this._procRow=null)},addSegments(segs){var _a3,_b3;this.clearProcessing(),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove();const makeRow=s=>{const{speakerName}=_abRowSpeaker(s);return colorFor(speakerName),roster.get(speakerName).count++,_abRowFromSegment(s)},CHUNK=80,first=segs.slice(0,CHUNK),frag0=document.createDocumentFragment();if(first.forEach(s=>frag0.appendChild(makeRow(s))),_abPage().appendChild(frag0),renderRoster(),segs.length>CHUNK){let i=CHUNK;const next=()=>{if(i>=segs.length){trim(),renderRoster();return}const batch=document.createDocumentFragment(),end=Math.min(i+CHUNK,segs.length);for(;i=0?prevIdx+1:0),segsAll=_audiobook.segments||[],gapTo=Math.min(segsAll.length-1,nextIdx>=0?nextIdx-1:segsAll.length-1),gapCount=Math.max(0,gapTo-gapFrom+1);r.innerHTML=`\u22EF ${gapCount>0?gapCount+" line"+(gapCount!==1?"s":"")+" hidden \u2014 ":""}click to expand`,r.title="Click to reveal the text between these passages",r.addEventListener("click",()=>{const gap=segsAll.slice(gapFrom,gapTo+1);if(!gap.length){r.remove();return}const PEEK=20,show=gap.slice(0,PEEK),rest=gap.slice(PEEK),frag=document.createDocumentFragment();for(const s of show){const isNarr=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator",spk=isNarr?"Narrator":s.speaker,c=colorFor(spk),row=document.createElement("div");row.className="ab-cv-row ab-cv-ctx-row"+(isNarr?" is-narr":""),row.__seg=s,row.innerHTML=`${escHtml(spk)}${s.emotion?' ('+escHtml(s.emotion)+")":""}${_abMarkQuotes(highlightText2(_abDisplayText(s)))}`,frag.appendChild(row)}if(rest.length){const next=document.createElement("div");next.className="ab-cv-divider ab-cv-divider-expand",next.innerHTML=`\u22EF ${rest.length} line${rest.length!==1?"s":""} hidden \u2014 click to expand`,next.title="Click to reveal more";const restSegs=rest;next.addEventListener("click",function(){const f2=document.createDocumentFragment();for(const s of restSegs){const isNarr=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator",spk=isNarr?"Narrator":s.speaker,c=colorFor(spk),row=document.createElement("div");row.className="ab-cv-row ab-cv-ctx-row"+(isNarr?" is-narr":""),row.__seg=s,row.innerHTML=`${escHtml(spk)}${s.emotion?' ('+escHtml(s.emotion)+")":""}${_abMarkQuotes(highlightText2(_abDisplayText(s)))}`,f2.appendChild(row)}next.replaceWith(f2)}),frag.appendChild(next)}r.replaceWith(frag)}),_abPage().appendChild(r),trim()},pagemark(pageNum){this.clearProcessing(),_abNewPage("Page "+pageNum,pageNum),trim()},complete(summary,onOpen,onRecast,onRecastUnknown){var _a3,_b3;_abHideCastingControl(),count&&(count.hidden=!0);const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!0);const completedSegments=Array.isArray(_audiobook.segments)&&_audiobook.segments.length?_audiobook.segments:typeof allSegments!="undefined"&&allSegments.length?allSegments:null;completedSegments&&_abRedrawSegmentsChunked(completedSegments);const cancelBtn=panel.querySelector("#ab-cv-cancel");cancelBtn&&(cancelBtn.hidden=!0);const foot=panel.querySelector("#ab-cv-foot");foot.hidden=!1;const sideEl=panel.querySelector(".ab-cv-side");if(sideEl){let doneEl=sideEl.querySelector(".ab-cv-side-done");doneEl||(doneEl=document.createElement("div"),doneEl.className="ab-cv-side-done",sideEl.appendChild(doneEl)),doneEl.innerHTML=` ${escHtml(summary)}`}const continueBtnHtml=_audiobook.completedChunks>0&&_audiobook.completedTotal>0&&_audiobook.completedChunks<_audiobook.completedTotal?``:"";foot.innerHTML=`${continueBtnHtml}`;const verificationPrompt=`Du bist ein Qualit\xE4tspr\xFCfer f\xFCr die Sprecherzuordnung eines bereits analysierten deutschen H\xF6rbuch-Textes. Eine erste KI hat jedem Segment bereits einen Sprecher zugewiesen ('Narrator' oder einen Charakternamen, ggf. 'Unknown'). Du bekommst diese Zuweisung NICHT \u2014 du siehst nur den Text und musst selbst neu urteilen, ob die Zeile zu Narration oder zu gesprochener Rede eines Charakters geh\xF6rt, und falls Dialog: zu welchem. Das ist eine Gegenprobe, keine Neuklassifizierung von Grund auf: dein Job ist nicht "wer k\xF6nnte das gesagt haben", sondern "ist diese Zeile wirklich Narration, oder wurde hier gesprochene Rede in den Erz\xE4hltext hineingezogen?". WORAUF DU PR\xDCFST \u2014 IN DIESER REIHENFOLGE (h\xF6chste Priorit\xE4t zuerst): 1. ZUERST alle 'Unknown'-Zeilen: L\xF6se den Sprecher \xFCber Kontext auf (Inquit-Formeln, Ping-Pong-Wechsel in Zwiegespr\xE4chen, Adressaten-Bezug, zuletzt genannte Person). 'Unknown' bleibt nur, wenn wirklich keine Ableitung m\xF6glich ist. Das ist die dringendste Kategorie \u2014 eine Zeile ohne jeden Sprecher ist schlimmer als eine falsch zugeordnete. @@ -1487,10 +1487,10 @@ ABSOLUTE REGELN: - Mische NIEMALS Narration und Dialog in einem Segment. - Sei konservativ: \xE4ndere eine Zeile nur, wenn du nach dieser Pr\xFCfung wirklich zu einem ANDEREN Ergebnis kommst als naheliegend w\xE4re \u2014 nicht jede Zeile muss sich \xE4ndern.`;window._abVerificationPrompt=verificationPrompt;const runVerificationPass=()=>{const choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);closePanel(),audiobookRecastUnknown(savedChoice.url,savedChoice.model,{prompt:verificationPrompt,includeNarrator:!0})},runConsistencyPass=async()=>{const active=_abActiveSegments(),segs=active.arr;if(!segs.length){toast("Nothing cast yet to check","error");return}const byName=new Map;segs.forEach((s,idx)=>{s.type!=="dialogue"||!s.speaker||/^Narrator$/i.test(s.speaker)||/^Unknown|Unbekannt/i.test(s.speaker)||(byName.has(s.speaker)||byName.set(s.speaker,[]),byName.get(s.speaker).push(idx))});const MIN_LINES=4,MAX_LINES_PER_CALL=60,candidates=[...byName.entries()].filter(([,idxs])=>idxs.length>=MIN_LINES);if(!candidates.length){toast("No characters with enough lines yet to check for consistency","error");return}const choice2=audiobookCurrentCastLlm(panel),savedChoice2=audiobookSaveLlmChoice(choice2.url,choice2.model);closePanel(),_abPushEditState(active.key,segs);const busy=_abShowBusyOverlay(`Checking voice consistency for ${candidates.length} character${candidates.length!==1?"s":""}\u2026`,!0);let checked=0,flaggedTotal=0,failedNames=[];const touchedSegs=[],knownNames=[...roster.keys()];try{for(const[name,idxs]of candidates){busy.setProgress(checked,candidates.length);let sampleIdxs=idxs;if(idxs.length>MAX_LINES_PER_CALL){const step=idxs.length/MAX_LINES_PER_CALL;sampleIdxs=Array.from({length:MAX_LINES_PER_CALL},(_,i)=>idxs[Math.floor(i*step)])}const lines=sampleIdxs.map(i=>({index:i,text:segs[i].text}));try{const r=await fetch("/api/audiobook-consistency-check",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({character:name,lines,known_characters:knownNames.filter(n=>n.toLowerCase()!==name.toLowerCase()),llm_url:savedChoice2.url,model:savedChoice2.model})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const data=await r.json(),outliers=Array.isArray(data==null?void 0:data.outliers)?data.outliers:[];for(const o of outliers){let idx=o.index;const quote=String(o.quote||"").trim().toLowerCase(),matchesQuote=i=>{var _a4;return!quote||(((_a4=segs[i])==null?void 0:_a4.text)||"").toLowerCase().includes(quote.slice(0,40))};if((typeof idx!="number"||!segs[idx]||segs[idx].speaker!==name||!matchesQuote(idx))&&(idx=quote?sampleIdxs.find(i=>segs[i].speaker===name&&matchesQuote(i)):void 0),typeof idx!="number"||!segs[idx]||segs[idx].speaker!==name)continue;const suggested=String(o.suggested_speaker||"").trim();let newSpeaker=null;!suggested||/^unknown|unbekannt$/i.test(suggested)?newSpeaker="Unknown":newSpeaker=knownNames.find(n=>n.toLowerCase()===suggested.toLowerCase())||null,!(!newSpeaker||newSpeaker===name)&&(segs[idx].speaker=newSpeaker,newSpeaker==="Unknown"&&(segs[idx].type="dialogue"),touchedSegs.push(segs[idx]),flaggedTotal++)}}catch(err){failedNames.push(name),console.error("[consistency check]",name,err)}checked++,busy.setProgress(checked,candidates.length)}}finally{busy.remove()}flaggedTotal&&(await _abPatchScatteredRows(touchedSegs,()=>{})||await _abRedrawSegmentsChunked(active.arr),_abPersistManualEdit());const failSuffix=failedNames.length?` (${failedNames.length} character${failedNames.length!==1?"s":""} failed to check: ${failedNames.slice(0,5).join(", ")})`:"";toast((flaggedTotal?`Consistency check: ${flaggedTotal} line${flaggedTotal!==1?"s":""} reassigned across ${checked} character${checked!==1?"s":""}`:`Consistency check: no mismatches found across ${checked} character${checked!==1?"s":""}`)+failSuffix,failedNames.length&&!flaggedTotal?"error":"success")};foot.querySelector("#ab-cv-open-reh").addEventListener("click",async()=>{closePanel(),await audiobookOpenCurrentInRehearser()});const applyPromptAndRun2=callback=>{const newPrompt=panel.querySelector("#ab-cv-prompt-text").value,choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt})}).finally(()=>{closePanel(),callback&&callback(savedChoice.url,savedChoice.model)})},runIdentifyAll=async()=>{await confirmDialog("Start from scratch? This will discard the current cast for this book and rebuild the character definitions from the text.",{title:"Discard current cast?",okLabel:"Start from scratch",danger:!0})&&applyPromptAndRun2(audiobookCast)},runIdentifyUnknown=()=>applyPromptAndRun2(audiobookRecastUnknown),runRepairNarration=()=>{const segs=_audiobook.segments||[],n=typeof _abSuspectNarration=="function"?_abSuspectNarration(segs).size:0;if(!n){toast("No narration lines look like mislabeled dialogue","success");return}toast(`Re-checking ${n} narration line${n!==1?"s":""} that look like spoken dialogue\u2026`,"info"),audiobookRecastUnknown(null,null,{repairNarration:!0,prompt:verificationPrompt})},runCastAll=()=>{typeof window.csForReader=="function"?window.csForReader():typeof csForReader=="function"?csForReader():toast("Character sheets not loaded yet","error")},runCastFresh=async()=>{await confirmDialog("Discard every generated character sheet for this book and rebuild every profile from a blank slate? This cannot be undone.",{title:"Discard all character sheets?",okLabel:"Discard & rebuild",danger:!0})&&(typeof window.csForReader=="function"?window.csForReader({fresh:!0}):typeof csForReader=="function"?csForReader({fresh:!0}):toast("Character sheets not loaded yet","error"))},runCastSelected=()=>{if(!existing.length){toast("No character sheets found yet for this book","error");return}_abOpenRecastSelectPopup(existing)},runCastContinueUncasted=async()=>{if(typeof clGetAllByTagOrBook!="function"||typeof csForReaderSelective!="function"||typeof CS_DETAIL_FIELDS=="undefined"){toast("Character sheets not loaded yet","error");return}let recs=[];try{recs=await clGetAllByTagOrBook(bookTitle)}catch{}const recByName=new Map;recs.forEach(r=>{var _a4;const name=String(r.name||"").trim().toLowerCase();if(name&&recByName.set(name,r),typeof clSplitIdentityTokens=="function")for(const a of clSplitIdentityTokens((_a4=r==null?void 0:r.sheet)==null?void 0:_a4.aliases,{aliases:!0})){const key=a.toLowerCase();recByName.has(key)||recByName.set(key,r)}});const roster2=(_audiobook.roster||[]).filter(n=>n&&!/^unknown$|^unbekannt$/i.test(n.trim())),MIN_LINES_TO_TRY=10,lineCounts=new Map;(_audiobook.segments||[]).forEach(s=>{if((s==null?void 0:s.type)!=="dialogue"||!s.speaker)return;const k=String(s.speaker).trim().toLowerCase();lineCounts.set(k,(lineCounts.get(k)||0)+1)});const tooSparse=[],incomplete=roster2.filter(n=>{const rec=recByName.get(String(n).trim().toLowerCase());return rec&&CS_DETAIL_FIELDS.some(f=>String((rec.sheet||{})[f]||"").trim())?!1:(lineCounts.get(String(n).trim().toLowerCase())||0){var _a4;if((_a4=document.getElementById("s-caststudio"))!=null&&_a4.classList.contains("is-active")&&typeof window.showStudioPhase=="function"){window.showStudioPhase(3);return}try{sessionStorage.setItem("ttsvc_cast_return","reader")}catch{}typeof navTo=="function"&&navTo("s-library"),typeof navLibraryView=="function"&&navLibraryView("characters")},runLibraryCleanup=async()=>{if(typeof clGetAllByTagOrBook!="function"||typeof clDelete!="function"){toast("Character library is not available right now","error");return}const roster2=(_audiobook.roster||[]).filter(n=>n&&!/^unknown$|^unbekannt$/i.test(n.trim()));if(!roster2.length){toast("No current roster to clean up against","error");return}const rosterSet=new Set(roster2.map(n=>n.trim().toLowerCase()));let recs=[];try{recs=await clGetAllByTagOrBook(bookTitle)}catch{}const toDelete=recs.filter(r=>!rosterSet.has(String(r.name||"").trim().toLowerCase()));if(!toDelete.length){toast(`Library already matches the current ${roster2.length}-name roster \u2014 nothing to remove`,"success");return}const preview=toDelete.slice(0,12).map(r=>r.name).join(", ")+(toDelete.length>12?`, +${toDelete.length-12} more`:"");if(!await confirmDialog(`Remove ${toDelete.length} character record${toDelete.length!==1?"s":""} that aren't in the current ${roster2.length}-name roster? This cannot be undone. -${preview}`,{title:"Clean up character library?",okLabel:`Remove ${toDelete.length}`,danger:!0}))return;let removed=0;for(const r of toDelete)try{await clDelete(r.id),removed++}catch{}toast(`Removed ${removed} character record${removed!==1?"s":""} not in the current roster`,"success")},bookTitle=((_a3=window.readerState)==null?void 0:_a3.title)||"";let existing=[];(async()=>{if(!(!bookTitle||typeof clGetAllByTagOrBook!="function"))try{existing=await clGetAllByTagOrBook(bookTitle)}catch{}})();const identifyMenu=foot.querySelector("#ab-cv-menu-identify"),castMenu=foot.querySelector("#ab-cv-menu-cast"),viewCastMenu=foot.querySelector("#ab-cv-menu-viewcast");identifyMenu==null||identifyMenu.addEventListener("click",()=>_abToggleFootMenu(identifyMenu,[{icon:"mdi-refresh",label:"1. Identify all characters",title:"Scan the text and build the cast list from scratch",onClick:runIdentifyAll,danger:!0},{icon:"mdi-auto-fix",label:"2. Auto-repair cast (2.1\u20132.5)",title:"Runs the repair sequence automatically: fix mislabeled dialogue, resolve unknown speakers, repeat while it helps, then a full verification only if still needed",onClick:()=>audiobookAutoRepairCast()},{icon:"mdi-comment-alert-outline",label:"2.1 Repair mislabeled dialogue",title:"Finds spoken lines that were typed as narration (usually because the \xBB \xAB quote marks were lost in extraction) and re-checks only those \u2014 far cheaper than verifying every narration line",onClick:runRepairNarration},{icon:"mdi-account-question-outline",label:"2.2 Identify unknown characters",title:"Re-scan only the unknown segments with the current prompt",onClick:runIdentifyUnknown},{icon:"mdi-repeat",label:"2.3 Run until < N unknown\u2026",title:"Repeats recast-unknown + narrator-verify passes automatically until the Unknown-speaker count drops below a target, or progress stalls",onClick:()=>{const input=window.prompt("Stop once fewer than this many Unknown speakers remain:","10");if(input==null)return;const threshold=parseInt(input,10);if(!Number.isFinite(threshold)||threshold<0){toast("Enter a whole number of 0 or more","error");return}audiobookRecastUntilThreshold(threshold)}},{icon:"mdi-shield-check-outline",label:"2.4 Verify all characters",title:"Second-pass plausibility check that keeps the existing cast and only corrects uncertain matches",onClick:runVerificationPass},{icon:"mdi-account-search-outline",label:"2.5 Check voice consistency",title:"Third-pass check: gathers every line already credited to each character across the whole book and flags any that don\u2019t match their established voice",onClick:runConsistencyPass}])),window._abRunConsistencyPass=runConsistencyPass,castMenu==null||castMenu.addEventListener("click",()=>_abToggleFootMenu(castMenu,[{icon:"mdi-account-multiple-plus-outline",label:"Cast all character roles",title:"Generate / refresh the character sheets for every cast character",onClick:runCastAll},{icon:"mdi-account-arrow-right-outline",label:"Continue uncasted characters",title:"Only generate profiles for characters with no detail yet \u2014 skips anyone already fully cast",onClick:runCastContinueUncasted},{icon:"mdi-account-check-outline",label:"Cast selected character roles",title:existing.length?"Generate / refresh the character sheets for selected cast characters":"No character sheets found yet for this book",disabled:!existing.length,onClick:runCastSelected},{divider:!0},{icon:"mdi-refresh",label:"New recast (discard & rebuild all)",title:"Discard every generated character sheet and rebuild every profile from scratch",onClick:runCastFresh,danger:!0}])),viewCastMenu==null||viewCastMenu.addEventListener("click",()=>_abToggleFootMenu(viewCastMenu,[{icon:"mdi-eye-outline",label:"View cast",title:"View the cast overview in the library",onClick:runViewCast},{icon:"mdi-folder-zip-outline",label:"Export cast archive (.zip)",title:"Download one zip: the cast as a readable Markdown script plus a Markdown sheet per character",onClick:audiobookExportCastMd},{divider:!0},{icon:"mdi-broom",label:"Clean up library to current roster",title:"Remove saved character records that aren't in the current roster \u2014 old spelling-variant duplicates and superseded names from past casting runs",onClick:runLibraryCleanup,danger:!0}])),(_b3=foot.querySelector("#ab-cv-continue"))==null||_b3.addEventListener("click",()=>applyPromptAndRun2((u,m)=>audiobookCast(u,m,{startIndex:_audiobook.completedChunks,segments:_audiobook.segments,roster:_audiobook.roster,narrationOnly:_audiobook.narratedPassages,degraded:_audiobook.degraded}))),panel.classList.add("ab-castpanel-done"),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1)},done(options={}){var _a3,_b3,_c3,_d3,_e3;if(!(options.stopped||_audiobook.cancel)){closePanel();return}this.clearProcessing(),count&&(count.hidden=!0);const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!0),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove();const message=options.message||"Casting stopped before any passage completed.";if(feed.querySelector(".ab-cv-row, .ab-cv-note, .ab-cv-divider, .ab-cv-page")){if(options.message){const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}}else{feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}chars.querySelector(".ab-char-item")||(chars.innerHTML='No completed characters yet.');const foot=panel.querySelector("#ab-cv-foot"),hasSegments=Array.isArray(_audiobook.segments)&&_audiobook.segments.length>0;foot.hidden=!1,foot.innerHTML=` ${escHtml(message)}${hasSegments?'':""}`,(_c3=foot.querySelector("#ab-cv-stopped-back"))==null||_c3.addEventListener("click",closePanel),(_d3=foot.querySelector("#ab-cv-stopped-review"))==null||_d3.addEventListener("click",async()=>{closePanel(),await audiobookOpenCurrentInRehearser()}),(_e3=foot.querySelector("#ab-cv-stopped-recast"))==null||_e3.addEventListener("click",()=>{var _a4;const newPrompt=(_a4=panel.querySelector("#ab-cv-prompt-text"))==null?void 0:_a4.value,currentChoice=audiobookCurrentCastLlm(panel),choice=audiobookSaveLlmChoice(currentChoice.url,currentChoice.model);typeof _appSettings!="undefined"&&newPrompt&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt||""})}).finally(()=>{closePanel(),audiobookCast(choice.url,choice.model)})}),panel.classList.add("ab-castpanel-done"),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1)},loadingRestore(){var _a3,_b3;(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent="Loading saved cast\u2026",feed.appendChild(r),chars.innerHTML='restoring\u2026';const statusMsg=panel.querySelector("#ab-cv-status-msg");statusMsg&&(statusMsg.style.display="inline-block",statusMsg.textContent="Checking saved cast before starting a new one.");const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn&&(castBtn.style.display="none")},setFreshState(message="Ready to identify characters."){var _a3,_b3;if((_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),!feed.querySelector(".ab-cv-row, .ab-cv-divider, .ab-cv-page")){feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}chars.querySelector(".ab-char-item")||(chars.innerHTML='No saved cast found.');const statusMsg=panel.querySelector("#ab-cv-status-msg");statusMsg&&(statusMsg.style.display="inline-block",statusMsg.textContent=message);const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn&&(castBtn.style.display="inline-block",castBtn.addEventListener("click",()=>{var _a4;const newPrompt=(_a4=panel.querySelector("#ab-cv-prompt-text"))==null?void 0:_a4.value,currentChoice=audiobookCurrentCastLlm(panel),choice=audiobookSaveLlmChoice(currentChoice.url,currentChoice.model);typeof _appSettings!="undefined"&&newPrompt&&(_appSettings.audiobook_prompt=newPrompt),audiobookCast(choice.url,choice.model)}))}}}async function audiobookRecastUnknown(overrideUrl,overrideModel,options={}){var _a2,_b2,_c2,_d2,_e2,_f2;if(_audiobook.running)return;const segs=_audiobook.segments;if(!segs||!segs.length)return;const originalSegments=segs.map(s=>({...s})),countUnknownDialogue=arr=>(arr||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length,beforeUnknownCount=countUnknownDialogue(segs),preResolved=audiobookResolveUnknowns(segs,[],_audiobook.roster||[]);preResolved.length&&toast(`${preResolved.length} Unknown line${preResolved.length!==1?"s":""} resolved by grammar rules`,"success");const unknownIdxs=[],_abRepairSuspects=options.repairNarration?_abSuspectNarration(segs):null;for(let i=0;iac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang();let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel()),promptOverride=typeof options.prompt=="string"?options.prompt:null;!promptOverride&&options.repairNarration&&typeof window._abVerificationPrompt=="string"&&(promptOverride=window._abVerificationPrompt);const groups=audiobookRecastGroups(unknownIdxs,segs),view=audiobookCastView(unknownIdxs.length,llm_url,model);view.recountRoster(segs);const _rcAliasMap=new Map;try{const bookTitle=((_a2=window.readerState)==null?void 0:_a2.title)||"",records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(bookTitle):[];for(const rec of records||[])if(rec!=null&&rec.name&&(_rcAliasMap.set(rec.name.toLowerCase(),rec.name),typeof clSplitIdentityTokens=="function"))for(const a of clSplitIdentityTokens(rec.aliases,{aliases:!0}))_rcAliasMap.set(a.toLowerCase(),rec.name)}catch{}const canonicalizeSpeaker=name=>name&&_rcAliasMap.get(name.toLowerCase())||name;view.processing("Waking up LLM model (this may take a few minutes if cold-booting)\u2026");try{await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify({text:"Wake up.",known_characters:[],recent:"",language,llm_url:((_b2=document.getElementById("ab-cv-llm-url"))==null?void 0:_b2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_c2=document.getElementById("ab-cv-llm-select"))==null?void 0:_c2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_WARMUP_TIMEOUT_MS)})},AUDIOBOOK_WARMUP_TIMEOUT_MS+5e3)}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0,_audiobook.running=!1,_audiobook.abort=null,view.done({stopped:!0,message:"Character definition stopped before any lines were updated."}),toast("Character definition stopped. Existing cast preserved.","info");return}}let done=0,prevIdx=-2,groupsSinceSave=0;const pendingReplacements=new Map,normalizeRecastSegment=(seg,fallback)=>{const type=(seg==null?void 0:seg.type)==="narration"?"narration":"dialogue";return{speaker:type==="narration"?"Narrator":canonicalizeSpeaker((seg==null?void 0:seg.speaker)||(fallback==null?void 0:fallback.speaker)||"Unknown"),type,emotion:type==="dialogue"&&((seg==null?void 0:seg.emotion)||(fallback==null?void 0:fallback.emotion))||"",text:(seg==null?void 0:seg.text)||(fallback==null?void 0:fallback.text)||""}};try{for(let group of groups){if(_audiobook.cancel)break;const unresolvedGroup=[];for(const idx of group)audiobookIsSpeechTagOnly((_d2=segs[idx])==null?void 0:_d2.text)?(segs[idx].type="narration",segs[idx].speaker="Narrator",segs[idx].emotion=""):unresolvedGroup.push(idx);if(!unresolvedGroup.length){group[0]!==prevIdx+1&&view.divider(prevIdx,group[0]),prevIdx=group[group.length-1],view.update(done,`Correcting narration tags ${done+1}-${done+group.length} / ${unknownIdxs.length}\u2026`);for(const idx of group)view.addSegments([segs[idx]]),done++;continue}group=unresolvedGroup,group[0]!==prevIdx+1&&view.divider(prevIdx,group[0]),prevIdx=group[group.length-1];const recastCtx=audiobookRecastContext(segs,group),passageText=recastCtx.text,lineLabel=group.length>1?`Attributing lines ${done+1}-${done+group.length} / ${unknownIdxs.length}\u2026`:`Attributing line ${done+1} / ${unknownIdxs.length}\u2026`;view.update(done,lineLabel),view.processing(passageText.trim());let data=null;const recastBody=audiobookAttributeBody({text:passageText.trim(),known_characters:_audiobook.roster.slice(-40),recent:recastCtx.recent||"",language,llm_url:((_e2=document.getElementById("ab-cv-llm-url"))==null?void 0:_e2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_f2=document.getElementById("ab-cv-llm-select"))==null?void 0:_f2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_RECAST_TIMEOUT_MS)},promptOverride);try{try{data=await audiobookAttributeStream(recastBody,view,ac.signal,AUDIOBOOK_RECAST_TIMEOUT_MS)}catch(streamErr){if(streamErr.name==="AbortError")throw streamErr;const r=await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify(recastBody)},AUDIOBOOK_RECAST_TIMEOUT_MS+5e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||e.error||r.statusText||"HTTP "+r.status)}data=await r.json()}}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0;break}view.note(`API Error while checking ${group.length>1?"a group of Unknown lines":"Unknown line"}: ${err.message||err}`);for(const idx of group)view.addSegments([segs[idx]]),done++;continue}if(data&&data.segments){const used=new Set;for(const idx of group){const targetSeg=segs[idx],seqMatch=audiobookFindReturnedSegmentSequence(targetSeg,data.segments,used);if(seqMatch&&seqMatch.segs.length>1){const repl=seqMatch.segs.map(s=>normalizeRecastSegment(s,targetSeg)).filter(s=>(s.text||"").trim()),hasResolved=repl.some(s=>s.type==="narration"||s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)),hasUnresolvedDialogue=repl.some(s=>s.type==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)));if(repl.length&&hasResolved&&!hasUnresolvedDialogue){seqMatch.idxs.forEach(i=>used.add(i)),pendingReplacements.set(idx,repl),repl.forEach(s=>{s.type==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)&&!_audiobook.roster.includes(s.speaker)&&_audiobook.roster.push(s.speaker)});continue}}const match=audiobookFindReturnedSegment(targetSeg,data.segments,used);if(match&&match.seg.speaker){const speaker=match.seg.type==="narration"?"Narrator":canonicalizeSpeaker(match.seg.speaker);if(!speaker||/^Unknown|Unbekannt/i.test(speaker))continue;used.add(match.idx),targetSeg.type=match.seg.type==="narration"?"narration":"dialogue",targetSeg.speaker=speaker,targetSeg.emotion=targetSeg.type==="dialogue"&&(match.seg.emotion||targetSeg.emotion)||"",targetSeg.type==="dialogue"&&!_audiobook.roster.includes(speaker)&&_audiobook.roster.push(speaker)}}}for(const idx of group)view.addSegments(pendingReplacements.get(idx)||[segs[idx]]),done++;groupsSinceSave++,groupsSinceSave>=10&&_audiobook.lastText&&(groupsSinceSave=0,_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0),view.recountRoster(segs))}view.update(_audiobook.cancel?done:unknownIdxs.length)}catch(e){e.name!=="AbortError"&&view.note("Error defining unknowns: "+e.message)}finally{_audiobook.running=!1,_audiobook.abort=null}try{pendingReplacements.size&&[...pendingReplacements.entries()].sort((a,b)=>b[0]-a[0]).forEach(([idx,repl])=>segs.splice(idx,1,...repl));const{segments:deduped,removed:dupRemoved}=_audiobookDedupNearbyDuplicates(segs);dupRemoved&&(segs.splice(0,segs.length,...deduped),view.note(`Removed ${dupRemoved} duplicated line${dupRemoved!==1?"s":""} introduced by this verification pass.`));const afterUnknownCount=countUnknownDialogue(segs);if(afterUnknownCount>beforeUnknownCount){segs.splice(0,segs.length,...originalSegments);const how=_audiobook.cancel?"Stopped run":"Quality run";view.note(`${how} rolled back: Unknown segments increased from ${beforeUnknownCount} to ${afterUnknownCount}. Existing cast preserved.`),toast(`${how} rolled back because it increased Unknown speakers.`,"error")}const _rcDone=_audiobook.completedChunks||segs.length,_rcTotal=_audiobook.completedTotal||_rcDone;if(_audiobook.cancel){_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_rcDone,_rcTotal),view.done({stopped:!0,message:"Character definition stopped. Existing cast preserved."}),toast("Character definition stopped. Existing cast preserved.","info");return}{const _ph=_audiobookDropPhantomSpeakers(segs);_ph.dropped&&console.info("[recast] dropped",_ph.dropped,"phantom speaker line(s):",_ph.names.join(", "))}{const _ident=_audiobookConsolidateSpeakerAliases(segs);_ident.merged&&(console.info("[recast] consolidated",_ident.merged,"speaker label variant(s),",_ident.moved,"line(s)"),view.note(`Merged ${_ident.merged} duplicate character label${_ident.merged!==1?"s":""} (${_ident.moved} lines).`))}_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_rcDone,_rcTotal);const speakers=new Set(segs.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${segs.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown)}catch(e){console.error("[audiobookRecastUnknown] post-processing failed",e),view.done({stopped:!0,message:"Finished checking speakers, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}const AB_AUTO_REPAIR_KEY="ab-auto-repair-enabled";function audiobookAutoRepairEnabled(){try{return localStorage.getItem(AB_AUTO_REPAIR_KEY)!=="0"}catch{return!0}}async function audiobookAutoRepairCast(threshold=10,maxRounds=4){if(!(_audiobook.segments||[]).length){toast("Nothing cast yet to repair","error");return}const countUnknown=()=>(_audiobook.segments||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length;let prev=countUnknown();if(prev<=threshold){toast(`Cast looks clean \u2014 ${prev} unknown speaker${prev!==1?"s":""}`,"success");return}toast(`Auto-repair: ${prev} unknown speakers \u2014 running repair passes\u2026`,"info");for(let round=1;round<=maxRounds;round++){if(await audiobookRecastUnknown(null,null,{repairNarration:!0}),_audiobook.cancel){toast("Auto-repair stopped.","info");return}const now=countUnknown();if(now<=threshold){toast(`Auto-repair done: ${now} unknown left after ${round} round${round!==1?"s":""}.`,"success");return}if(now>=prev)break;prev=now}toast(`2.4 Still ${prev} unknown \u2014 running full verification pass\u2026`,"info"),await audiobookRecastUnknown(null,null,{includeNarrator:!0});const afterVerify=countUnknown();if(typeof window._abRunConsistencyPass=="function"&&!_audiobook.cancel){toast("2.5 Checking voice consistency\u2026","info");try{await window._abRunConsistencyPass()}catch(e){console.warn("[auto-repair 2.5]",e)}}const final=countUnknown();toast(final<=threshold?`Auto-repair done: ${final} unknown left.`:`Auto-repair finished: ${final} unknown remain (target was under ${threshold}).`,final<=threshold?"success":"error")}window.audiobookAutoRepairCast=audiobookAutoRepairCast;async function audiobookRecastUntilThreshold(threshold=10,maxPasses=8){if(_audiobook.running){toast("Casting is already running","error");return}const segs=_audiobook.segments;if(!segs||!segs.length){toast("No cast to check","error");return}const countUnknown=()=>(segs||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length;let prev=countUnknown();if(prev<=threshold){toast(`Already at ${prev} unknown speakers (target: <${threshold})`,"success");return}toast(`Running recast + verify passes until under ${threshold} unknown speakers (currently ${prev})\u2026`,"info");for(let pass=1;pass<=maxPasses;pass++){if(await audiobookRecastUnknown(null,null,{repairNarration:!0}),_audiobook.cancel){toast("Stopped \u2014 cancelled mid-pass.","info");return}const now=countUnknown();if(now<=threshold){toast(`Done: ${now} unknown speakers remain (target reached in ${pass} pass${pass!==1?"es":""}).`,"success");return}if(now>=prev){toast(`Stopped after ${pass} pass${pass!==1?"es":""}: no further progress (${now} unknown remain, target was <${threshold}). Likely GPU/LLM contention or genuinely ambiguous lines.`,"error");return}prev=now}toast(`Stopped after ${maxPasses} passes: ${prev} unknown speakers remain (target was <${threshold}).`,"error")}window.audiobookRecastUntilThreshold=audiobookRecastUntilThreshold;async function audiobookOpenCastView(){var _a2;if(_audiobook.running)return;const text=audiobookScopeText();if(!text){toast("Import a document first","error");return}const _curBook=window.readerState&&readerState.savedId||null;_curBook&&_audiobook.bookId&&_audiobook.bookId!==_curBook&&(_audiobook.segments=null,_audiobook.lastText=null,_audiobook.roster=null),_audiobook.bookId=_curBook;const llm_url=audiobookLlmUrl(),model=audiobookLlmModel(),chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,AUDIOBOOK_CHUNK_CHARS):[text];if(_audiobook.segments&&_audiobook.segments.length>0&&_audiobook.lastText===text){const view=audiobookCastView(chunks.length,llm_url,model,!1);view.rebuild(_audiobook.segments);const speakers=new Set(_audiobook.segments.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${_audiobook.segments.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown);return}const _applyDraft=(draft,view,source)=>{const draftDone=Number.isFinite(Number(draft.done))?Number(draft.done):0,draftTotal=Number.isFinite(Number(draft.total))?Number(draft.total):0;_abStampSegmentPages(draft.segments,draft.pageMarks,text),_audiobook.segments=draft.segments,_audiobook.roster=draft.roster||[],_audiobook.lastText=text,_audiobook.pageMarks=draft.pageMarks||[],_audiobook.rehId=draft.rehId||null,_audiobook.completedChunks=draftDone>0?draftDone:0,_audiobook.completedTotal=draftTotal>0?draftTotal:0,view.rebuild(draft.segments);const ageMs=Date.now()-(draft.savedAt||0),ageMins=Math.round(ageMs/6e4),ageStr=ageMins<1?"gerade eben":ageMins<60?`vor ${ageMins} Min.`:`vor ${Math.round(ageMins/60)} Std.`,pct=draftTotal>0?Math.max(0,Math.min(100,Math.round(draftDone/draftTotal*100))):100,wasDone=draftTotal>0&&draftDone>=draftTotal,canContinue=draftDone>0&&draftTotal>0&&draftDones.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} Charakter${speakers.size!==1?"e":""} \xB7 ${draft.segments.length} Segmente`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown)},_localDraft=_abLoadDraft(text);if(_localDraft&&_localDraft.segments&&_localDraft.segments.length>0){const view=audiobookCastView(chunks.length,llm_url,model,!1),bookId2=_abBookId();let chosen=_localDraft,source="local",serverDraft=null;if(bookId2){try{serverDraft=await _abLoadDraftServer(bookId2)}catch{}if(serverDraft&&(serverDraft.savedAt||0)>(_localDraft.savedAt||0)){chosen=serverDraft,source="server";try{localStorage.setItem(_abDraftKey(bookId2),JSON.stringify(serverDraft))}catch{}}}_applyDraft(chosen,view,source),bookId2&&source==="local"&&(!serverDraft||(_localDraft.savedAt||0)>(serverDraft.savedAt||0))&&fetch(`/api/reader/docs/${encodeURIComponent(bookId2)}/scripts/cast`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({..._localDraft,bookId:bookId2,title:((_a2=window.readerState)==null?void 0:_a2.title)||_localDraft.title||""})}).catch(()=>{});return}const bookId=_abBookId();if(bookId){const view=audiobookCastView(chunks.length,llm_url,model,!1);view.loadingRestore();try{const serverDraft=await _abLoadDraftServer(bookId);if(serverDraft&&serverDraft.segments&&serverDraft.segments.length>0){try{localStorage.setItem(_abDraftKey(bookId),JSON.stringify(serverDraft))}catch{}_applyDraft(serverDraft,view,"server")}else view.setFreshState("No saved cast was found for this saved book. Starting a new cast will create a new draft.")}catch{view.setFreshState("Saved cast could not be checked. Browser autosave was already searched; starting a new cast will create a new draft.")}return}audiobookCastView(chunks.length,llm_url,model,!0)}async function audiobookCast(overrideUrl,overrideModel,resume){var _a2,_b2;if(_audiobook.running)return;const text=audiobookScopeText();if(!text){toast("Import a document first","error");return}if(typeof parseScript!="function"){toast("Rehearser not loaded yet \u2014 try again in a moment","error");return}_audiobook.running=!0,!!_abBookId()||!await _abEnsureLibraryBook()&&typeof toast=="function"&&toast("Autosave will use this browser only until the book is saved to the library.","info");const chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,AUDIOBOOK_CHUNK_CHARS):[text],startIndex=resume&&resume.startIndex>0&&resume.startIndex0;_audiobook.cancel=!1,isResume||_abClearDraft(),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!0);const ac=new AbortController;_audiobook.abort=()=>ac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang(text);let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel());const view=audiobookCastView(chunks.length,llm_url,model,!1);isResume&&resume.segments&&resume.segments.length&&view.rebuild(resume.segments),view.processing("Waking up LLM model (this may take a few minutes if cold-booting)\u2026");try{await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify({text:"Wake up.",known_characters:[],recent:"",language,llm_url:((_a2=document.getElementById("ab-cv-llm-url"))==null?void 0:_a2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_b2=document.getElementById("ab-cv-llm-select"))==null?void 0:_b2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_WARMUP_TIMEOUT_MS)})},AUDIOBOOK_WARMUP_TIMEOUT_MS+5e3)}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0,_audiobook.running=!1,_audiobook.abort=null,view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}}const allSegments2=isResume?resume.segments.slice():[];_audiobook.liveSegments=allSegments2;const roster=isResume?(resume.roster||[]).slice():[];let narrationOnly=isResume&&resume.narrationOnly||0,degraded=isResume&&resume.degraded||0,completedChunks=startIndex;_abStartDraftAutosave(()=>{allSegments2.length&&_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)});const _pgMarks=(_audiobook.pageMarks||[]).slice();_pgMarks.length&&_pgMarks[0].offset<=2&&_pgMarks.shift();let _pgMarkIdx=0,_pgCharPos=0,_curPageNum=1;if(isResume){for(let k=0;k0&&(_curPageNum=_pgMarks[_pgMarkIdx-1].page+1)}try{for(let i=startIndex;i=_pgMarks[_pgMarkIdx].offset;)_curPageNum=_pgMarks[_pgMarkIdx].page+1,view.pagemark(_curPageNum),_pgMarkIdx++;if(_pgCharPos+=chunks[i].length+1,!audiobookHasDialogue(chunks[i])){const seg={speaker:"Narrator",type:"narration",text:chunks[i],emotion:"",page:_curPageNum};allSegments2.push(seg),narrationOnly++,view.addSegments([seg]),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length);continue}const recent=allSegments2.filter(s=>s.type==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)).slice(-6).map(s=>`${s.speaker}: ${(s.text||"").slice(0,80)}`).join(` +${preview}`,{title:"Clean up character library?",okLabel:`Remove ${toDelete.length}`,danger:!0}))return;let removed=0;for(const r of toDelete)try{await clDelete(r.id),removed++}catch{}toast(`Removed ${removed} character record${removed!==1?"s":""} not in the current roster`,"success")},bookTitle=((_a3=window.readerState)==null?void 0:_a3.title)||"";let existing=[];(async()=>{if(!(!bookTitle||typeof clGetAllByTagOrBook!="function"))try{existing=await clGetAllByTagOrBook(bookTitle)}catch{}})();const identifyMenu=foot.querySelector("#ab-cv-menu-identify"),castMenu=foot.querySelector("#ab-cv-menu-cast"),viewCastMenu=foot.querySelector("#ab-cv-menu-viewcast");identifyMenu==null||identifyMenu.addEventListener("click",()=>_abToggleFootMenu(identifyMenu,[{icon:"mdi-refresh",label:"1. Identify all characters",title:"Scan the text and build the cast list from scratch",onClick:runIdentifyAll,danger:!0},{icon:"mdi-auto-fix",label:"2. Auto-repair cast (2.1\u20132.5)",title:"Runs the repair sequence automatically: fix mislabeled dialogue, resolve unknown speakers, repeat while it helps, then a full verification only if still needed",onClick:()=>audiobookAutoRepairCast()},{icon:"mdi-comment-alert-outline",label:"2.1 Repair mislabeled dialogue",title:"Finds spoken lines that were typed as narration (usually because the \xBB \xAB quote marks were lost in extraction) and re-checks only those \u2014 far cheaper than verifying every narration line",onClick:runRepairNarration},{icon:"mdi-account-question-outline",label:"2.2 Identify unknown characters",title:"Re-scan only the unknown segments with the current prompt",onClick:runIdentifyUnknown},{icon:"mdi-repeat",label:"2.3 Run until < N unknown\u2026",title:"Repeats recast-unknown + narrator-verify passes automatically until the Unknown-speaker count drops below a target, or progress stalls",onClick:()=>{const input=window.prompt("Stop once fewer than this many Unknown speakers remain:","10");if(input==null)return;const threshold=parseInt(input,10);if(!Number.isFinite(threshold)||threshold<0){toast("Enter a whole number of 0 or more","error");return}audiobookRecastUntilThreshold(threshold)}},{icon:"mdi-shield-check-outline",label:"2.4 Verify all characters",title:"Second-pass plausibility check that keeps the existing cast and only corrects uncertain matches",onClick:runVerificationPass},{icon:"mdi-account-search-outline",label:"2.5 Check voice consistency",title:"Third-pass check: gathers every line already credited to each character across the whole book and flags any that don\u2019t match their established voice",onClick:runConsistencyPass}])),window._abRunConsistencyPass=runConsistencyPass,castMenu==null||castMenu.addEventListener("click",()=>_abToggleFootMenu(castMenu,[{icon:"mdi-account-multiple-plus-outline",label:"Cast all character roles",title:"Generate / refresh the character sheets for every cast character",onClick:runCastAll},{icon:"mdi-account-arrow-right-outline",label:"Continue uncasted characters",title:"Only generate profiles for characters with no detail yet \u2014 skips anyone already fully cast",onClick:runCastContinueUncasted},{icon:"mdi-account-check-outline",label:"Select characters to cast or recast roles",title:existing.length?"Choose character roles to generate or refresh":"No character sheets found yet for this book",disabled:!existing.length,onClick:runCastSelected},{divider:!0},{icon:"mdi-refresh",label:"New recast (discard & rebuild all)",title:"Discard every generated character sheet and rebuild every profile from scratch",onClick:runCastFresh,danger:!0}])),viewCastMenu==null||viewCastMenu.addEventListener("click",()=>_abToggleFootMenu(viewCastMenu,[{icon:"mdi-eye-outline",label:"View cast",title:"View the cast overview in the library",onClick:runViewCast},{icon:"mdi-folder-zip-outline",label:"Export cast archive (.zip)",title:"Download one zip: the cast as a readable Markdown script plus a Markdown sheet per character",onClick:audiobookExportCastMd},{divider:!0},{icon:"mdi-broom",label:"Clean up library to current roster",title:"Remove saved character records that aren't in the current roster \u2014 old spelling-variant duplicates and superseded names from past casting runs",onClick:runLibraryCleanup,danger:!0}])),(_b3=foot.querySelector("#ab-cv-continue"))==null||_b3.addEventListener("click",()=>applyPromptAndRun2((u,m)=>audiobookCast(u,m,{startIndex:_audiobook.completedChunks,segments:_audiobook.segments,roster:_audiobook.roster,narrationOnly:_audiobook.narratedPassages,degraded:_audiobook.degraded}))),panel.classList.add("ab-castpanel-done"),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1)},done(options={}){var _a3,_b3,_c3,_d3,_e3;if(_abHideCastingControl(),!(options.stopped||_audiobook.cancel)){closePanel();return}this.clearProcessing(),count&&(count.hidden=!0);const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!0),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove();const message=options.message||"Casting stopped before any passage completed.";if(feed.querySelector(".ab-cv-row, .ab-cv-note, .ab-cv-divider, .ab-cv-page")){if(options.message){const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}}else{feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}chars.querySelector(".ab-char-item")||(chars.innerHTML='No completed characters yet.');const foot=panel.querySelector("#ab-cv-foot"),hasSegments=Array.isArray(_audiobook.segments)&&_audiobook.segments.length>0;foot.hidden=!1,foot.innerHTML=` ${escHtml(message)}${hasSegments?'':""}`,(_c3=foot.querySelector("#ab-cv-stopped-back"))==null||_c3.addEventListener("click",closePanel),(_d3=foot.querySelector("#ab-cv-stopped-review"))==null||_d3.addEventListener("click",async()=>{closePanel(),await audiobookOpenCurrentInRehearser()}),(_e3=foot.querySelector("#ab-cv-stopped-recast"))==null||_e3.addEventListener("click",()=>{var _a4;const newPrompt=(_a4=panel.querySelector("#ab-cv-prompt-text"))==null?void 0:_a4.value,currentChoice=audiobookCurrentCastLlm(panel),choice=audiobookSaveLlmChoice(currentChoice.url,currentChoice.model);typeof _appSettings!="undefined"&&newPrompt&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt||""})}).finally(()=>{closePanel(),audiobookCast(choice.url,choice.model)})}),panel.classList.add("ab-castpanel-done"),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1)},loadingRestore(){var _a3,_b3;(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent="Loading saved cast\u2026",feed.appendChild(r),chars.innerHTML='restoring\u2026';const statusMsg=panel.querySelector("#ab-cv-status-msg");statusMsg&&(statusMsg.style.display="inline-block",statusMsg.textContent="Checking saved cast before starting a new one.");const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn&&(castBtn.style.display="none")},setFreshState(message="Ready to identify characters."){var _a3,_b3;if((_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),!feed.querySelector(".ab-cv-row, .ab-cv-divider, .ab-cv-page")){feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}chars.querySelector(".ab-char-item")||(chars.innerHTML='No saved cast found.');const statusMsg=panel.querySelector("#ab-cv-status-msg");statusMsg&&(statusMsg.style.display="inline-block",statusMsg.textContent=message);const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn&&(castBtn.style.display="inline-block",castBtn.addEventListener("click",()=>{var _a4;const newPrompt=(_a4=panel.querySelector("#ab-cv-prompt-text"))==null?void 0:_a4.value,currentChoice=audiobookCurrentCastLlm(panel),choice=audiobookSaveLlmChoice(currentChoice.url,currentChoice.model);typeof _appSettings!="undefined"&&newPrompt&&(_appSettings.audiobook_prompt=newPrompt),audiobookCast(choice.url,choice.model)}))}}}async function audiobookRecastUnknown(overrideUrl,overrideModel,options={}){var _a2,_b2,_c2,_d2,_e2,_f2;if(_audiobook.running)return;const segs=_audiobook.segments;if(!segs||!segs.length)return;const originalSegments=segs.map(s=>({...s})),countUnknownDialogue=arr=>(arr||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length,beforeUnknownCount=countUnknownDialogue(segs),preResolved=audiobookResolveUnknowns(segs,[],_audiobook.roster||[]);preResolved.length&&toast(`${preResolved.length} Unknown line${preResolved.length!==1?"s":""} resolved by grammar rules`,"success");const unknownIdxs=[],_abRepairSuspects=options.repairNarration?_abSuspectNarration(segs):null;for(let i=0;iac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang();let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel()),promptOverride=typeof options.prompt=="string"?options.prompt:null;!promptOverride&&options.repairNarration&&typeof window._abVerificationPrompt=="string"&&(promptOverride=window._abVerificationPrompt);const groups=audiobookRecastGroups(unknownIdxs,segs),view=audiobookCastView(unknownIdxs.length,llm_url,model);view.recountRoster(segs);const _rcAliasMap=new Map;try{const bookTitle=((_a2=window.readerState)==null?void 0:_a2.title)||"",records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(bookTitle):[];for(const rec of records||[])if(rec!=null&&rec.name&&(_rcAliasMap.set(rec.name.toLowerCase(),rec.name),typeof clSplitIdentityTokens=="function"))for(const a of clSplitIdentityTokens(rec.aliases,{aliases:!0}))_rcAliasMap.set(a.toLowerCase(),rec.name)}catch{}const canonicalizeSpeaker=name=>name&&_rcAliasMap.get(name.toLowerCase())||name;view.processing("Waking up LLM model (this may take a few minutes if cold-booting)\u2026");try{await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify({text:"Wake up.",known_characters:[],recent:"",language,llm_url:((_b2=document.getElementById("ab-cv-llm-url"))==null?void 0:_b2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_c2=document.getElementById("ab-cv-llm-select"))==null?void 0:_c2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_WARMUP_TIMEOUT_MS)})},AUDIOBOOK_WARMUP_TIMEOUT_MS+5e3)}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0,_audiobook.running=!1,_audiobook.abort=null,view.done({stopped:!0,message:"Character definition stopped before any lines were updated."}),toast("Character definition stopped. Existing cast preserved.","info");return}}let done=0,prevIdx=-2,groupsSinceSave=0;const pendingReplacements=new Map,normalizeRecastSegment=(seg,fallback)=>{const type=(seg==null?void 0:seg.type)==="narration"?"narration":"dialogue";return{speaker:type==="narration"?"Narrator":canonicalizeSpeaker((seg==null?void 0:seg.speaker)||(fallback==null?void 0:fallback.speaker)||"Unknown"),type,emotion:type==="dialogue"&&((seg==null?void 0:seg.emotion)||(fallback==null?void 0:fallback.emotion))||"",text:(seg==null?void 0:seg.text)||(fallback==null?void 0:fallback.text)||""}};try{for(let group of groups){if(_audiobook.cancel)break;const unresolvedGroup=[];for(const idx of group)audiobookIsSpeechTagOnly((_d2=segs[idx])==null?void 0:_d2.text)?(segs[idx].type="narration",segs[idx].speaker="Narrator",segs[idx].emotion=""):unresolvedGroup.push(idx);if(!unresolvedGroup.length){group[0]!==prevIdx+1&&view.divider(prevIdx,group[0]),prevIdx=group[group.length-1],view.update(done,`Correcting narration tags ${done+1}-${done+group.length} / ${unknownIdxs.length}\u2026`);for(const idx of group)view.addSegments([segs[idx]]),done++;continue}group=unresolvedGroup,group[0]!==prevIdx+1&&view.divider(prevIdx,group[0]),prevIdx=group[group.length-1];const recastCtx=audiobookRecastContext(segs,group),passageText=recastCtx.text,lineLabel=group.length>1?`Attributing lines ${done+1}-${done+group.length} / ${unknownIdxs.length}\u2026`:`Attributing line ${done+1} / ${unknownIdxs.length}\u2026`;view.update(done,lineLabel),view.processing(passageText.trim());let data=null;const recastBody=audiobookAttributeBody({text:passageText.trim(),known_characters:_audiobook.roster.slice(-40),recent:recastCtx.recent||"",language,llm_url:((_e2=document.getElementById("ab-cv-llm-url"))==null?void 0:_e2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_f2=document.getElementById("ab-cv-llm-select"))==null?void 0:_f2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_RECAST_TIMEOUT_MS)},promptOverride);try{try{data=await audiobookAttributeStream(recastBody,view,ac.signal,AUDIOBOOK_RECAST_TIMEOUT_MS)}catch(streamErr){if(streamErr.name==="AbortError")throw streamErr;const r=await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify(recastBody)},AUDIOBOOK_RECAST_TIMEOUT_MS+5e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||e.error||r.statusText||"HTTP "+r.status)}data=await r.json()}}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0;break}view.note(`API Error while checking ${group.length>1?"a group of Unknown lines":"Unknown line"}: ${err.message||err}`);for(const idx of group)view.addSegments([segs[idx]]),done++;continue}if(data&&data.segments){const used=new Set;for(const idx of group){const targetSeg=segs[idx],seqMatch=audiobookFindReturnedSegmentSequence(targetSeg,data.segments,used);if(seqMatch&&seqMatch.segs.length>1){const repl=seqMatch.segs.map(s=>normalizeRecastSegment(s,targetSeg)).filter(s=>(s.text||"").trim()),hasResolved=repl.some(s=>s.type==="narration"||s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)),hasUnresolvedDialogue=repl.some(s=>s.type==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)));if(repl.length&&hasResolved&&!hasUnresolvedDialogue){seqMatch.idxs.forEach(i=>used.add(i)),pendingReplacements.set(idx,repl),repl.forEach(s=>{s.type==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)&&!_audiobook.roster.includes(s.speaker)&&_audiobook.roster.push(s.speaker)});continue}}const match=audiobookFindReturnedSegment(targetSeg,data.segments,used);if(match&&match.seg.speaker){const speaker=match.seg.type==="narration"?"Narrator":canonicalizeSpeaker(match.seg.speaker);if(!speaker||/^Unknown|Unbekannt/i.test(speaker))continue;used.add(match.idx),targetSeg.type=match.seg.type==="narration"?"narration":"dialogue",targetSeg.speaker=speaker,targetSeg.emotion=targetSeg.type==="dialogue"&&(match.seg.emotion||targetSeg.emotion)||"",targetSeg.type==="dialogue"&&!_audiobook.roster.includes(speaker)&&_audiobook.roster.push(speaker)}}}for(const idx of group)view.addSegments(pendingReplacements.get(idx)||[segs[idx]]),done++;groupsSinceSave++,groupsSinceSave>=10&&_audiobook.lastText&&(groupsSinceSave=0,_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0),view.recountRoster(segs))}view.update(_audiobook.cancel?done:unknownIdxs.length)}catch(e){e.name!=="AbortError"&&view.note("Error defining unknowns: "+e.message)}finally{_audiobook.running=!1,_audiobook.abort=null,_abHideCastingControl()}try{pendingReplacements.size&&[...pendingReplacements.entries()].sort((a,b)=>b[0]-a[0]).forEach(([idx,repl])=>segs.splice(idx,1,...repl));const{segments:deduped,removed:dupRemoved}=_audiobookDedupNearbyDuplicates(segs);dupRemoved&&(segs.splice(0,segs.length,...deduped),view.note(`Removed ${dupRemoved} duplicated line${dupRemoved!==1?"s":""} introduced by this verification pass.`));const afterUnknownCount=countUnknownDialogue(segs);if(afterUnknownCount>beforeUnknownCount){segs.splice(0,segs.length,...originalSegments);const how=_audiobook.cancel?"Stopped run":"Quality run";view.note(`${how} rolled back: Unknown segments increased from ${beforeUnknownCount} to ${afterUnknownCount}. Existing cast preserved.`),toast(`${how} rolled back because it increased Unknown speakers.`,"error")}const _rcDone=_audiobook.completedChunks||segs.length,_rcTotal=_audiobook.completedTotal||_rcDone;if(_audiobook.cancel){_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_rcDone,_rcTotal),view.done({stopped:!0,message:"Character definition stopped. Existing cast preserved."}),toast("Character definition stopped. Existing cast preserved.","info");return}{const _ph=_audiobookDropPhantomSpeakers(segs);_ph.dropped&&console.info("[recast] dropped",_ph.dropped,"phantom speaker line(s):",_ph.names.join(", "))}{const _ident=_audiobookConsolidateSpeakerAliases(segs);_ident.merged&&(console.info("[recast] consolidated",_ident.merged,"speaker label variant(s),",_ident.moved,"line(s)"),view.note(`Merged ${_ident.merged} duplicate character label${_ident.merged!==1?"s":""} (${_ident.moved} lines).`))}_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_rcDone,_rcTotal);const speakers=new Set(segs.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${segs.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown)}catch(e){console.error("[audiobookRecastUnknown] post-processing failed",e),view.done({stopped:!0,message:"Finished checking speakers, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}const AB_AUTO_REPAIR_KEY="ab-auto-repair-enabled";function audiobookAutoRepairEnabled(){try{return localStorage.getItem(AB_AUTO_REPAIR_KEY)!=="0"}catch{return!0}}async function audiobookAutoRepairCast(threshold=10,maxRounds=4){if(!(_audiobook.segments||[]).length){toast("Nothing cast yet to repair","error");return}const countUnknown=()=>(_audiobook.segments||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length;let prev=countUnknown();if(prev<=threshold){toast(`Cast looks clean \u2014 ${prev} unknown speaker${prev!==1?"s":""}`,"success");return}toast(`Auto-repair: ${prev} unknown speakers \u2014 running repair passes\u2026`,"info");for(let round=1;round<=maxRounds;round++){if(await audiobookRecastUnknown(null,null,{repairNarration:!0}),_audiobook.cancel){toast("Auto-repair stopped.","info");return}const now=countUnknown();if(now<=threshold){toast(`Auto-repair done: ${now} unknown left after ${round} round${round!==1?"s":""}.`,"success");return}if(now>=prev)break;prev=now}toast(`2.4 Still ${prev} unknown \u2014 running full verification pass\u2026`,"info"),await audiobookRecastUnknown(null,null,{includeNarrator:!0});const afterVerify=countUnknown();if(typeof window._abRunConsistencyPass=="function"&&!_audiobook.cancel){toast("2.5 Checking voice consistency\u2026","info");try{await window._abRunConsistencyPass()}catch(e){console.warn("[auto-repair 2.5]",e)}}const final=countUnknown();toast(final<=threshold?`Auto-repair done: ${final} unknown left.`:`Auto-repair finished: ${final} unknown remain (target was under ${threshold}).`,final<=threshold?"success":"error")}window.audiobookAutoRepairCast=audiobookAutoRepairCast;async function audiobookRecastUntilThreshold(threshold=10,maxPasses=8){if(_audiobook.running){toast("Casting is already running","error");return}const segs=_audiobook.segments;if(!segs||!segs.length){toast("No cast to check","error");return}const countUnknown=()=>(segs||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length;let prev=countUnknown();if(prev<=threshold){toast(`Already at ${prev} unknown speakers (target: <${threshold})`,"success");return}toast(`Running recast + verify passes until under ${threshold} unknown speakers (currently ${prev})\u2026`,"info");for(let pass=1;pass<=maxPasses;pass++){if(await audiobookRecastUnknown(null,null,{repairNarration:!0}),_audiobook.cancel){toast("Stopped \u2014 cancelled mid-pass.","info");return}const now=countUnknown();if(now<=threshold){toast(`Done: ${now} unknown speakers remain (target reached in ${pass} pass${pass!==1?"es":""}).`,"success");return}if(now>=prev){toast(`Stopped after ${pass} pass${pass!==1?"es":""}: no further progress (${now} unknown remain, target was <${threshold}). Likely GPU/LLM contention or genuinely ambiguous lines.`,"error");return}prev=now}toast(`Stopped after ${maxPasses} passes: ${prev} unknown speakers remain (target was <${threshold}).`,"error")}window.audiobookRecastUntilThreshold=audiobookRecastUntilThreshold;async function audiobookOpenCastView(){var _a2;if(_audiobook.running)return;const text=audiobookScopeText();if(!text){toast("Import a document first","error");return}const _curBook=window.readerState&&readerState.savedId||null;_curBook&&_audiobook.bookId&&_audiobook.bookId!==_curBook&&(_audiobook.segments=null,_audiobook.lastText=null,_audiobook.roster=null),_audiobook.bookId=_curBook;const llm_url=audiobookLlmUrl(),model=audiobookLlmModel(),chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,AUDIOBOOK_CHUNK_CHARS):[text];if(_audiobook.segments&&_audiobook.segments.length>0&&_audiobook.lastText===text){const view=audiobookCastView(chunks.length,llm_url,model,!1);view.rebuild(_audiobook.segments);const speakers=new Set(_audiobook.segments.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${_audiobook.segments.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown);return}const _applyDraft=(draft,view,source)=>{const draftDone=Number.isFinite(Number(draft.done))?Number(draft.done):0,draftTotal=Number.isFinite(Number(draft.total))?Number(draft.total):0;_abStampSegmentPages(draft.segments,draft.pageMarks,text),_audiobook.segments=draft.segments,_audiobook.roster=draft.roster||[],_audiobook.lastText=text,_audiobook.pageMarks=draft.pageMarks||[],_audiobook.rehId=draft.rehId||null,_audiobook.completedChunks=draftDone>0?draftDone:0,_audiobook.completedTotal=draftTotal>0?draftTotal:0,view.rebuild(draft.segments);const ageMs=Date.now()-(draft.savedAt||0),ageMins=Math.round(ageMs/6e4),ageStr=ageMins<1?"gerade eben":ageMins<60?`vor ${ageMins} Min.`:`vor ${Math.round(ageMins/60)} Std.`,pct=draftTotal>0?Math.max(0,Math.min(100,Math.round(draftDone/draftTotal*100))):100,wasDone=draftTotal>0&&draftDone>=draftTotal,canContinue=draftDone>0&&draftTotal>0&&draftDones.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} Charakter${speakers.size!==1?"e":""} \xB7 ${draft.segments.length} Segmente`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown)},_localDraft=_abLoadDraft(text);if(_localDraft&&_localDraft.segments&&_localDraft.segments.length>0){const view=audiobookCastView(chunks.length,llm_url,model,!1),bookId2=_abBookId();let chosen=_localDraft,source="local",serverDraft=null;if(bookId2){try{serverDraft=await _abLoadDraftServer(bookId2)}catch{}if(serverDraft&&(serverDraft.savedAt||0)>(_localDraft.savedAt||0)){chosen=serverDraft,source="server";try{localStorage.setItem(_abDraftKey(bookId2),JSON.stringify(serverDraft))}catch{}}}_applyDraft(chosen,view,source),bookId2&&source==="local"&&(!serverDraft||(_localDraft.savedAt||0)>(serverDraft.savedAt||0))&&fetch(`/api/reader/docs/${encodeURIComponent(bookId2)}/scripts/cast`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({..._localDraft,bookId:bookId2,title:((_a2=window.readerState)==null?void 0:_a2.title)||_localDraft.title||""})}).catch(()=>{});return}const bookId=_abBookId();if(bookId){const view=audiobookCastView(chunks.length,llm_url,model,!1);view.loadingRestore();try{const serverDraft=await _abLoadDraftServer(bookId);if(serverDraft&&serverDraft.segments&&serverDraft.segments.length>0){try{localStorage.setItem(_abDraftKey(bookId),JSON.stringify(serverDraft))}catch{}_applyDraft(serverDraft,view,"server")}else view.setFreshState("No saved cast was found for this saved book. Starting a new cast will create a new draft.")}catch{view.setFreshState("Saved cast could not be checked. Browser autosave was already searched; starting a new cast will create a new draft.")}return}audiobookCastView(chunks.length,llm_url,model,!0)}async function audiobookCast(overrideUrl,overrideModel,resume){var _a2,_b2;if(_audiobook.running)return;const text=audiobookScopeText();if(!text){toast("Import a document first","error");return}if(typeof parseScript!="function"){toast("Rehearser not loaded yet \u2014 try again in a moment","error");return}_audiobook.running=!0,!!_abBookId()||!await _abEnsureLibraryBook()&&typeof toast=="function"&&toast("Autosave will use this browser only until the book is saved to the library.","info");const chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,AUDIOBOOK_CHUNK_CHARS):[text],startIndex=resume&&resume.startIndex>0&&resume.startIndex0;_audiobook.cancel=!1,isResume||_abClearDraft(),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!0);const ac=new AbortController;_audiobook.abort=()=>ac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang(text);let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel());const view=audiobookCastView(chunks.length,llm_url,model,!1);isResume&&resume.segments&&resume.segments.length&&view.rebuild(resume.segments),view.processing("Waking up LLM model (this may take a few minutes if cold-booting)\u2026");try{await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify({text:"Wake up.",known_characters:[],recent:"",language,llm_url:((_a2=document.getElementById("ab-cv-llm-url"))==null?void 0:_a2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_b2=document.getElementById("ab-cv-llm-select"))==null?void 0:_b2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_WARMUP_TIMEOUT_MS)})},AUDIOBOOK_WARMUP_TIMEOUT_MS+5e3)}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0,_audiobook.running=!1,_audiobook.abort=null,view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}}const allSegments2=isResume?resume.segments.slice():[];_audiobook.liveSegments=allSegments2;const roster=isResume?(resume.roster||[]).slice():[];let narrationOnly=isResume&&resume.narrationOnly||0,degraded=isResume&&resume.degraded||0,completedChunks=startIndex;_abStartDraftAutosave(()=>{allSegments2.length&&_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)});const _pgMarks=(_audiobook.pageMarks||[]).slice();_pgMarks.length&&_pgMarks[0].offset<=2&&_pgMarks.shift();let _pgMarkIdx=0,_pgCharPos=0,_curPageNum=1;if(isResume){for(let k=0;k0&&(_curPageNum=_pgMarks[_pgMarkIdx-1].page+1)}try{for(let i=startIndex;i=_pgMarks[_pgMarkIdx].offset;)_curPageNum=_pgMarks[_pgMarkIdx].page+1,view.pagemark(_curPageNum),_pgMarkIdx++;if(_pgCharPos+=chunks[i].length+1,!audiobookHasDialogue(chunks[i])){const seg={speaker:"Narrator",type:"narration",text:chunks[i],emotion:"",page:_curPageNum};allSegments2.push(seg),narrationOnly++,view.addSegments([seg]),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length);continue}const recent=allSegments2.filter(s=>s.type==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)).slice(-6).map(s=>`${s.speaker}: ${(s.text||"").slice(0,80)}`).join(` `);view.processing(chunks[i]);const attributeChunk=async(chunkText,recentCtx,timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS)=>{var _a3,_b3;const body={text:chunkText,known_characters:roster.slice(-40),recent:recentCtx,language,llm_url:((_a3=document.getElementById("ab-cv-llm-url"))==null?void 0:_a3.value.trim())||llm_url,model:audiobookSafeLlmModel(((_b3=document.getElementById("ab-cv-llm-select"))==null?void 0:_b3.value)||model),timeout_seconds:audiobookTimeoutSeconds(timeoutMs)};try{const d=await audiobookAttributeStream(body,view,ac.signal,timeoutMs);return Array.isArray(d.segments)?d.segments:null}catch(err){if(err.name==="AbortError")throw err}try{const r=await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify(body)},timeoutMs+5e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||e.error||r.statusText||"HTTP "+r.status)}const d=await r.json();return Array.isArray(d.segments)?d.segments:null}catch(err){if(err.name==="AbortError")throw err;return{error:err.message}}};let segs;try{let result=await attributeChunk(chunks[i],recent),data=null;if(result&&!result.error)data=result;else if(result&&result.error){view.note(`\u26A0\uFE0F Passage ${i+1} timed out \u2014 retrying in two halves\u2026`);const half=Math.floor(chunks[i].length/2),splitAt=chunks[i].lastIndexOf(" ",half)||half,chunkA=chunks[i].slice(0,splitAt).trim(),chunkB=chunks[i].slice(splitAt).trim(),resA=await attributeChunk(chunkA,recent,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS),resB=await attributeChunk(chunkB,recent,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS),segsA=Array.isArray(resA)?resA:null,segsB=Array.isArray(resB)?resB:null;if(segsA||segsB){const arrA=segsA||audiobookSplitByQuotes(chunkA),arrB=segsB||audiobookSplitByQuotes(chunkB);data=[...arrA,...arrB],segsA||(degraded++,view.note(`\u26A0\uFE0F Passage ${i+1} (first half) \u2014 auto-detected (retry also failed)`)),segsB||(degraded++,view.note(`\u26A0\uFE0F Passage ${i+1} (second half) \u2014 auto-detected (retry also failed)`))}else view.note(`\u274C Passage ${i+1} \u2014 LLM error: ${result.error}`),data=null}segs=Array.isArray(data)?data:data&&Array.isArray(data.segments)?data.segments:[];const hasDialogue=segs.some(s=>s.type==="dialogue");if(!segs.length||!hasDialogue&&audiobookHasDialogue(chunks[i])){segs=audiobookSplitByQuotes(chunks[i]),degraded++;const named=segs.filter(s=>s.type==="dialogue"&&!/^Unknown|Unbekannt/i.test(s.speaker)).length;view.note(`Passage ${i+1} \u2014 auto-detected dialogue${named?` (${named} speaker${named!==1?"s":""} from tags)`:" (set speakers in review)"}`)}else{let cleanedSegs=[];for(let s of segs){if(s.text=s.text||"",s.type==="dialogue"&&audiobookIsSpeechTagOnly(s.text)&&(s.type="narration",s.speaker="Narrator",s.emotion=""),s.type==="dialogue"&&s.text.trim().length>10){const tText=s.text.trim(),idx=chunks[i].indexOf(tText);if(idx!==-1){const surround=chunks[i].slice(Math.max(0,idx-8),idx)+chunks[i].slice(idx+tText.length,idx+tText.length+8);/[«»„“”"‟‚‘’›‹『「—–]/.test(surround)||(s.type="narration",s.speaker="Narrator",s.emotion="")}}if(cleanedSegs.length>0){let last=cleanedSegs[cleanedSegs.length-1];if(s.text.trim()===last.text.trim())if(s.type==="dialogue"&&last.type!=="dialogue"){cleanedSegs[cleanedSegs.length-1]=s;continue}else{if(s.type!=="dialogue"&&last.type==="dialogue")continue;continue}if(s.type==="dialogue"&&last.type==="narration"){const tS=s.text.trim(),tL=last.text.trim();tL.endsWith(tS)&&(last.text=tL.slice(0,-tS.length).trim(),last.text||cleanedSegs.pop())}}s.text.trim()&&cleanedSegs.push(s)}let mergedSegs=[];for(let s of cleanedSegs){if(mergedSegs.length>0){let last=mergedSegs[mergedSegs.length-1];if(s.type==="narration"&&last.type==="narration"&&(s.speaker||"Narrator").toLowerCase()==="narrator"&&(last.speaker||"Narrator").toLowerCase()==="narrator"){/[.!?]$/.test(last.text.trim())?last.text=last.text.trimEnd()+` -`+s.text.trimStart():last.text=last.text.trimEnd()+" "+s.text.trimStart();continue}}mergedSegs.push(s)}let respiltSegs=[];for(const s of mergedSegs)s.type==="narration"&&audiobookHasDialogue(s.text)?respiltSegs.push(...audiobookSplitByQuotes(s.text)):respiltSegs.push(s);segs=respiltSegs}}catch(chunkErr){if(chunkErr.name==="AbortError")throw chunkErr;degraded++,segs=audiobookSplitByQuotes(chunks[i]),view.note(`\u274C Passage ${i+1} \u2014 unexpected error (${chunkErr.message}) \u2014 auto-detected dialogue instead`)}audiobookResolveUnknowns(segs,allSegments2.slice(-2),roster),segs.forEach(s=>{const speakerName=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator"?"Narrator":s.speaker;!/^Unknown|Unbekannt/i.test(speakerName)&&!roster.includes(speakerName)&&roster.push(speakerName)}),segs.forEach(s=>{s.page=_curPageNum,allSegments2.push(s)}),view.addSegments(segs),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)}view.update(_audiobook.cancel?completedChunks:chunks.length)}catch(err){err.name==="AbortError"?(_audiobook.cancel=!0,view.note("Casting stopped. Completed passages were preserved.")):(_audiobook.cancel=!0,view.note(`\u274C Casting stopped \u2014 unexpected error: ${err.message}`))}finally{_abStopDraftAutosave(),_audiobook.running=!1,_audiobook.abort=null}if(!allSegments2.length){if(_audiobook.cancel){view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}view.done(),toast("No segments produced","error");return}try{const _quoteFixedSegs=_audiobookFixOrphanedQuoteMarks(allSegments2),{segments:_rejoined,merged:_mergedCount}=_audiobookMergeSplitSentences(_quoteFixedSegs);_mergedCount&&console.info("[cast] rejoined",_mergedCount,"sentence(s) split by quoted terms");const _mergedSegs=_audiobookMergeAdjacentSameSpeaker(_rejoined);allSegments2.length=0,allSegments2.push(..._mergedSegs);{const _ph=_audiobookDropPhantomSpeakers(allSegments2);_ph.dropped&&console.info("[cast] dropped",_ph.dropped,"phantom speaker line(s):",_ph.names.join(", "))}{const _ident=_audiobookConsolidateSpeakerAliases(allSegments2);if(_ident.merged){console.info("[cast] consolidated",_ident.merged,"speaker label variant(s),",_ident.moved,"line(s)");try{toast(`Merged ${_ident.merged} duplicate character label${_ident.merged!==1?"s":""} (${_ident.moved} lines)`,"success")}catch{}}}_audiobook.segments=allSegments2,_audiobook.lastText=text,_audiobook.roster=roster,_audiobook.narratedPassages=narrationOnly,_audiobook.degraded=degraded,_audiobook.completedChunks=_audiobook.cancel?completedChunks:chunks.length,_audiobook.completedTotal=chunks.length,_abSaveDraft(allSegments2,roster,text,_audiobook.completedChunks,chunks.length),audiobookSaveAsRehearsal({silent:!0}),_audiobook.cancel&&toast("Casting stopped early. Progress preserved.","info");const speakers=new Set(allSegments2.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),stoppedEarly=_audiobook.cancel&&completedChunks(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length>10&&setTimeout(()=>{audiobookAutoRepairCast().catch(e=>console.warn("[auto-repair]",e))},800)}catch(e){console.error("[audiobookCast] post-processing failed",e),view.done({stopped:!0,message:"Finished casting, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}async function audiobookOpenCurrentInRehearser(){var _a2;const segs=_audiobook.segments||[];if(!segs.length)return toast("No cast to open","error"),!1;if(typeof window.rehLoadRecord=="function"&&typeof _audiobookBuildRehRecord=="function")try{const rec=await _audiobookBuildRehRecord(segs);return _audiobook.rehId&&(rec.id=_audiobook.rehId),(_a2=document.getElementById("audiobook-preview"))==null||_a2.remove(),typeof navTo=="function"&&navTo("s-rehearser"),window.rehLoadRecord(rec),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),toast("Opened cast in Script Rehearser","success"),audiobookSaveAsRehearsal({silent:!0}).catch(()=>{}),!0}catch(err){console.warn("[audiobook] failed to build rehearser record directly, falling back:",err)}await audiobookSaveAsRehearsal({silent:!0});const{script,emotions}=audiobookBuildScript(segs);return await audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),!0}function audiobookShowPreview(){var _a2;audiobookOpenCurrentInRehearser()}function audiobookApplyPreviewAndOpen(){const segs=_audiobook.segments;document.querySelectorAll("#audiobook-seglist .audiobook-seg-sp").forEach(inp=>{const i=+inp.dataset.i,v=inp.value.trim()||"Narrator";segs[i].speaker=v,segs[i].type=v.toLowerCase()==="narrator"?"narration":"dialogue"}),document.querySelectorAll("#audiobook-seglist .audiobook-seg-emo").forEach(inp=>{const i=+inp.dataset.i;segs[i].emotion=inp.value.trim()});const{script,emotions}=audiobookBuildScript(segs);audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions)}function audiobookBuildScript(segments){let script="";const emotions=[],marks=_audiobook.pageMarks||[],src=_audiobook.lastText||"";let markIdx=0,searchPos=0;marks.length&&marks[0].offset<=2&&(markIdx=1);for(const s of segments){const t=(s.text||"").trim();if(!t)continue;if(src&&markIdx=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx=marks[markIdx].offset;)script.trim()&&(script+=` +`+s.text.trimStart():last.text=last.text.trimEnd()+" "+s.text.trimStart();continue}}mergedSegs.push(s)}let respiltSegs=[];for(const s of mergedSegs)s.type==="narration"&&audiobookHasDialogue(s.text)?respiltSegs.push(...audiobookSplitByQuotes(s.text)):respiltSegs.push(s);segs=respiltSegs}}catch(chunkErr){if(chunkErr.name==="AbortError")throw chunkErr;degraded++,segs=audiobookSplitByQuotes(chunks[i]),view.note(`\u274C Passage ${i+1} \u2014 unexpected error (${chunkErr.message}) \u2014 auto-detected dialogue instead`)}audiobookResolveUnknowns(segs,allSegments2.slice(-2),roster),segs.forEach(s=>{const speakerName=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator"?"Narrator":s.speaker;!/^Unknown|Unbekannt/i.test(speakerName)&&!roster.includes(speakerName)&&roster.push(speakerName)}),segs.forEach(s=>{s.page=_curPageNum,allSegments2.push(s)}),view.addSegments(segs),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)}view.update(_audiobook.cancel?completedChunks:chunks.length)}catch(err){err.name==="AbortError"?(_audiobook.cancel=!0,view.note("Casting stopped. Completed passages were preserved.")):(_audiobook.cancel=!0,view.note(`\u274C Casting stopped \u2014 unexpected error: ${err.message}`))}finally{_abStopDraftAutosave(),_audiobook.running=!1,_audiobook.abort=null,_abHideCastingControl()}if(!allSegments2.length){if(_audiobook.cancel){view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}view.done(),toast("No segments produced","error");return}try{const _quoteFixedSegs=_audiobookFixOrphanedQuoteMarks(allSegments2),{segments:_rejoined,merged:_mergedCount}=_audiobookMergeSplitSentences(_quoteFixedSegs);_mergedCount&&console.info("[cast] rejoined",_mergedCount,"sentence(s) split by quoted terms");const _mergedSegs=_audiobookMergeAdjacentSameSpeaker(_rejoined);allSegments2.length=0,allSegments2.push(..._mergedSegs);{const _ph=_audiobookDropPhantomSpeakers(allSegments2);_ph.dropped&&console.info("[cast] dropped",_ph.dropped,"phantom speaker line(s):",_ph.names.join(", "))}{const _ident=_audiobookConsolidateSpeakerAliases(allSegments2);if(_ident.merged){console.info("[cast] consolidated",_ident.merged,"speaker label variant(s),",_ident.moved,"line(s)");try{toast(`Merged ${_ident.merged} duplicate character label${_ident.merged!==1?"s":""} (${_ident.moved} lines)`,"success")}catch{}}}_audiobook.segments=allSegments2,_audiobook.lastText=text,_audiobook.roster=roster,_audiobook.narratedPassages=narrationOnly,_audiobook.degraded=degraded,_audiobook.completedChunks=_audiobook.cancel?completedChunks:chunks.length,_audiobook.completedTotal=chunks.length,_abSaveDraft(allSegments2,roster,text,_audiobook.completedChunks,chunks.length),audiobookSaveAsRehearsal({silent:!0}),_audiobook.cancel&&toast("Casting stopped early. Progress preserved.","info");const speakers=new Set(allSegments2.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),stoppedEarly=_audiobook.cancel&&completedChunks(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length>10&&setTimeout(()=>{audiobookAutoRepairCast().catch(e=>console.warn("[auto-repair]",e))},800)}catch(e){console.error("[audiobookCast] post-processing failed",e),view.done({stopped:!0,message:"Finished casting, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}async function audiobookOpenCurrentInRehearser(){var _a2;const segs=_audiobook.segments||[];if(!segs.length)return toast("No cast to open","error"),!1;if(typeof window.rehLoadRecord=="function"&&typeof _audiobookBuildRehRecord=="function")try{const rec=await _audiobookBuildRehRecord(segs);return _audiobook.rehId&&(rec.id=_audiobook.rehId),(_a2=document.getElementById("audiobook-preview"))==null||_a2.remove(),typeof navTo=="function"&&navTo("s-rehearser"),window.rehLoadRecord(rec),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),toast("Opened cast in Script Rehearser","success"),audiobookSaveAsRehearsal({silent:!0}).catch(()=>{}),!0}catch(err){console.warn("[audiobook] failed to build rehearser record directly, falling back:",err)}await audiobookSaveAsRehearsal({silent:!0});const{script,emotions}=audiobookBuildScript(segs);return await audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),!0}function audiobookShowPreview(){var _a2;audiobookOpenCurrentInRehearser()}function audiobookApplyPreviewAndOpen(){const segs=_audiobook.segments;document.querySelectorAll("#audiobook-seglist .audiobook-seg-sp").forEach(inp=>{const i=+inp.dataset.i,v=inp.value.trim()||"Narrator";segs[i].speaker=v,segs[i].type=v.toLowerCase()==="narrator"?"narration":"dialogue"}),document.querySelectorAll("#audiobook-seglist .audiobook-seg-emo").forEach(inp=>{const i=+inp.dataset.i;segs[i].emotion=inp.value.trim()});const{script,emotions}=audiobookBuildScript(segs);audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions)}function audiobookBuildScript(segments){let script="";const emotions=[],marks=_audiobook.pageMarks||[],src=_audiobook.lastText||"";let markIdx=0,searchPos=0;marks.length&&marks[0].offset<=2&&(markIdx=1);for(const s of segments){const t=(s.text||"").trim();if(!t)continue;if(src&&markIdx=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx=marks[markIdx].offset;)script.trim()&&(script+=` \f${marks[markIdx].page+1} `),markIdx++}s.type==="dialogue"&&s.speaker&&s.speaker.toLowerCase()!=="narrator"?(script+=` `+s.speaker.toUpperCase()+` @@ -1540,7 +1540,7 @@ For each character output these fields: - mannerisms: habitual gestures, tics, body language, habits and quirks - communication_style: how they communicate socially \u2014 blunt, formal, warm, guarded, sarcastic, etc. - voice_pattern: speech style \u2014 accent, pacing, vocabulary, register and verbal tics (for voice casting) -- voice_design_prompt: concise English Qwen voice-design prompt (15-45 words). Include age impression, gender/androgyny if inferable, pitch, timbre, pace, accent/register, emotional baseline and suitability for audiobook dialogue. Do NOT mention plot spoilers. +- voice_design_prompt: concise English voice-design prompt, maximum 480 characters. Use only objective, audible traits; no lore-only labels or repeated synonyms. Include gender as a hard audible constraint, age impression, pitch, timbre, pace, accent/register, emotion and audiobook use. Translate species/role into audible cues (orc: chest-led/gravel-edged; vampire: cool/velvety/controlled; elf: airy/precise/adult; demon: dark/subtly otherworldly; warrior: disciplined command cadence; blacksmith: work-worn/practical; mage: measured/precise). Give one distinctive vocal fingerprint. Do NOT mention plot spoilers. - image_prompt: detailed English image-generation prompt for this character. Include face, age impression, build, hair/eyes/skin if known, clothing, posture, props, mood, genre/style, and visible symbols. Mark inferred traits with '*'. - inventory: 1-3 defining items/props/clothing (array of short strings) - secret: dark secret or fatal flaw @@ -1555,16 +1555,16 @@ IDENTITY MERGING: A character may appear under multiple names in the book (first Reuse the EXACT names from the known-characters list for returning characters when they are the canonical name or an alias of this character. Do not merge characters merely because their names appear near each other, in the known-character list, or in relationships. Respond with STRICT JSON only: {"sheets":[{"name":"","aliases":"","first_name":"","last_name":"","title":"","profession":"","age_estimate":"","race_species":"","languages":"","nationality_background":"","social_class":"","archetype":"","gender":"","physical":"","clothing":"","alignment":"","moral_alignment_score":50,"arc_direction":"neutral","arc_note":"","attribute_high":"","attribute_low":"","skills":"","capabilities":"","backstory":"","relationships":"","motivation":"","fears":"","mannerisms":"","communication_style":"","voice_pattern":"","voice_design_prompt":"","image_prompt":"","inventory":[],"secret":"","conflict_style":"","win_condition":"","reputation":"","religious_beliefs":"","notes":"","tier":"main","sources":[{"page":1,"quote":"","line_hint":""}]}]} -/no-think`;function csLlmUrl(){var _a2;const explicit=(_a2=$("reh-llm-url"))==null?void 0:_a2.value.trim();if(explicit)return explicit;const fromSettings=typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_url?_appSettings.llm_url:"";return fromSettings&&!/localhost:11434|127\.0\.0\.1:11434/.test(fromSettings)?fromSettings:""}function csLlmModel(){var _a2;return((_a2=$("reh-llm-model"))==null?void 0:_a2.value)||""}function csLang(){var _a2;return((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||""}function csReaderPageHost(){return $("reader-charsheets-panel")}function csReaderText(){var _a2,_b2,_c2;if(typeof readerScopeIndices!="function"||!((_a2=readerState==null?void 0:readerState.sentences)!=null&&_a2.length))return"";let out="",lastPage=-1;for(const i of readerScopeIndices()){const u=readerState.sentences[i],pg=(_c2=(_b2=u.words)==null?void 0:_b2[0])==null?void 0:_c2.page;readerState.mode==="pdf"&&pg!=null&&pg!==lastPage&&(out+=` +/no-think`;function csLlmUrl(){var _a2;const explicit=(_a2=$("reh-llm-url"))==null?void 0:_a2.value.trim();if(explicit)return explicit;const fromSettings=typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_url?_appSettings.llm_url:"";return fromSettings&&!/localhost:11434|127\.0\.0\.1:11434/.test(fromSettings)?fromSettings:""}function csLlmModel(){var _a2;return((_a2=$("reh-llm-model"))==null?void 0:_a2.value)||""}function csLang(){var _a2;return((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||""}function csReaderPageHost(){return $("reader-charsheets-panel")}async function csEnsureBookContext(book,text,roster,opts={}){if(!book||!text||typeof _getBookProfile!="function")return{};let existing={};try{existing=await _getBookProfile(book)}catch{}if(!["genre","setting","era","language"].filter(k=>!String((existing==null?void 0:existing[k])||"").trim()).length&&!opts.force)return existing;try{const r=await fetch("/api/book-profile/generate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({book,text,known_characters:Array.isArray(roster)?roster:[],llm_url:csLlmUrl(),model:csLlmModel(),language:csLang()})});if(!r.ok)throw new Error((await r.text()).slice(0,300)||"HTTP "+r.status);const data=await r.json();return typeof _bookProfileCache!="undefined"&&_bookProfileCache.set(book,data.profile||{}),data.profile||existing}catch(e){return console.warn("[book context] automatic generation failed:",e),typeof toast=="function"&&toast("Book context could not be generated; profile casting will continue","info"),existing}}function csReaderText(){var _a2,_b2,_c2;if(typeof readerScopeIndices!="function"||!((_a2=readerState==null?void 0:readerState.sentences)!=null&&_a2.length))return"";let out="",lastPage=-1;for(const i of readerScopeIndices()){const u=readerState.sentences[i],pg=(_c2=(_b2=u.words)==null?void 0:_b2[0])==null?void 0:_c2.page;readerState.mode==="pdf"&&pg!=null&&pg!==lastPage&&(out+=` [p.${pg+1}] `,lastPage=pg),out+=u.text+" "}return out.trim()}function csRehearserText(){if(!window.rehState||!(rehState.lines||[]).length)return"";let out="",page=1,started=!1;for(const l of rehState.lines){if(l.type==="pagebreak"){page++,out+=` [p.${page}] `;continue}const t=(typeof stripMarkdown=="function"?stripMarkdown(l.text||""):l.text||"").trim();t&&(started||(out+="[p.1] ",started=!0),out+=(l.type==="dialog"&&l.speaker?l.speaker+": ":"")+t+` `)}return out.trim()}const CS_SCALAR_FIELDS=["aliases","first_name","last_name","full_name","title","profession","age_estimate","race_species","languages","nationality_background","social_class","archetype","physical","clothing","alignment","arc_note","attribute_high","attribute_low","skills","capabilities","backstory","relationships","motivation","fears","mannerisms","communication_style","voice_pattern","secret","conflict_style","win_condition","reputation","religious_beliefs","notes","voice_design_prompt","image_prompt","silly_tavern_prompt","concept_art_prompt"],CS_DETAIL_FIELDS=CS_SCALAR_FIELDS.filter(f=>!["aliases","first_name","last_name","full_name","title"].includes(f)),CS_IDENTITY_FIELDS=["name","aliases","first_name","last_name","full_name"],CS_ALIAS_MAX_TOKENS=12,CS_ALIAS_MAX_CHARS=500,CS_SOURCE_FIELDS={physical:["physical","appearance","body","look"],clothing:["clothing","appearance","armour","armor","item"],age_estimate:["age","older","young","teen","adult"],race_species:["race","species","kind","orc","elf","human","vampire"],languages:["language","languages","tongue","dialect","speech"],nationality_background:["nationality","background","origin","homeland","culture"],social_class:["class","social","status","rank"],relationships:["relationship","ally","rival","enemy","family"],motivation:["motivation","intention","goal","desire"],fears:["fear","dread"],mannerisms:["mannerism","habit","gesture","voice"],communication_style:["communication","communicate","demeanor","style"],voice_pattern:["voice","speech","dialogue"],backstory:["backstory","origin","history"],alignment:["alignment","ethos","morality"],profession:["profession","occupation","job","role"],skills:["skill","capability","ability"],capabilities:["capability","ability","combat","magic"],reputation:["reputation","known as","status","perceived"],religious_beliefs:["religion","faith","belief","priest","god","gods","temple"],notes:["note","misc","miscellaneous"],secret:["secret","flaw"],conflict_style:["conflict","fight","flight","manipulate"],win_condition:["win","goal"]};function csExistingSummary(map){if(!map.size)return"";const KEY_FIELDS=["age_estimate","race_species","languages","nationality_background","social_class","profession","physical","clothing","communication_style","voice_pattern","backstory","motivation","relationships","reputation","religious_beliefs","notes"];return[...map.values()].slice(0,40).map(s=>{const missing=KEY_FIELDS.filter(f=>!(s[f]||"").trim()),suffix=missing.lengths.trim()).filter(Boolean).filter(s=>s.length<=80&&!/^needs?:/i.test(s)&&!/^complete$/i.test(s));return opts.aliases&&(raw.length>CS_ALIAS_MAX_CHARS||parts.length>CS_ALIAS_MAX_TOKENS)?[]:parts.slice(0,opts.aliases?CS_ALIAS_MAX_TOKENS:void 0)}function csCleanRoster(names){const out=[],seen=new Set;return(names||[]).forEach(n=>{const name=_csStr(n).trim(),key=name.toLowerCase();!name||seen.has(key)||key==="narrator"||/^unknown|unbekannt$/i.test(name)||(seen.add(key),out.push(name))}),out}function csRosterKey(v){return String(v||"").trim().toLowerCase()}function csKnownReaderRoster(){const names=[],add=name=>{name=_csStr(name).trim();const key=name.toLowerCase();!name||key==="narrator"||/^unknown|unbekannt$/i.test(name)||names.some(n=>n.toLowerCase()===key)||names.push(name)},ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook;return ab&&((ab.roster||[]).forEach(add),[ab.segments,ab.liveSegments].forEach(segs=>{(segs||[]).forEach(s=>{(s==null?void 0:s.type)==="dialogue"&&add(s.speaker)})})),document.querySelectorAll("#ab-cv-chars .ab-char-item[data-name], #audiobook-roster option[value]").forEach(el=>{add(el.dataset.name||el.value||el.textContent)}),csCleanRoster(names)}function csBlankSheet(name){return{name,aliases:"",first_name:"",last_name:"",full_name:"",title:"",profession:"",age_estimate:"",race_species:"",languages:"",nationality_background:"",social_class:"",archetype:"",gender:"",physical:"",clothing:"",alignment:"",arc_note:"",attribute_high:"",attribute_low:"",skills:"",capabilities:"",backstory:"",relationships:"",motivation:"",fears:"",mannerisms:"",communication_style:"",voice_pattern:"",secret:"",conflict_style:"",win_condition:"",reputation:"",religious_beliefs:"",notes:"",voice_design_prompt:"",image_prompt:"",inventory:[],sources:[],moral_alignment_score:50,arc_direction:"neutral",tier:"supporting"}}function csSeedSheet(raw){const name=_csStr(raw==null?void 0:raw.name).trim(),seed=csBlankSheet(name);if(!name)return seed;raw!=null&&raw.image&&(seed._image=raw.image);const src=raw!=null&&raw.sheet&&typeof raw.sheet=="object"?raw.sheet:raw;if(CS_SCALAR_FIELDS.forEach(f=>{src&&src[f]!=null&&(seed[f]=_csStr(src[f]))}),Array.isArray(src==null?void 0:src.inventory)?seed.inventory=[...src.inventory]:typeof(src==null?void 0:src.inventory)=="string"&&(seed.inventory=src.inventory.split(",").map(x=>x.trim()).filter(Boolean)),Array.isArray(src==null?void 0:src.sources)&&(seed.sources=src.sources.map(item=>item&&typeof item=="object"?{page:item.page!=null?item.page:null,quote:_csStr(item.quote).slice(0,240),line_hint:_csStr(item.line_hint).slice(0,60)}:null).filter(Boolean)),src!=null&&src.tier&&(seed.tier=String(src.tier).toLowerCase().startsWith("main")?"main":"supporting"),(src==null?void 0:src.moral_alignment_score)!=null){const mas=parseInt(src.moral_alignment_score,10);Number.isNaN(mas)||(seed.moral_alignment_score=Math.max(0,Math.min(100,mas)))}return src!=null&&src.arc_direction&&(seed.arc_direction=src.arc_direction),src!=null&&src.gender&&(seed.gender=String(src.gender).trim().toLowerCase()),(src==null?void 0:src.line_count)!=null&&(seed.line_count=src.line_count),seed}function csNameTokens(sheet){const out=new Set;for(const f of CS_IDENTITY_FIELDS){const raw=f==="name"?sheet==null?void 0:sheet.name:sheet==null?void 0:sheet[f];csSplitIdentityTokens(raw,{aliases:f==="aliases"}).forEach(s=>out.add(s.toLowerCase()))}return out}function csSheetMatchesRoster(sheet,roster=[]){const allowed=new Set((roster||[]).map(csRosterKey).filter(Boolean));if(!allowed.size)return!0;for(const token of csNameTokens(sheet))for(const name of allowed)if(token===name||token.includes(name)||name.includes(token))return!0;return!1}function csIdentityNeedles(value){return csSplitIdentityTokens(value,{aliases:!0}).filter(v=>{const n=String(v||"").trim();return n&&n.length>1&&!/^(die|der|das|den|dem|des|ein|eine|einer|er|sie|es|ich|du|wir|ihr)$/i.test(n)})}function csRecordNeedles(rec){var _a2,_b2,_c2;const seen=new Set,out=[],add=value=>{csIdentityNeedles(value).forEach(v=>{const key=v.toLowerCase();seen.has(key)||(seen.add(key),out.push(v))})};return add(rec==null?void 0:rec.name),add((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.aliases),add([(_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name,(_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name].filter(Boolean).join(" ")),out}function csParagraphBlocks(text){return String(text||"").replace(/\r\n/g,` +`)}function _csStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function csSplitIdentityTokens(v,opts={}){const raw=_csStr(v),parts=raw.split(/[,;/|]|\baka\b|\baka\.\b|\balias(?:es)?\b|\bgenannt\b|\bnamens\b|\bcalled\b|\bknown as\b/i).map(s=>s.trim()).filter(Boolean).filter(s=>s.length<=80&&!/^needs?:/i.test(s)&&!/^complete$/i.test(s));return opts.aliases&&(raw.length>CS_ALIAS_MAX_CHARS||parts.length>CS_ALIAS_MAX_TOKENS)?[]:parts.slice(0,opts.aliases?CS_ALIAS_MAX_TOKENS:void 0)}function csCleanRoster(names){const out=[],seen=new Set;return(names||[]).forEach(n=>{const name=_csStr(n).trim(),key=name.toLowerCase();!name||seen.has(key)||key==="narrator"||/^unknown|unbekannt$/i.test(name)||(seen.add(key),out.push(name))}),out}function csRosterKey(v){return String(v||"").trim().toLowerCase()}function csKnownReaderRoster(){const names=[],add=name=>{name=_csStr(name).trim();const key=name.toLowerCase();!name||key==="narrator"||/^unknown|unbekannt$/i.test(name)||names.some(n=>n.toLowerCase()===key)||names.push(name)},ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook;return ab&&((ab.roster||[]).forEach(add),[ab.segments,ab.liveSegments].forEach(segs=>{(segs||[]).forEach(s=>{(s==null?void 0:s.type)==="dialogue"&&add(s.speaker)})})),document.querySelectorAll("#ab-cv-chars .ab-char-item[data-name], #audiobook-roster option[value]").forEach(el=>{add(el.dataset.name||el.value||el.textContent)}),csCleanRoster(names)}function csBlankSheet(name){return{name,aliases:"",first_name:"",last_name:"",full_name:"",title:"",profession:"",age_estimate:"",race_species:"",languages:"",nationality_background:"",social_class:"",archetype:"",gender:"",physical:"",clothing:"",alignment:"",arc_note:"",attribute_high:"",attribute_low:"",skills:"",capabilities:"",backstory:"",relationships:"",motivation:"",fears:"",mannerisms:"",communication_style:"",voice_pattern:"",secret:"",conflict_style:"",win_condition:"",reputation:"",religious_beliefs:"",notes:"",voice_design_prompt:"",image_prompt:"",inventory:[],sources:[],moral_alignment_score:50,arc_direction:"neutral",tier:"supporting"}}function csSeedSheet(raw){const name=_csStr(raw==null?void 0:raw.name).trim(),seed=csBlankSheet(name);if(!name)return seed;raw!=null&&raw.image&&(seed._image=raw.image);const src=raw!=null&&raw.sheet&&typeof raw.sheet=="object"?raw.sheet:raw;if(CS_SCALAR_FIELDS.forEach(f=>{src&&src[f]!=null&&(seed[f]=_csStr(src[f]))}),Array.isArray(src==null?void 0:src.inventory)?seed.inventory=[...src.inventory]:typeof(src==null?void 0:src.inventory)=="string"&&(seed.inventory=src.inventory.split(",").map(x=>x.trim()).filter(Boolean)),Array.isArray(src==null?void 0:src.sources)&&(seed.sources=src.sources.map(item=>item&&typeof item=="object"?{page:item.page!=null?item.page:null,quote:_csStr(item.quote).slice(0,240),line_hint:_csStr(item.line_hint).slice(0,60)}:null).filter(Boolean)),src!=null&&src.tier&&(seed.tier=String(src.tier).toLowerCase().startsWith("main")?"main":"supporting"),(src==null?void 0:src.moral_alignment_score)!=null){const mas=parseInt(src.moral_alignment_score,10);Number.isNaN(mas)||(seed.moral_alignment_score=Math.max(0,Math.min(100,mas)))}return src!=null&&src.arc_direction&&(seed.arc_direction=src.arc_direction),src!=null&&src.gender&&(seed.gender=String(src.gender).trim().toLowerCase()),(src==null?void 0:src.line_count)!=null&&(seed.line_count=src.line_count),seed}function csNameTokens(sheet){const out=new Set;for(const f of CS_IDENTITY_FIELDS){const raw=f==="name"?sheet==null?void 0:sheet.name:sheet==null?void 0:sheet[f];csSplitIdentityTokens(raw,{aliases:f==="aliases"}).forEach(s=>out.add(s.toLowerCase()))}return out}function csSheetMatchesRoster(sheet,roster=[]){const allowed=new Set((roster||[]).map(csRosterKey).filter(Boolean));if(!allowed.size)return!0;for(const token of csNameTokens(sheet))for(const name of allowed)if(token===name||token.includes(name)||name.includes(token))return!0;return!1}function csRosterCanonicalName(sheet,roster=[]){const names=csCleanRoster(roster);if(!names.length)return"";const tokens=csNameTokens(sheet);return names.find(n=>tokens.has(n.toLowerCase()))||""}function csCanonicalizeRosterNames(sheets,roster=[]){const canonical=new Map;for(const original of sheets||[]){if(!original)continue;const sheet={...original},rosterName=csRosterCanonicalName(sheet,roster);rosterName&&sheet.name&&sheet.name.toLowerCase()!==rosterName.toLowerCase()&&(sheet.aliases=csMergeAliases({name:rosterName,aliases:sheet.aliases},{name:sheet.name,aliases:""}),sheet.name=rosterName),rosterName&&(sheet._rosterNameAuthoritative=!0);const key=String(sheet.name||"").trim().toLowerCase();key&&(canonical.has(key)?csMerge(canonical,[sheet]):canonical.set(key,sheet))}return[...canonical.values()]}function csIdentityNeedles(value){return csSplitIdentityTokens(value,{aliases:!0}).filter(v=>{const n=String(v||"").trim();return n&&n.length>1&&!/^(die|der|das|den|dem|des|ein|eine|einer|er|sie|es|ich|du|wir|ihr)$/i.test(n)})}function csRecordNeedles(rec){var _a2,_b2,_c2;const seen=new Set,out=[],add=value=>{csIdentityNeedles(value).forEach(v=>{const key=v.toLowerCase();seen.has(key)||(seen.add(key),out.push(v))})};return add(rec==null?void 0:rec.name),add((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.aliases),add([(_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name,(_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name].filter(Boolean).join(" ")),out}function csParagraphBlocks(text){return String(text||"").replace(/\r\n/g,` `).split(/\n\s*\n+/).map(s=>s.trim()).filter(Boolean)}function csReaderParagraphBlocks(){var _a2,_b2,_c2;if(readerState.mode==="text"&&readerState.docText)return csParagraphBlocks(readerState.docText);const base=Array.isArray(readerState.baseSentences)&&readerState.baseSentences.length?readerState.baseSentences:Array.isArray(readerState.sentences)?readerState.sentences:[];if(!base.length)return csParagraphBlocks(csReaderText());const paras=[];let cur="",curPage=null;for(const s of base){const page=(_c2=(_b2=(_a2=s==null?void 0:s.words)==null?void 0:_a2[0])==null?void 0:_b2.page)!=null?_c2:null,pageChanged=readerState.mode==="pdf"&&page!=null&&curPage!=null&&page!==curPage;(s!=null&&s.paraStart||pageChanged)&&cur.trim()&&(paras.push(cur.trim()),cur=""),!cur&&readerState.mode==="pdf"&&page!=null&&(cur+=`[p.${page+1}] `),cur+=(cur&&!/\s$/.test(cur)?" ":"")+String((s==null?void 0:s.text)||"").trim(),curPage=page!=null?page:curPage}return cur.trim()&¶s.push(cur.trim()),paras}function csParagraphHasNeedle(paragraph,needle){const p=String(paragraph||""),n=String(needle||"").trim();if(!n||n.length<2)return!1;const lowerP=p.toLowerCase(),lowerN=n.toLowerCase();if(lowerN.includes(" "))return lowerP.includes(lowerN);const esc=lowerN.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(^|[^\\p{L}\\p{N}_])${esc}(?=$|[^\\p{L}\\p{N}_])`,"iu").test(p)}function csEvidenceWindowText(text,needles,before=2,after=2){const paras=Array.isArray(text)?text.slice():csParagraphBlocks(text);if(!paras.length)return String(text||"").trim();const hits=new Set;return paras.forEach((para,idx)=>{if((needles||[]).some(n=>csParagraphHasNeedle(para,n)))for(let i=idx-before;i<=idx+after;i++)i>=0&&ia-b).map(i=>paras[i]).join(` -`).trim():""}function csFindMergeKey(map,sheet){const incoming=csNameTokens(sheet);if(!incoming.size)return(sheet.name||"").trim().toLowerCase();for(const[key,existing]of map.entries()){const existingNames=csNameTokens(existing);for(const n of incoming)if(existingNames.has(n))return key}return(sheet.name||"").trim().toLowerCase()}function csMergeAliases(existing,incoming){const names=new Map,add=v=>csSplitIdentityTokens(v,{aliases:!0}).forEach(s=>names.set(s.toLowerCase(),s));return add(existing.aliases),add(incoming.aliases),incoming.name&&incoming.name!==existing.name&&add(incoming.name),[...names.values()].filter(n=>n.toLowerCase()!==String(existing.name||"").toLowerCase()).join(", ")}function csMerge(map,sheets){const changed=[];for(const s of sheets){const name=(s.name||"").trim();if(!name)continue;s.aliases=csMergeAliases({name,aliases:""},s);const key=csFindMergeKey(map,s);if(!map.has(key)){const copy={...s,name,inventory:[...s.inventory||[]],sources:[...s.sources||[]]};CS_SCALAR_FIELDS.forEach(f=>{copy[f]=_csStr(copy[f])}),map.set(key,copy),changed.push({key,name:copy.name,status:"new"});continue}const e=map.get(key),before=JSON.stringify(e),oldAliases=e.aliases;CS_SCALAR_FIELDS.forEach(f=>{const sv=_csStr(s[f]);sv.length>(e[f]||"").length&&(e[f]=sv)}),e.aliases=csMergeAliases({...e,aliases:oldAliases},s),s.tier==="main"&&(e.tier="main"),s.moral_alignment_score!=null&&(e.moral_alignment_score=e.moral_alignment_score!=null?Math.round((e.moral_alignment_score+s.moral_alignment_score)/2):s.moral_alignment_score),s.arc_direction&&s.arc_direction!=="neutral"&&(e.arc_direction=s.arc_direction),s.gender&&!e.gender&&(e.gender=s.gender),(s.inventory||[]).forEach(it=>{it&&!e.inventory.includes(it)&&e.inventory.length<3&&e.inventory.push(it)}),(s.sources||[]).forEach(src=>{src&&src.quote&&e.sources.length<12&&!e.sources.some(x=>x.quote===src.quote)&&e.sources.push(src)}),JSON.stringify(e)!==before&&changed.push({key,name:e.name,status:"updated"})}return changed}function csConsolidateAliasDuplicates(map,establishedKeys,establishedCreated){let changed=!0;for(;changed;){changed=!1;for(const[key,s]of map){const dupKey=csSplitIdentityTokens(s.aliases,{aliases:!0}).map(a=>a.toLowerCase()).find(a=>a!==key&&map.has(a));if(!dupKey)continue;const other=map.get(dupKey),sEst=establishedKeys.has(key),oEst=establishedKeys.has(dupKey);let keepKey=key,dropKey=dupKey;if(oEst&&!sEst)keepKey=dupKey,dropKey=key;else if(sEst===oEst){const filled=sh=>CS_DETAIL_FIELDS.filter(f=>_csStr(sh[f]).trim()).length,sFilled=filled(s),oFilled=filled(other);if(oFilled>sFilled)keepKey=dupKey,dropKey=key;else if(oFilled===sFilled&&establishedCreated){const sT=establishedCreated.get(key),oT=establishedCreated.get(dupKey);sT!=null&&oT!=null&&oT{const dv=_csStr(drop[f]);dv.length>(keep[f]||"").length&&(keep[f]=dv)}),keep.aliases=csMergeAliases({...keep,aliases:oldAliases},drop),drop.tier==="main"&&(keep.tier="main"),drop.line_count!=null&&(keep.line_count=Math.max(keep.line_count||0,drop.line_count)),(drop.inventory||[]).forEach(it=>{it&&!keep.inventory.includes(it)&&keep.inventory.length<3&&keep.inventory.push(it)}),(drop.sources||[]).forEach(src=>{src&&src.quote&&keep.sources.length<12&&!keep.sources.some(x=>x.quote===src.quote)&&keep.sources.push(src)}),map.set(keepKey,keep),map.delete(dropKey),changed=!0;break}}}function csProgressSnapshot(map,changed=[]){const changedByKey=new Map(changed.map(x=>[x.key,x.status]));return[...map.entries()].map(([key,s])=>{const filled=CS_DETAIL_FIELDS.filter(f=>_csStr(s[f]).trim()).length;return{key,name:s.name||key,alias:[s.title,s.profession].filter(Boolean).join(" \xB7 "),status:changedByKey.get(key)||"",filled,main:s.tier==="main",gender:s.gender||"",lineCount:Number(s.line_count)||0,image:s._image||""}}).sort((a,b)=>(b.status?1:0)-(a.status?1:0)||b.filled-a.filled||a.name.localeCompare(b.name))}async function csGenerateStream(body,onDelta,outerSignal,idleTimeoutMs=6e4){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/character-sheets/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify(body)});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(` +`).trim():""}function csFindMergeKey(map,sheet){const incoming=csNameTokens(sheet);if(!incoming.size)return(sheet.name||"").trim().toLowerCase();for(const[key,existing]of map.entries()){const existingNames=csNameTokens(existing);for(const n of incoming)if(existingNames.has(n))return key}return(sheet.name||"").trim().toLowerCase()}function csMergeAliases(existing,incoming){const names=new Map,add=v=>csSplitIdentityTokens(v,{aliases:!0}).forEach(s=>names.set(s.toLowerCase(),s));return add(existing.aliases),add(incoming.aliases),incoming.name&&incoming.name!==existing.name&&add(incoming.name),[...names.values()].filter(n=>n.toLowerCase()!==String(existing.name||"").toLowerCase()).join(", ")}function csMerge(map,sheets){const changed=[];for(const s of sheets){const name=(s.name||"").trim();if(!name)continue;s.aliases=csMergeAliases({name,aliases:""},s);const key=csFindMergeKey(map,s);if(!map.has(key)){const copy={...s,name,inventory:[...s.inventory||[]],sources:[...s.sources||[]]};CS_SCALAR_FIELDS.forEach(f=>{copy[f]=_csStr(copy[f])}),map.set(key,copy),changed.push({key,name:copy.name,status:"new"});continue}const e=map.get(key),before=JSON.stringify(e),oldAliases=e.aliases;CS_SCALAR_FIELDS.forEach(f=>{const sv=_csStr(s[f]);sv.length>(e[f]||"").length&&(e[f]=sv)}),e.aliases=csMergeAliases({...e,aliases:oldAliases},s),s.tier==="main"&&(e.tier="main"),s.moral_alignment_score!=null&&(e.moral_alignment_score=e.moral_alignment_score!=null?Math.round((e.moral_alignment_score+s.moral_alignment_score)/2):s.moral_alignment_score),s.arc_direction&&s.arc_direction!=="neutral"&&(e.arc_direction=s.arc_direction),s.gender&&!e.gender&&(e.gender=s.gender),(s.inventory||[]).forEach(it=>{it&&!e.inventory.includes(it)&&e.inventory.length<3&&e.inventory.push(it)}),(s.sources||[]).forEach(src=>{src&&src.quote&&e.sources.length<12&&!e.sources.some(x=>x.quote===src.quote)&&e.sources.push(src)}),JSON.stringify(e)!==before&&changed.push({key,name:e.name,status:"updated"})}return changed}function csConsolidateAliasDuplicates(map,establishedKeys,establishedCreated,roster=[]){let changed=!0;for(;changed;){changed=!1;for(const[key,s]of map){const dupKey=csSplitIdentityTokens(s.aliases,{aliases:!0}).map(a=>a.toLowerCase()).find(a=>a!==key&&map.has(a));if(!dupKey)continue;const other=map.get(dupKey),sEst=establishedKeys.has(key),oEst=establishedKeys.has(dupKey),rosterNames=csCleanRoster(roster),sRoster=rosterNames.findIndex(n=>n.toLowerCase()===key),oRoster=rosterNames.findIndex(n=>n.toLowerCase()===dupKey);let keepKey=key,dropKey=dupKey;if(sRoster>=0&&oRoster>=0&&sRoster!==oRoster)oRoster=0)keepKey=dupKey,dropKey=key;else if(oEst&&!sEst)keepKey=dupKey,dropKey=key;else if(sEst===oEst){const filled=sh=>CS_DETAIL_FIELDS.filter(f=>_csStr(sh[f]).trim()).length,sFilled=filled(s),oFilled=filled(other);if(oFilled>sFilled)keepKey=dupKey,dropKey=key;else if(oFilled===sFilled&&establishedCreated){const sT=establishedCreated.get(key),oT=establishedCreated.get(dupKey);sT!=null&&oT!=null&&oT{const dv=_csStr(drop[f]);dv.length>(keep[f]||"").length&&(keep[f]=dv)}),keep.aliases=csMergeAliases({...keep,aliases:oldAliases},drop),drop.tier==="main"&&(keep.tier="main"),drop.line_count!=null&&(keep.line_count=Math.max(keep.line_count||0,drop.line_count)),(drop.inventory||[]).forEach(it=>{it&&!keep.inventory.includes(it)&&keep.inventory.length<3&&keep.inventory.push(it)}),(drop.sources||[]).forEach(src=>{src&&src.quote&&keep.sources.length<12&&!keep.sources.some(x=>x.quote===src.quote)&&keep.sources.push(src)}),map.set(keepKey,keep),map.delete(dropKey),changed=!0;break}}}function csProgressSnapshot(map,changed=[]){const changedByKey=new Map(changed.map(x=>[x.key,x.status]));return[...map.entries()].map(([key,s])=>{const filled=CS_DETAIL_FIELDS.filter(f=>_csStr(s[f]).trim()).length;return{key,name:s.name||key,alias:[s.title,s.profession].filter(Boolean).join(" \xB7 "),status:changedByKey.get(key)||"",filled,main:s.tier==="main",gender:s.gender||"",lineCount:Number(s.line_count)||0,image:s._image||""}}).sort((a,b)=>(b.status?1:0)-(a.status?1:0)||b.filled-a.filled||a.name.localeCompare(b.name))}async function csGenerateStream(body,onDelta,outerSignal,idleTimeoutMs=6e4){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/character-sheets/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify(body)});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(` -`))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&onDelta&&onDelta(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}async function csGenerate(text,cacheKey,initialRoster,opts={}){if(_cs.running)return null;if(!text)return toast("Nothing to analyse","error"),null;const chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,CS_CHUNK_CHARS):[text];_cs.running=!0,_cs.cancel=!1;try{const prog=csProgress(chunks.length,opts.pageHost||null),llm_url=csLlmUrl(),model=csLlmModel();let language=csLang();if(typeof detectLang=="function"){const detected=detectLang(text);if(detected&&(!language||/^auto$/i.test(language)||language.toLowerCase()==="english"&&detected.toLowerCase()!=="english")){language=detected;const sel=$("reh-design-lang");if(sel){const o=[...sel.options].find(x=>x.value.toLowerCase()===detected.toLowerCase()||x.textContent.toLowerCase()===detected.toLowerCase());o&&(sel.value=o.value)}}}const map=new Map,lineCounts=opts.lineCounts instanceof Map?opts.lineCounts:new Map,seedSheets=Array.isArray(opts.seedSheets)?opts.seedSheets:[],establishedKeys=new Set,establishedCreated=new Map;seedSheets.forEach(seedRaw=>{const seed=csSeedSheet(seedRaw),name=String(seed.name||"").trim();if(!name)return;const lc=lineCounts.get(name.toLowerCase());lc!=null&&(seed.line_count=lc),map.set(name.toLowerCase(),seed),establishedKeys.add(name.toLowerCase());const created=seedRaw!=null&&seedRaw.created?new Date(seedRaw.created).getTime():NaN;isNaN(created)||establishedCreated.set(name.toLowerCase(),created)});const roster=csCleanRoster(initialRoster),targetMode=roster.length>0,knownTotal=targetMode?roster.length:null;roster.forEach(name=>{const key=name.toLowerCase();if(!map.has(key)){const blank=csBlankSheet(name),lc=lineCounts.get(key);lc!=null&&(blank.line_count=lc),map.set(key,blank)}});try{for(let i=0;iCS_DETAIL_FIELDS.some(f=>(s[f]||"").trim())).length,charLabel=knownTotal?`${knownTotal} cast characters queued \xB7 ${detailed} profiles with details`:`${map.size} characters found`;prog.update(i,`Passage ${i+1} / ${chunks.length}\u2026`,charLabel,csProgressSnapshot(map),[...map.values()]),prog.startPassage(chunks[i]);const maxAttempts=4;for(let attempt=1;attempt<=maxAttempts;attempt++)try{const character_sheets_prompt=typeof _appSettings!="undefined"&&_appSettings.character_sheets_prompt||CS_DEFAULT_PROMPT,body={text:chunks[i],known_characters:roster.slice(0,120),target_mode:targetMode,existing:csExistingSummary(map),language,llm_url,model,character_sheets_prompt};let data=null,sawDelta=!1;try{data=await csGenerateStream(body,delta=>{sawDelta=!0,prog.thinking(delta)}),sawDelta||prog.noStream()}catch{sawDelta||prog.noStream();const r=await fetch("/api/character-sheets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){let detail=`HTTP ${r.status}`;try{const e=await r.json();detail=e.detail||e.error||detail}catch{}throw new Error(detail)}const raw=await r.text();try{data=JSON.parse(raw)}catch{throw new Error("Invalid JSON from server (passage may be too large)")}}const incomingSheets=targetMode?(data.sheets||[]).filter(s=>csSheetMatchesRoster(s,roster)):data.sheets||[];incomingSheets.forEach(s=>{const ownName=String(s.name||"").trim().toLowerCase();["first_name","last_name","full_name"].forEach(f=>{const v=String(s[f]||"").trim().toLowerCase();v&&v!==ownName&&roster.some(n=>n.toLowerCase()===v)&&(console.warn(`[character sheets] dropped ${f}="${s[f]}" from "${s.name}" \u2014 matches a different known character`),s[f]="")})});const changed=csMerge(map,incomingSheets);changed.length&&prog.focus(changed[0].name),targetMode||((data.characters||[]).forEach(n=>{roster.some(r=>r.toLowerCase()===String(n||"").toLowerCase())||roster.push(n)}),(data.sheets||[]).forEach(s=>{csNameTokens(s).forEach(n=>{const display=[s.name,s.title,s.profession,s.aliases].filter(Boolean).join(", ").split(/[,;/|]/).map(x=>x.trim()).find(x=>x.toLowerCase()===n)||n;display&&!roster.some(r=>r.toLowerCase()===display.toLowerCase())&&roster.push(display)})}));const newDetailed=[...map.values()].filter(s=>CS_DETAIL_FIELDS.some(f=>(s[f]||"").trim())).length,newCharLabel=knownTotal?`${knownTotal} cast characters queued \xB7 ${newDetailed} profiles with details`:`${map.size} characters found`;prog.update(i+1,`Passage ${i+1} / ${chunks.length}\u2026`,newCharLabel,csProgressSnapshot(map,changed),[...map.values()]);break}catch(e){const msg=e.message||String(e),isRateLimit=/429|too many requests|rate.?limit/i.test(msg),isNetworkError=/failed to fetch|network\s*error|load failed|timed?\s*out|timeout|\b50[234]\b|econnreset|econnrefused|socket hang up|connection (refused|reset|closed)/i.test(msg);if((isRateLimit||isNetworkError)&&attemptsetTimeout(res,waitMs));continue}console.error("Character sheets passage",i+1,"failed:",e),toast("Passage "+(i+1)+" failed: "+msg,"error");break}}}finally{prog.done()}if(_cs.cancel)return toast("Cancelled","error"),null;csConsolidateAliasDuplicates(map,establishedKeys,establishedCreated);const sheets=[...map.values()];return cacheKey&&(_cs.cache[cacheKey]=sheets),sheets}catch(e){return console.error("[character sheets] generation crashed:",e),toast("Character sheet generation crashed: "+(e&&e.message?e.message:String(e)),"error"),null}finally{_cs.running=!1}}function csProgress(total,hostEl=null){var _a2,_b2;const usingHost=!!hostEl;let root=usingHost?hostEl:document.getElementById("cs-progress");if(!root){if(usingHost)return null;root=document.createElement("div"),root.id="cs-progress",root.className="audiobook-overlay",document.body.appendChild(root)}if(!root.querySelector("#cs-progress-fill")){usingHost&&(root.innerHTML=""),root.innerHTML=`
+`))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&onDelta&&onDelta(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}async function csGenerate(text,cacheKey,initialRoster,opts={}){if(_cs.running)return null;if(!text)return toast("Nothing to analyse","error"),null;const chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,CS_CHUNK_CHARS):[text];_cs.running=!0,_cs.cancel=!1;try{const prog=csProgress(chunks.length,opts.pageHost||null),llm_url=csLlmUrl(),model=csLlmModel();let language=csLang();if(typeof detectLang=="function"){const detected=detectLang(text);if(detected&&(!language||/^auto$/i.test(language)||language.toLowerCase()==="english"&&detected.toLowerCase()!=="english")){language=detected;const sel=$("reh-design-lang");if(sel){const o=[...sel.options].find(x=>x.value.toLowerCase()===detected.toLowerCase()||x.textContent.toLowerCase()===detected.toLowerCase());o&&(sel.value=o.value)}}}const map=new Map,lineCounts=opts.lineCounts instanceof Map?opts.lineCounts:new Map,seedSheets=Array.isArray(opts.seedSheets)?opts.seedSheets:[],establishedKeys=new Set,establishedCreated=new Map;seedSheets.forEach(seedRaw=>{const seed=csSeedSheet(seedRaw),name=String(seed.name||"").trim();if(!name)return;const lc=lineCounts.get(name.toLowerCase());lc!=null&&(seed.line_count=lc),map.set(name.toLowerCase(),seed),establishedKeys.add(name.toLowerCase());const created=seedRaw!=null&&seedRaw.created?new Date(seedRaw.created).getTime():NaN;isNaN(created)||establishedCreated.set(name.toLowerCase(),created)});const roster=csCleanRoster(initialRoster),targetMode=roster.length>0,knownTotal=targetMode?roster.length:null;roster.forEach(name=>{const key=name.toLowerCase();if(!map.has(key)){const blank=csBlankSheet(name),lc=lineCounts.get(key);lc!=null&&(blank.line_count=lc),map.set(key,blank)}});try{for(let i=0;iCS_DETAIL_FIELDS.some(f=>(s[f]||"").trim())).length,charLabel=knownTotal?`${knownTotal} cast characters queued \xB7 ${detailed} profiles with details`:`${map.size} characters found`;prog.update(i,`Passage ${i+1} / ${chunks.length}\u2026`,charLabel,csProgressSnapshot(map),[...map.values()]),prog.startPassage(chunks[i]);const maxAttempts=4;for(let attempt=1;attempt<=maxAttempts;attempt++)try{const character_sheets_prompt=typeof _appSettings!="undefined"&&_appSettings.character_sheets_prompt||CS_DEFAULT_PROMPT,body={text:chunks[i],known_characters:roster.slice(0,120),target_mode:targetMode,existing:csExistingSummary(map),language,llm_url,model,character_sheets_prompt};let data=null,sawDelta=!1;try{data=await csGenerateStream(body,delta=>{sawDelta=!0,prog.thinking(delta)}),sawDelta||prog.noStream()}catch{sawDelta||prog.noStream();const r=await fetch("/api/character-sheets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){let detail=`HTTP ${r.status}`;try{const e=await r.json();detail=e.detail||e.error||detail}catch{}throw new Error(detail)}const raw=await r.text();try{data=JSON.parse(raw)}catch{throw new Error("Invalid JSON from server (passage may be too large)")}}const incomingSheets=targetMode?(data.sheets||[]).filter(s=>csSheetMatchesRoster(s,roster)):data.sheets||[];incomingSheets.forEach(s=>{const ownName=String(s.name||"").trim().toLowerCase();["first_name","last_name","full_name"].forEach(f=>{const v=String(s[f]||"").trim().toLowerCase();v&&v!==ownName&&roster.some(n=>n.toLowerCase()===v)&&(console.warn(`[character sheets] dropped ${f}="${s[f]}" from "${s.name}" \u2014 matches a different known character`),s[f]="")})});const changed=csMerge(map,incomingSheets);changed.length&&prog.focus(changed[0].name),targetMode||((data.characters||[]).forEach(n=>{roster.some(r=>r.toLowerCase()===String(n||"").toLowerCase())||roster.push(n)}),(data.sheets||[]).forEach(s=>{csNameTokens(s).forEach(n=>{const display=[s.name,s.title,s.profession,s.aliases].filter(Boolean).join(", ").split(/[,;/|]/).map(x=>x.trim()).find(x=>x.toLowerCase()===n)||n;display&&!roster.some(r=>r.toLowerCase()===display.toLowerCase())&&roster.push(display)})}));const newDetailed=[...map.values()].filter(s=>CS_DETAIL_FIELDS.some(f=>(s[f]||"").trim())).length,newCharLabel=knownTotal?`${knownTotal} cast characters queued \xB7 ${newDetailed} profiles with details`:`${map.size} characters found`;prog.update(i+1,`Passage ${i+1} / ${chunks.length}\u2026`,newCharLabel,csProgressSnapshot(map,changed),[...map.values()]);break}catch(e){const msg=e.message||String(e),isRateLimit=/429|too many requests|rate.?limit/i.test(msg),isNetworkError=/failed to fetch|network\s*error|load failed|timed?\s*out|timeout|\b50[234]\b|econnreset|econnrefused|socket hang up|connection (refused|reset|closed)/i.test(msg);if((isRateLimit||isNetworkError)&&attemptsetTimeout(res,waitMs));continue}console.error("Character sheets passage",i+1,"failed:",e),toast("Passage "+(i+1)+" failed: "+msg,"error");break}}}finally{prog.done()}if(_cs.cancel)return toast("Cancelled","error"),null;csConsolidateAliasDuplicates(map,establishedKeys,establishedCreated,roster);const sheets=csCanonicalizeRosterNames([...map.values()],roster);return cacheKey&&(_cs.cache[cacheKey]=sheets),sheets}catch(e){return console.error("[character sheets] generation crashed:",e),toast("Character sheet generation crashed: "+(e&&e.message?e.message:String(e)),"error"),null}finally{_cs.running=!1}}function csProgress(total,hostEl=null){var _a2,_b2;const usingHost=!!hostEl;let root=usingHost?hostEl:document.getElementById("cs-progress");if(!root){if(usingHost)return null;root=document.createElement("div"),root.id="cs-progress",root.className="audiobook-overlay",document.body.appendChild(root)}if(!root.querySelector("#cs-progress-fill")){usingHost&&(root.innerHTML=""),root.innerHTML=`
Character sheets @@ -1652,7 +1652,7 @@ Respond with STRICT JSON only:
${arcNote?`
${escHtml(arc.label)} \xB7 ${escHtml(arcNote)}
`:`
${escHtml(arc.label)}
`} -
`}function csBuildImagePrompt(s,bookProfile){var _a2;if(_csStr(s.image_prompt).trim())return _csStr(s.image_prompt).trim();const parts=[];s.race_species&&parts.push(s.race_species),s.archetype&&parts.push(s.archetype),s.physical&&parts.push(s.physical),s.clothing&&parts.push(s.clothing),s.alignment&&parts.push(s.alignment);const pct=(_a2=s.moral_alignment_score)!=null?_a2:50;parts.push(pct>=70?"benevolent expression":pct<=30?"dark and menacing presence":"ambiguous expression"),s.arc_direction==="bad-to-good"&&parts.push("redemptive aura"),s.arc_direction==="good-to-bad"&&parts.push("ominous aura, turning to darkness");const bp=bookProfile||{},settingBits=[bp.genre,bp.setting,bp.era].filter(Boolean),settingClause=settingBits.length?`This character belongs to the following book/world: ${settingBits.join(", ")}. Every visual choice \u2014 architecture, clothing materials, weaponry, ethnicity mix, technology level \u2014 must fit THAT world, not a generic modern or real-world default. `:"";return`Create a complete character reference sheet for an original character named ${s.name}, ${parts.filter(Boolean).join(", ")}. `+settingClause+`Base the setting, era, and art style strictly on the character's own described archetype and clothing above \u2014 do not default to a modern or real-world 20th/21st-century look for occupation-sounding titles (e.g. an "Admiral" or "General" in a fantasy/period setting should NOT be drawn in a contemporary military uniform); every visual choice should fit the world implied by the description, not the real one, unless the description itself is explicitly modern/contemporary. Include a full-body front view as the anchor, a turnaround panel (side and back views), an expression sheet with 3-5 headshots matching their personality, a color palette swatch for hair/eyes/outfit, and labeled callouts for signature props or clothing details. Clean production concept-art layout, plain neutral background, original character not based on any copyrighted character.`}function csBuildVoicePrompt(s){var _a2;if(_csStr(s.voice_design_prompt).trim())return _csStr(s.voice_design_prompt).trim();const parts=[];s.voice_pattern&&parts.push(s.voice_pattern),s.mannerisms&&parts.push(s.mannerisms),s.archetype&&parts.push(`archetype: ${s.archetype}`);const pct=(_a2=s.moral_alignment_score)!=null?_a2:50;return pct>=70?parts.push("warm, trustworthy tone"):pct<=30?parts.push("cold, threatening or sinister tone"):parts.push("neutral, measured tone"),parts.filter(Boolean).join(". ")}async function csDeepAnalysis(sheet,sourceText){const llm_url=csLlmUrl(),model=csLlmModel(),language=csLang(),modal=document.createElement("div");modal.className="audiobook-overlay",modal.innerHTML=`
+
`}function csBuildImagePrompt(s,bookProfile){var _a2;if(_csStr(s.image_prompt).trim())return _csStr(s.image_prompt).trim();const parts=[];s.gender&&parts.push(`exactly ${s.gender}`),s.race_species&&parts.push(s.race_species),s.archetype&&parts.push(s.archetype),s.physical&&parts.push(s.physical),s.clothing&&parts.push(s.clothing),s.alignment&&parts.push(s.alignment);const pct=(_a2=s.moral_alignment_score)!=null?_a2:50;parts.push(pct>=70?"benevolent expression":pct<=30?"dark and menacing presence":"ambiguous expression"),s.arc_direction==="bad-to-good"&&parts.push("redemptive aura"),s.arc_direction==="good-to-bad"&&parts.push("ominous aura, turning to darkness");const bp=bookProfile||{},settingBits=[bp.genre,bp.setting,bp.era].filter(Boolean),settingClause=settingBits.length?`This character belongs to the following book/world: ${settingBits.join(", ")}. Every visual choice \u2014 architecture, clothing materials, weaponry, ethnicity mix, technology level \u2014 must fit THAT world, not a generic modern or real-world default. `:"";return`Create a complete character reference sheet for an original character named ${s.name}, ${parts.filter(Boolean).join(", ")}. VISUAL IDENTITY LOCK: use the exact gender, age impression, species, face shape, skin tone, hair, eyes, facial hair, distinguishing marks, build, clothing, colors, armor, jewelry, weapons, tools, and signature props established here. Keep one identical face identity anchor across the portrait, front/side/back turnaround, expression sheet, and later concept art. `+settingClause+`Base the setting, era, and art style strictly on the character's own described archetype and clothing above \u2014 do not default to a modern or real-world 20th/21st-century look for occupation-sounding titles (e.g. an "Admiral" or "General" in a fantasy/period setting should NOT be drawn in a contemporary military uniform); every visual choice should fit the world implied by the description, not the real one, unless the description itself is explicitly modern/contemporary. Include a full-body front view as the anchor, a turnaround panel (side and back views), an expression sheet with 3-5 headshots matching their personality, a color palette swatch for hair/eyes/outfit, and labeled callouts for signature props or clothing details. Clean production concept-art layout, plain neutral background, original character not based on any copyrighted character.`}function csBuildVoicePrompt(s){var _a2;if(_csStr(s.voice_design_prompt).trim())return _csStr(s.voice_design_prompt).trim();const parts=[];s.voice_pattern&&parts.push(s.voice_pattern),s.mannerisms&&parts.push(s.mannerisms),s.archetype&&parts.push(`archetype: ${s.archetype}`);const pct=(_a2=s.moral_alignment_score)!=null?_a2:50;return pct>=70?parts.push("warm, trustworthy tone"):pct<=30?parts.push("cold, threatening or sinister tone"):parts.push("neutral, measured tone"),parts.filter(Boolean).join(". ")}async function csDeepAnalysis(sheet,sourceText){const llm_url=csLlmUrl(),model=csLlmModel(),language=csLang(),modal=document.createElement("div");modal.className="audiobook-overlay",modal.innerHTML=`
Deep Analysis \u2014 ${escHtml(sheet.name)} @@ -1787,7 +1787,7 @@ Respond with STRICT JSON only:
${group("Main characters",main)}${group("Supporting characters",supp)}
-
`,document.body.appendChild(ov),ov.addEventListener("click",e=>{e.target===ov&&ov.remove()})),csWireResultInteractions(ov,sheets,sourceText,book,inline),typeof _wireCharCards=="function"){const listEl=ov.querySelector(".cs-list")||ov;_wireCharCards(ov,recsById,allRecs,null,{container:listEl,onBack:()=>csShow(sheets,title,sourceText,book,hostEl)})}}async function csSaveToLibrary(book,sheets){if(typeof clUpsertMany=="function")try{const n=await clUpsertMany(book,sheets);n&&toast(`${n} character${n!==1?"s":""} saved to library`,"success")}catch{}}async function csAutoGenerateExternalPrompts(book,sheets){if(!Array.isArray(sheets)||!sheets.length)return;const target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""};let done=0,failed=0,repeatMsg="",repeatCount=0;toast(`Generating SillyTavern + Concept Art prompts for ${sheets.length} character${sheets.length!==1?"s":""}\u2026`,"info");for(const s of sheets)if(!(!s||!s.name))try{const sample=[s.physical,s.backstory,s.motivation].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:s.name,book,sheet:s,language,llm_url:target.url,model:target.model,fields:["silly_tavern_prompt","concept_art_prompt"]})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();d.silly_tavern_prompt&&(s.silly_tavern_prompt=d.silly_tavern_prompt),d.concept_art_prompt&&(s.concept_art_prompt=d.concept_art_prompt),done++,repeatCount=0}catch(e){failed++;const msg=e&&e.message?e.message:String(e);if(console.error("[auto external prompts]",s.name,e),msg===repeatMsg?repeatCount++:(repeatMsg=msg,repeatCount=1),repeatCount>=3)break}try{await clUpsertMany(book,sheets)}catch{}const suffix=failed?` (${failed} failed${repeatMsg?": "+repeatMsg.slice(0,160):""})`:"";if(toast(`External prompts generated for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),typeof _charAutoGenerateConceptArt!="function")return;const withPrompt=sheets.filter(s=>s&&s.name&&_libStr(s.concept_art_prompt).trim());if(!withPrompt.length)return;let imgDone=0,imgFailed=0,imgRepeatMsg="",imgRepeatCount=0;toast(`Generating concept art for ${withPrompt.length} character${withPrompt.length!==1?"s":""}\u2026`,"info");for(const s of withPrompt)try{await _charAutoGenerateConceptArt({id:clKey(book,s.name),sheet:s}),imgDone++,imgRepeatCount=0}catch(e){imgFailed++;const msg=e&&e.message?e.message:String(e);if(console.error("[auto concept art]",s.name,e),msg===imgRepeatMsg?imgRepeatCount++:(imgRepeatMsg=msg,imgRepeatCount=1),imgRepeatCount>=3)break}const imgSuffix=imgFailed?` (${imgFailed} failed${imgRepeatMsg?": "+imgRepeatMsg.slice(0,160):""})`:"";toast(`Concept art generated for ${imgDone} character${imgDone!==1?"s":""}${imgSuffix}`,imgFailed&&!imgDone?"error":"success")}function csGoToLibrary(){typeof navTo=="function"&&navTo("s-library"),typeof navLibraryView=="function"?navLibraryView("characters"):typeof libraryRender=="function"&&libraryRender("characters"),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib")}function csAttachLineCounts(sheets,counts){!counts||!counts.size||sheets.forEach(function(s){const n=counts.get(String(s.name||"").trim().toLowerCase());n!=null&&(s.line_count=n)})}async function csForReader(opts={}){var _a2;const fresh=!!opts.fresh,text=csReaderText(),book=readerState.title||"Untitled book",key="reader:"+(readerState.title||"")+":"+(typeof readerScopeIndices=="function"?readerScopeIndices().length:0);typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();if(!fresh&&_cs.cache[key]){await csSaveToLibrary(book,_cs.cache[key]),pageHost&&await csShow(_cs.cache[key],book,text,book,pageHost);return}const knownRoster=csKnownReaderRoster();let seedSheets=[];if(!fresh)try{if(typeof clGetAllByTagOrBook=="function"){const existing=await clGetAllByTagOrBook(book),rosterSet=new Set(knownRoster.map(n=>n.toLowerCase())),stillCast=rec=>!rosterSet.size||rosterSet.has(String((rec==null?void 0:rec.name)||"").trim().toLowerCase());seedSheets=(existing||[]).filter(stillCast).map(rec=>csSeedSheet(rec)).filter(s=>String(s.name||"").trim())}}catch{seedSheets=[]}const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook,lineCounts=new Map;if((_a2=ab==null?void 0:ab.segments)!=null&&_a2.length)for(const s of ab.segments){if((s==null?void 0:s.type)!=="dialogue"||!s.speaker)continue;const k=String(s.speaker).trim().toLowerCase();lineCounts.set(k,(lineCounts.get(k)||0)+1)}const sheets=await csGenerate(text,key,knownRoster,{pageHost,seedSheets,lineCounts});if(sheets){if(!sheets.length){toast("No characters found","error");return}lineCounts.size&&csAttachLineCounts(sheets,lineCounts),await csSaveToLibrary(book,sheets),pageHost&&await csShow(sheets,book,text,book,pageHost),toast(sheets.length+" character sheets saved \u2014 Library \u2192 Cast","success"),csAutoGenerateExternalPrompts(book,sheets)}}async function csForReaderSelective(selectedNames){var _a2;const wanted=new Set((selectedNames||[]).map(n=>String(n).trim().toLowerCase()));if(!wanted.size){toast("No characters selected","error");return}const text=csReaderText(),book=readerState.title||"Untitled book";typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();let records=[];try{records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(book):[]}catch{records=[]}const picked=[],seen=new Set,matchesWanted=rec=>{var _a3,_b2,_c2,_d2;const tokens=csNameTokens({name:(rec==null?void 0:rec.name)||"",aliases:((_a3=rec==null?void 0:rec.sheet)==null?void 0:_a3.aliases)||"",first_name:((_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name)||"",last_name:((_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name)||"",full_name:((_d2=rec==null?void 0:rec.sheet)==null?void 0:_d2.full_name)||""});for(const t of tokens){const lower=String(t||"").toLowerCase();for(const w of wanted)if(lower===w||lower.includes(w)||w.includes(lower))return!0}return!1};if((records||[]).forEach(rec=>{if(!rec||!rec.name||!matchesWanted(rec))return;const key=String(rec.name||"").trim().toLowerCase();seen.has(key)||(seen.add(key),picked.push(rec))}),(selectedNames||[]).forEach(name=>{const key=String(name||"").trim().toLowerCase();if(!key||seen.has(key))return;const fallback={name:String(name||"").trim(),sheet:csBlankSheet(String(name||"").trim())};seen.add(key),picked.push(fallback)}),picked.sort((a,b)=>{var _a3,_b2;return(((_a3=b==null?void 0:b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a==null?void 0:a.sheet)==null?void 0:_b2.line_count)||0)||String((a==null?void 0:a.name)||"").localeCompare(String((b==null?void 0:b.name)||""))}),!picked.length){toast("No matching cast characters found","error");return}const finalMap=new Map;for(const rec of picked){const targetName=String(rec.name||"").trim();if(!targetName)continue;const needles=csRecordNeedles(rec),scanText=csEvidenceWindowText(csReaderParagraphBlocks(),needles,2,2)||text,seedSheet=csSeedSheet(rec),sheets=await csGenerate(scanText,null,[targetName],{pageHost,seedSheets:[seedSheet]});if(!sheets)return;const filtered2=sheets.filter(s=>wanted.has(String(s.name||"").trim().toLowerCase()));filtered2.length&&(((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.line_count)!=null&&filtered2.forEach(s=>{s.line_count=rec.sheet.line_count}),csMerge(finalMap,filtered2))}const filtered=[...finalMap.values()];if(!filtered.length){toast("None of the selected characters turned up in this pass \u2014 try again or pick different ones","error");return}const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook;if(ab!=null&&ab.roster){const counts=new Map;ab.roster.forEach(function(info,name){counts.set(String(name).trim().toLowerCase(),info.count||0)}),csAttachLineCounts(filtered,counts)}await csSaveToLibrary(book,filtered),pageHost&&await csShow(filtered,book,text,book,pageHost),toast(filtered.length+" character"+(filtered.length!==1?"s":"")+" defined","success")}async function csForRehearser(){var _a2,_b2,_c2,_d2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Character sheets",book=((_b2=$("reh-script-title"))==null?void 0:_b2.value.trim())||"Untitled script",text=csRehearserText(),key="reh:"+title+":"+(rehState.lines||[]).length;typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();if(_cs.cache[key]){await csSaveToLibrary(book,_cs.cache[key]),pageHost&&await csShow(_cs.cache[key],title,text,book,pageHost);return}let seedSheets=[];try{typeof clGetAllByTagOrBook=="function"&&(seedSheets=(await clGetAllByTagOrBook(book)||[]).map(rec=>csSeedSheet(rec)).filter(s=>String(s.name||"").trim()))}catch{seedSheets=[]}const sheets=await csGenerate(text,key,null,{pageHost,seedSheets});if(sheets){if(!sheets.length){toast("No characters found","error");return}if((_d2=(_c2=window.rehState)==null?void 0:_c2.lines)!=null&&_d2.length){const counts=new Map;rehState.lines.forEach(function(l){if(l.type!=="dialog"||!l.speaker)return;const k=String(l.speaker).trim().toLowerCase();counts.set(k,(counts.get(k)||0)+1)}),csAttachLineCounts(sheets,counts)}await csSaveToLibrary(book,sheets),pageHost&&await csShow(sheets,title,text,book,pageHost),toast(sheets.length+" character sheets saved \u2014 Library \u2192 Cast","success")}}window.csForReader=csForReader,window.csForReaderSelective=csForReaderSelective,window.csForRehearser=csForRehearser,(_rc=$("reader-charsheets-btn"))==null||_rc.addEventListener("click",csForReader),(_sc=$("reh-charsheets-btn"))==null||_sc.addEventListener("click",csForRehearser);const CL_EDIT_FIELDS=[["name","Name"],["aliases","Aliases / also known as"],["first_name","First name"],["last_name","Last name"],["title","Title / role"],["age_estimate","Estimated age"],["race_species","Race / species"],["languages","Languages"],["nationality_background","Nationality / background"],["social_class","Social class"],["archetype","Archetype"],["physical","Physical"],["clothing","Clothing & Appearance"],["alignment","Alignment & Ethos"],["arc_note","Arc note"],["skills","Trained Skills"],["capabilities","Capabilities"],["backstory","Backstory & Origin"],["relationships","Relationships"],["motivation","Motivation"],["fears","Fears"],["mannerisms","Mannerisms & Habits"],["communication_style","Communication style"],["reputation","Reputation"],["religious_beliefs","Religious beliefs"],["notes","Notes"],["voice_pattern","Voice & Speech"],["voice_design_prompt","Voice Design Prompt"],["image_prompt","Image Generation Prompt"],["silly_tavern_prompt","SillyTavern Character Prompt"],["concept_art_prompt","Concept Art Prompt"],["secret","Dark Secret / Fatal Flaw"],["conflict_style","Conflict Style"],["win_condition","Win Condition"]];async function clGetAll(){const r=await fetch("/api/characters");if(!r.ok)throw new Error("clGetAll failed: "+r.status);return(await r.json()).characters||[]}async function clGet(id){const r=await fetch("/api/characters/"+encodeURIComponent(id));if(r.status!==404){if(!r.ok)throw new Error("clGet failed: "+r.status);return r.json()}}async function clPut(rec){rec!=null&&rec.color&&(rec.sheet=rec.sheet||{},rec.sheet.color=clNormalizeColor(rec.color,rec.name));const r=await fetch("/api/characters/"+encodeURIComponent(rec.id),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(rec)});if(!r.ok)throw new Error("clPut failed: "+r.status);const saved=await r.json();return typeof window._rehSyncCastVoiceFromLibrary=="function"&&window._rehSyncCastVoiceFromLibrary(saved),saved}async function clDelete(id){const r=await fetch("/api/characters/"+encodeURIComponent(id),{method:"DELETE"});if(!r.ok)throw new Error("clDelete failed: "+r.status)}function clKey(book,name){return`${String(book||"").trim()}::${String(name||"").trim()}`.toLowerCase()}function clHslToHex(h,s,l){s/=100,l/=100;const k=n=>(n+h/30)%12,a=s*Math.min(l,1-l),f=n=>l-a*Math.max(-1,Math.min(k(n)-3,Math.min(9-k(n),1)));return"#"+[f(0),f(8),f(4)].map(x=>Math.round(255*x).toString(16).padStart(2,"0")).join("")}function clNameHue(name){return Math.abs((name||"?").split("").reduce((h,c)=>(h*31+c.charCodeAt(0))%360,0))}function clNormalizeColor(color,name){const c=String(color||"").trim();return/^#[0-9a-f]{6}$/i.test(c)?c:/^#[0-9a-f]{3}$/i.test(c)?"#"+c.slice(1).split("").map(ch=>ch+ch).join(""):clHslToHex(clNameHue(name),58,43)}const CL_IDENTITY_FIELDS=["name","aliases","first_name","last_name","full_name"],CL_ALIAS_MAX_TOKENS=12,CL_ALIAS_MAX_CHARS=500,CL_ALIAS_STOPWORDS=new Set(["die","der","das","den","dem","des","ein","eine","einer","er","sie","es","ich","du","wir","ihr","the","a","an","he","she","it","they","who"]);function clSplitIdentityTokens(v,opts={}){const raw=_clStr(v),parts=raw.split(/[,;/|]|\baka\b|\baka\.\b|\balias(?:es)?\b|\bgenannt\b|\bnamens\b|\bcalled\b|\bknown as\b/i).map(x=>x.trim()).filter(Boolean).filter(x=>x.length<=80&&!/^needs?:/i.test(x)&&!/^complete$/i.test(x)).filter(x=>!CL_ALIAS_STOPWORDS.has(x.toLowerCase()));return opts.aliases&&(raw.length>CL_ALIAS_MAX_CHARS||parts.length>CL_ALIAS_MAX_TOKENS)?[]:parts.slice(0,opts.aliases?CL_ALIAS_MAX_TOKENS:void 0)}function clIdentityNames(recOrSheet){const s=(recOrSheet==null?void 0:recOrSheet.sheet)||recOrSheet||{},out=new Set,add=(v,opts={})=>clSplitIdentityTokens(v,opts).forEach(x=>out.add(x.toLowerCase()));return add((recOrSheet==null?void 0:recOrSheet.name)||s.name),CL_IDENTITY_FIELDS.filter(k=>k!=="name").forEach(k=>add(s[k],{aliases:k==="aliases"})),out}function clMergeAliases(existing,incoming){const names=new Map,add=v=>clSplitIdentityTokens(v,{aliases:!0}).forEach(x=>names.set(x.toLowerCase(),x));return add(existing.aliases),add(incoming.aliases),incoming.name&&incoming.name!==existing.name&&add(incoming.name),[...names.values()].filter(n=>n.toLowerCase()!==String(existing.name||"").toLowerCase()).join(", ")}function clSameIdentity(rec,book,sheet){if(String((rec==null?void 0:rec.book)||"").trim().toLowerCase()!==String(book||"").trim().toLowerCase())return!1;const a=clIdentityNames(rec),b=clIdentityNames(sheet);for(const n of b)if(a.has(n))return!0;return!1}function _clStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _clSanitize(sheet){const out={...sheet};return(typeof CS_SCALAR_FIELDS!="undefined"?CS_SCALAR_FIELDS:CL_EDIT_FIELDS.map(f=>f[0]).filter(k=>k!=="name")).forEach(f=>{out[f]!=null&&(out[f]=_clStr(out[f]))}),out}function clMergeSheet(existing,incoming){const e={...existing},inc=_clSanitize(incoming),scalars=typeof CS_SCALAR_FIELDS!="undefined"?CS_SCALAR_FIELDS:CL_EDIT_FIELDS.map(f=>f[0]).filter(k=>k!=="name"),oldAliases=e.aliases;return scalars.forEach(f=>{(inc[f]||"").length>(e[f]||"").length&&(e[f]=inc[f])}),e.aliases=clMergeAliases({...e,aliases:oldAliases},incoming),incoming.tier==="main"&&(e.tier="main"),incoming.moral_alignment_score!=null&&(e.moral_alignment_score=e.moral_alignment_score!=null?Math.round((e.moral_alignment_score+incoming.moral_alignment_score)/2):incoming.moral_alignment_score),incoming.arc_direction&&incoming.arc_direction!=="neutral"&&(e.arc_direction=incoming.arc_direction),incoming.gender&&!e.gender&&(e.gender=incoming.gender),incoming.line_count!=null&&(e.line_count=incoming.line_count),e.inventory=[...existing.inventory||[]],(incoming.inventory||[]).forEach(it=>{it&&!e.inventory.includes(it)&&e.inventory.length<3&&e.inventory.push(it)}),e.sources=[...existing.sources||[]],(incoming.sources||[]).forEach(src=>{src&&src.quote&&e.sources.length<12&&!e.sources.some(x=>x.quote===src.quote)&&e.sources.push(src)}),e}function clMergeTags(...parts){const set=new Set;return parts.forEach(p=>String(p||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>set.add(t))),[...set].join(", ")}async function clUpsert(book,sheet,knownId){var _a2;const name=(sheet.name||"").trim();if(!name)return null;const bk=(book||"").trim()||"Unsorted",aliasPrev=knownId?await clGet(knownId).catch(()=>null):(await clGetAll().catch(()=>[])).find(r=>clSameIdentity(r,bk,sheet)),id=(aliasPrev==null?void 0:aliasPrev.id)||knownId||clKey(bk,name),now=new Date,prev=aliasPrev||await clGet(id).catch(()=>null),merged=prev?clMergeSheet(prev.sheet||{},sheet):{..._clSanitize(sheet),name},canonicalName=(prev==null?void 0:prev.name)||name;merged.name=canonicalName;const tags=clMergeTags(prev==null?void 0:prev.tags,sheet.tags,bk),color=clNormalizeColor((prev==null?void 0:prev.color)||((_a2=prev==null?void 0:prev.sheet)==null?void 0:_a2.color)||sheet.color,canonicalName);merged.color=color;const rec={id,book:bk,name:canonicalName,tags,sheet:merged,color,analysis:(prev==null?void 0:prev.analysis)||null,voice:sheet.voice||(prev==null?void 0:prev.voice)||null,image:sheet.image||(prev==null?void 0:prev.image)||null,created:(prev==null?void 0:prev.created)||now,updated:now};return await clPut(rec),rec}async function clSetImage(id,dataUrl){const rec=await clGet(id).catch(()=>null);if(rec)return rec.image=dataUrl||null,rec.updated=new Date,await clPut(rec),rec}window.clSetImage=clSetImage;async function clGetAllByTagOrBook(title){const key=String(title||"").trim().toLowerCase();return key?(await clGetAll().catch(()=>[])).filter(r=>String(r.book||"").trim().toLowerCase()===key?!0:String(r.tags||"").split(",").some(t=>t.trim().toLowerCase()===key)):[]}async function clUpsertMany(book,sheets){let n=0;for(const s of sheets||[])await clUpsert(book,s)&&n++;return n}(async function(){try{if((await clGetAll()).length>0)return;const idbRecs=await _clIdbGetAll().catch(()=>[]);if(!idbRecs.length)return;const r=await fetch("/api/characters/migrate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(idbRecs)});if(r.ok){const d=await r.json();console.log(`[characters-library] migrated ${d.imported} records from IndexedDB \u2192 SQLite`)}}catch(e){console.warn("[characters-library] migration skipped:",e)}})();function _clIdbGetAll(){return new Promise((resolve,reject)=>{const req=indexedDB.open("character-library",1);req.onerror=()=>resolve([]),req.onsuccess=e=>{const db=e.target.result;if(!db.objectStoreNames.contains("characters")){db.close(),resolve([]);return}const all=db.transaction("characters","readonly").objectStore("characters").getAll();all.onsuccess=ev=>{db.close(),resolve(ev.target.result||[])},all.onerror=()=>{db.close(),resolve([])}}})}let _clRecords=[];async function clRender(){if(!document.getElementById("cl-grid"))return;const filterEl=document.getElementById("cl-book-filter"),searchEl=document.getElementById("cl-search");filterEl&&!filterEl.dataset.bound&&(filterEl.dataset.bound="1",filterEl.addEventListener("change",clApplyFilter)),searchEl&&!searchEl.dataset.bound&&(searchEl.dataset.bound="1",searchEl.addEventListener("input",clApplyFilter));try{_clRecords=await clGetAll()}catch{_clRecords=[]}const filter=document.getElementById("cl-book-filter"),prods=clAllProductions();if(filter){const cur=filter.value;filter.innerHTML=``+prods.map(b=>``).join(""),cur&&prods.includes(cur)&&(filter.value=cur)}clApplyFilter()}function clAllProductions(){const set=new Set;return _clRecords.forEach(r=>{r.book&&set.add(r.book),String(r.tags||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>set.add(t))}),[...set].sort((a,b)=>a.localeCompare(b))}function clApplyFilter(){var _a2,_b2;const grid=document.getElementById("cl-grid");if(!grid)return;const book=((_a2=document.getElementById("cl-book-filter"))==null?void 0:_a2.value)||"",q=(((_b2=document.getElementById("cl-search"))==null?void 0:_b2.value)||"").trim().toLowerCase();let recs=_clRecords.slice();if(book){const bk=book.toLowerCase();recs=recs.filter(r=>(r.book||"").toLowerCase()===bk||String(r.tags||"").split(",").some(t=>t.trim().toLowerCase()===bk))}if(q&&(recs=recs.filter(r=>{var _a3,_b3,_c2,_d2,_e2;return(r.name||"").toLowerCase().includes(q)||(((_a3=r.sheet)==null?void 0:_a3.aliases)||"").toLowerCase().includes(q)||(((_b3=r.sheet)==null?void 0:_b3.first_name)||"").toLowerCase().includes(q)||(((_c2=r.sheet)==null?void 0:_c2.last_name)||"").toLowerCase().includes(q)||(((_d2=r.sheet)==null?void 0:_d2.title)||"").toLowerCase().includes(q)||(((_e2=r.sheet)==null?void 0:_e2.archetype)||"").toLowerCase().includes(q)||(r.tags||"").toLowerCase().includes(q)||(r.book||"").toLowerCase().includes(q)})),!recs.length){grid.innerHTML=`
+
`,document.body.appendChild(ov),ov.addEventListener("click",e=>{e.target===ov&&ov.remove()})),csWireResultInteractions(ov,sheets,sourceText,book,inline),typeof _wireCharCards=="function"){const listEl=ov.querySelector(".cs-list")||ov;_wireCharCards(ov,recsById,allRecs,null,{container:listEl,onBack:()=>csShow(sheets,title,sourceText,book,hostEl)})}}async function csSaveToLibrary(book,sheets){if(typeof clUpsertMany=="function")try{const n=await clUpsertMany(book,sheets);n&&toast(`${n} character${n!==1?"s":""} saved to library`,"success")}catch{}}async function csAutoGenerateExternalPrompts(book,sheets){if(!Array.isArray(sheets)||!sheets.length)return;const target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""};let done=0,failed=0,repeatMsg="",repeatCount=0;toast(`Generating SillyTavern + Concept Art prompts for ${sheets.length} character${sheets.length!==1?"s":""}\u2026`,"info");for(const s of sheets)if(!(!s||!s.name))try{const sample=[s.physical,s.backstory,s.motivation].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:s.name,book,sheet:s,language,llm_url:target.url,model:target.model,fields:["silly_tavern_prompt","concept_art_prompt"]})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();d.silly_tavern_prompt&&(s.silly_tavern_prompt=d.silly_tavern_prompt),d.concept_art_prompt&&(s.concept_art_prompt=d.concept_art_prompt),done++,repeatCount=0}catch(e){failed++;const msg=e&&e.message?e.message:String(e);if(console.error("[auto external prompts]",s.name,e),msg===repeatMsg?repeatCount++:(repeatMsg=msg,repeatCount=1),repeatCount>=3)break}try{await clUpsertMany(book,sheets)}catch{}const suffix=failed?` (${failed} failed${repeatMsg?": "+repeatMsg.slice(0,160):""})`:"";if(toast(`External prompts generated for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),typeof _charAutoGenerateConceptArt!="function")return;const withPrompt=sheets.filter(s=>s&&s.name&&_libStr(s.concept_art_prompt).trim());if(!withPrompt.length)return;let imgDone=0,imgFailed=0,imgRepeatMsg="",imgRepeatCount=0;toast(`Generating concept art for ${withPrompt.length} character${withPrompt.length!==1?"s":""}\u2026`,"info");for(const s of withPrompt)try{await _charAutoGenerateConceptArt({id:clKey(book,s.name),sheet:s}),imgDone++,imgRepeatCount=0}catch(e){imgFailed++;const msg=e&&e.message?e.message:String(e);if(console.error("[auto concept art]",s.name,e),msg===imgRepeatMsg?imgRepeatCount++:(imgRepeatMsg=msg,imgRepeatCount=1),imgRepeatCount>=3)break}const imgSuffix=imgFailed?` (${imgFailed} failed${imgRepeatMsg?": "+imgRepeatMsg.slice(0,160):""})`:"";toast(`Concept art generated for ${imgDone} character${imgDone!==1?"s":""}${imgSuffix}`,imgFailed&&!imgDone?"error":"success")}function csGoToLibrary(){typeof navTo=="function"&&navTo("s-library"),typeof navLibraryView=="function"?navLibraryView("characters"):typeof libraryRender=="function"&&libraryRender("characters"),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib")}function csAttachLineCounts(sheets,counts){!counts||!counts.size||sheets.forEach(function(s){const n=counts.get(String(s.name||"").trim().toLowerCase());n!=null&&(s.line_count=n)})}async function csForReader(opts={}){var _a2;const fresh=!!opts.fresh,text=csReaderText(),book=readerState.title||"Untitled book",key="reader:"+(readerState.title||"")+":"+(typeof readerScopeIndices=="function"?readerScopeIndices().length:0);typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();if(!fresh&&_cs.cache[key]){const cached=csCanonicalizeRosterNames(_cs.cache[key],csKnownReaderRoster());_cs.cache[key]=cached,await csSaveToLibrary(book,cached),pageHost&&await csShow(cached,book,text,book,pageHost);return}const knownRoster=csKnownReaderRoster();await csEnsureBookContext(book,text,knownRoster,{force:fresh});let seedSheets=[];if(!fresh)try{if(typeof clGetAllByTagOrBook=="function"){const existing=await clGetAllByTagOrBook(book),stillCast=rec=>{var _a3,_b2,_c2,_d2;return!knownRoster.length||csSheetMatchesRoster({name:(rec==null?void 0:rec.name)||"",aliases:((_a3=rec==null?void 0:rec.sheet)==null?void 0:_a3.aliases)||"",first_name:((_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name)||"",last_name:((_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name)||"",full_name:((_d2=rec==null?void 0:rec.sheet)==null?void 0:_d2.full_name)||""},knownRoster)};seedSheets=(existing||[]).filter(stillCast).map(rec=>{const sheet=csSeedSheet(rec),rosterName=csRosterCanonicalName(sheet,knownRoster);return rosterName&&sheet.name.toLowerCase()!==rosterName.toLowerCase()&&(sheet.aliases=csMergeAliases({name:rosterName,aliases:sheet.aliases},{name:sheet.name,aliases:""}),sheet.name=rosterName),sheet}).filter(s=>String(s.name||"").trim())}}catch{seedSheets=[]}const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook,lineCounts=new Map;if((_a2=ab==null?void 0:ab.segments)!=null&&_a2.length)for(const s of ab.segments){if((s==null?void 0:s.type)!=="dialogue"||!s.speaker)continue;const k=String(s.speaker).trim().toLowerCase();lineCounts.set(k,(lineCounts.get(k)||0)+1)}const sheets=await csGenerate(text,key,knownRoster,{pageHost,seedSheets,lineCounts});if(sheets){if(!sheets.length){toast("No characters found","error");return}lineCounts.size&&csAttachLineCounts(sheets,lineCounts),await csSaveToLibrary(book,sheets),pageHost&&await csShow(sheets,book,text,book,pageHost),toast(sheets.length+" character sheets saved \u2014 Library \u2192 Cast","success"),csAutoGenerateExternalPrompts(book,sheets)}}async function csForReaderSelective(selectedNames){var _a2;const wanted=new Set((selectedNames||[]).map(n=>String(n).trim().toLowerCase()));if(!wanted.size){toast("No characters selected","error");return}const text=csReaderText(),book=readerState.title||"Untitled book";typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost(),knownRoster=csKnownReaderRoster();let records=[];try{records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(book):[]}catch{records=[]}const picked=[],seen=new Set,matchesWanted=rec=>{var _a3,_b2,_c2,_d2;const tokens=csNameTokens({name:(rec==null?void 0:rec.name)||"",aliases:((_a3=rec==null?void 0:rec.sheet)==null?void 0:_a3.aliases)||"",first_name:((_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name)||"",last_name:((_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name)||"",full_name:((_d2=rec==null?void 0:rec.sheet)==null?void 0:_d2.full_name)||""});for(const t of tokens){const lower=String(t||"").toLowerCase();for(const w of wanted)if(lower===w||lower.includes(w)||w.includes(lower))return!0}return!1};if((selectedNames||[]).forEach(name=>{const canonical=String(name||"").trim(),key=canonical.toLowerCase();if(!key||seen.has(key))return;let chosen=(records||[]).find(rec=>String((rec==null?void 0:rec.name)||"").trim().toLowerCase()===key)||(records||[]).find(rec=>{var _a3,_b2,_c2,_d2;return!rec||!rec.name?!1:csNameTokens({name:rec.name,aliases:((_a3=rec.sheet)==null?void 0:_a3.aliases)||"",first_name:((_b2=rec.sheet)==null?void 0:_b2.first_name)||"",last_name:((_c2=rec.sheet)==null?void 0:_c2.last_name)||"",full_name:((_d2=rec.sheet)==null?void 0:_d2.full_name)||""}).some(t=>{const lower=String(t||"").toLowerCase();return lower===key||lower.includes(key)||key.includes(lower)})})||{name:canonical,sheet:csBlankSheet(canonical)};chosen.name!==canonical&&(chosen=Object.assign({},chosen,{name:canonical,sheet:Object.assign({},chosen.sheet||{},{name:canonical})})),seen.add(key),picked.push(chosen)}),picked.sort((a,b)=>{var _a3,_b2;return(((_a3=b==null?void 0:b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a==null?void 0:a.sheet)==null?void 0:_b2.line_count)||0)||String((a==null?void 0:a.name)||"").localeCompare(String((b==null?void 0:b.name)||""))}),!picked.length){toast("No matching cast characters found","error");return}const finalMap=new Map;for(const rec of picked){const targetName=String(rec.name||"").trim();if(!targetName)continue;const rosterName=targetName,needles=csRecordNeedles(rec),scanText=csEvidenceWindowText(csReaderParagraphBlocks(),needles,2,2)||text,seedSheet=csSeedSheet(rec);seedSheet.name=targetName;const sheets=await csGenerate(scanText,null,[rosterName],{pageHost,seedSheets:[seedSheet]});if(!sheets)return;const filtered2=sheets.filter(s=>wanted.has(String(s.name||"").trim().toLowerCase()));filtered2.length&&(filtered2.forEach(s=>{s.name=targetName,s._rosterNameAuthoritative=!0}),((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.line_count)!=null&&filtered2.forEach(s=>{s.line_count=rec.sheet.line_count}),csMerge(finalMap,filtered2))}const filtered=[...finalMap.values()];if(!filtered.length){toast("None of the selected characters turned up in this pass \u2014 try again or pick different ones","error");return}const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook;if(ab!=null&&ab.roster){const counts=new Map;ab.roster.forEach(function(info,name){counts.set(String(name).trim().toLowerCase(),info.count||0)}),csAttachLineCounts(filtered,counts)}await csSaveToLibrary(book,filtered),pageHost&&await csShow(filtered,book,text,book,pageHost),toast(filtered.length+" character"+(filtered.length!==1?"s":"")+" defined","success")}async function csForRehearser(){var _a2,_b2,_c2,_d2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Character sheets",book=((_b2=$("reh-script-title"))==null?void 0:_b2.value.trim())||"Untitled script",text=csRehearserText(),key="reh:"+title+":"+(rehState.lines||[]).length;typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();if(_cs.cache[key]){await csSaveToLibrary(book,_cs.cache[key]),pageHost&&await csShow(_cs.cache[key],title,text,book,pageHost);return}let seedSheets=[];try{typeof clGetAllByTagOrBook=="function"&&(seedSheets=(await clGetAllByTagOrBook(book)||[]).map(rec=>csSeedSheet(rec)).filter(s=>String(s.name||"").trim()))}catch{seedSheets=[]}const sheets=await csGenerate(text,key,null,{pageHost,seedSheets});if(sheets){if(!sheets.length){toast("No characters found","error");return}if((_d2=(_c2=window.rehState)==null?void 0:_c2.lines)!=null&&_d2.length){const counts=new Map;rehState.lines.forEach(function(l){if(l.type!=="dialog"||!l.speaker)return;const k=String(l.speaker).trim().toLowerCase();counts.set(k,(counts.get(k)||0)+1)}),csAttachLineCounts(sheets,counts)}await csSaveToLibrary(book,sheets),pageHost&&await csShow(sheets,title,text,book,pageHost),toast(sheets.length+" character sheets saved \u2014 Library \u2192 Cast","success")}}window.csForReader=csForReader,window.csForReaderSelective=csForReaderSelective,window.csForRehearser=csForRehearser,(_rc=$("reader-charsheets-btn"))==null||_rc.addEventListener("click",csForReader),(_sc=$("reh-charsheets-btn"))==null||_sc.addEventListener("click",csForRehearser);const CL_EDIT_FIELDS=[["name","Name"],["aliases","Aliases / also known as"],["first_name","First name"],["last_name","Last name"],["title","Title / role"],["age_estimate","Estimated age"],["race_species","Race / species"],["languages","Languages"],["nationality_background","Nationality / background"],["social_class","Social class"],["archetype","Archetype"],["physical","Physical"],["clothing","Clothing & Appearance"],["alignment","Alignment & Ethos"],["arc_note","Arc note"],["skills","Trained Skills"],["capabilities","Capabilities"],["backstory","Backstory & Origin"],["relationships","Relationships"],["motivation","Motivation"],["fears","Fears"],["mannerisms","Mannerisms & Habits"],["communication_style","Communication style"],["reputation","Reputation"],["religious_beliefs","Religious beliefs"],["notes","Notes"],["voice_pattern","Voice & Speech"],["voice_design_prompt","Voice Design Prompt"],["image_prompt","Image Generation Prompt"],["silly_tavern_prompt","SillyTavern Character Prompt"],["concept_art_prompt","Concept Art Prompt"],["secret","Dark Secret / Fatal Flaw"],["conflict_style","Conflict Style"],["win_condition","Win Condition"]];async function clGetAll(){const r=await fetch("/api/characters");if(!r.ok)throw new Error("clGetAll failed: "+r.status);return(await r.json()).characters||[]}async function clGet(id){const r=await fetch("/api/characters/"+encodeURIComponent(id));if(r.status!==404){if(!r.ok)throw new Error("clGet failed: "+r.status);return r.json()}}async function clPut(rec){rec!=null&&rec.color&&(rec.sheet=rec.sheet||{},rec.sheet.color=clNormalizeColor(rec.color,rec.name));const r=await fetch("/api/characters/"+encodeURIComponent(rec.id),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(rec)});if(!r.ok)throw new Error("clPut failed: "+r.status);const saved=await r.json();return typeof window._rehSyncCastVoiceFromLibrary=="function"&&window._rehSyncCastVoiceFromLibrary(saved),saved}async function clDelete(id){const r=await fetch("/api/characters/"+encodeURIComponent(id),{method:"DELETE"});if(!r.ok)throw new Error("clDelete failed: "+r.status)}function clKey(book,name){return`${String(book||"").trim()}::${String(name||"").trim()}`.toLowerCase()}function clHslToHex(h,s,l){s/=100,l/=100;const k=n=>(n+h/30)%12,a=s*Math.min(l,1-l),f=n=>l-a*Math.max(-1,Math.min(k(n)-3,Math.min(9-k(n),1)));return"#"+[f(0),f(8),f(4)].map(x=>Math.round(255*x).toString(16).padStart(2,"0")).join("")}function clNameHue(name){return Math.abs((name||"?").split("").reduce((h,c)=>(h*31+c.charCodeAt(0))%360,0))}function clNormalizeColor(color,name){const c=String(color||"").trim();return/^#[0-9a-f]{6}$/i.test(c)?c:/^#[0-9a-f]{3}$/i.test(c)?"#"+c.slice(1).split("").map(ch=>ch+ch).join(""):clHslToHex(clNameHue(name),58,43)}const CL_IDENTITY_FIELDS=["name","aliases","first_name","last_name","full_name"],CL_ALIAS_MAX_TOKENS=12,CL_ALIAS_MAX_CHARS=500,CL_ALIAS_STOPWORDS=new Set(["die","der","das","den","dem","des","ein","eine","einer","er","sie","es","ich","du","wir","ihr","the","a","an","he","she","it","they","who"]);function clSplitIdentityTokens(v,opts={}){const raw=_clStr(v),parts=raw.split(/[,;/|]|\baka\b|\baka\.\b|\balias(?:es)?\b|\bgenannt\b|\bnamens\b|\bcalled\b|\bknown as\b/i).map(x=>x.trim()).filter(Boolean).filter(x=>x.length<=80&&!/^needs?:/i.test(x)&&!/^complete$/i.test(x)).filter(x=>!CL_ALIAS_STOPWORDS.has(x.toLowerCase()));return opts.aliases&&(raw.length>CL_ALIAS_MAX_CHARS||parts.length>CL_ALIAS_MAX_TOKENS)?[]:parts.slice(0,opts.aliases?CL_ALIAS_MAX_TOKENS:void 0)}function clIdentityNames(recOrSheet){const s=(recOrSheet==null?void 0:recOrSheet.sheet)||recOrSheet||{},out=new Set,add=(v,opts={})=>clSplitIdentityTokens(v,opts).forEach(x=>out.add(x.toLowerCase()));return add((recOrSheet==null?void 0:recOrSheet.name)||s.name),CL_IDENTITY_FIELDS.filter(k=>k!=="name").forEach(k=>add(s[k],{aliases:k==="aliases"})),out}function clMergeAliases(existing,incoming){const names=new Map,add=v=>clSplitIdentityTokens(v,{aliases:!0}).forEach(x=>names.set(x.toLowerCase(),x));return add(existing.aliases),add(incoming.aliases),incoming.name&&incoming.name!==existing.name&&add(incoming.name),[...names.values()].filter(n=>n.toLowerCase()!==String(existing.name||"").toLowerCase()).join(", ")}function clSameIdentity(rec,book,sheet){if(String((rec==null?void 0:rec.book)||"").trim().toLowerCase()!==String(book||"").trim().toLowerCase())return!1;const a=clIdentityNames(rec),b=clIdentityNames(sheet);for(const n of b)if(a.has(n))return!0;return!1}function _clStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _clSanitize(sheet){const out={...sheet};return(typeof CS_SCALAR_FIELDS!="undefined"?CS_SCALAR_FIELDS:CL_EDIT_FIELDS.map(f=>f[0]).filter(k=>k!=="name")).forEach(f=>{out[f]!=null&&(out[f]=_clStr(out[f]))}),out}function clMergeSheet(existing,incoming){const e={...existing},inc=_clSanitize(incoming),scalars=typeof CS_SCALAR_FIELDS!="undefined"?CS_SCALAR_FIELDS:CL_EDIT_FIELDS.map(f=>f[0]).filter(k=>k!=="name"),oldAliases=e.aliases;return scalars.forEach(f=>{(inc[f]||"").length>(e[f]||"").length&&(e[f]=inc[f])}),e.aliases=clMergeAliases({...e,aliases:oldAliases},incoming),incoming.tier==="main"&&(e.tier="main"),incoming.moral_alignment_score!=null&&(e.moral_alignment_score=e.moral_alignment_score!=null?Math.round((e.moral_alignment_score+incoming.moral_alignment_score)/2):incoming.moral_alignment_score),incoming.arc_direction&&incoming.arc_direction!=="neutral"&&(e.arc_direction=incoming.arc_direction),incoming.gender&&!e.gender&&(e.gender=incoming.gender),incoming.line_count!=null&&(e.line_count=incoming.line_count),e.inventory=[...existing.inventory||[]],(incoming.inventory||[]).forEach(it=>{it&&!e.inventory.includes(it)&&e.inventory.length<3&&e.inventory.push(it)}),e.sources=[...existing.sources||[]],(incoming.sources||[]).forEach(src=>{src&&src.quote&&e.sources.length<12&&!e.sources.some(x=>x.quote===src.quote)&&e.sources.push(src)}),e}function clMergeTags(...parts){const set=new Set;return parts.forEach(p=>String(p||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>set.add(t))),[...set].join(", ")}async function clUpsert(book,sheet,knownId){var _a2;const name=(sheet.name||"").trim();if(!name)return null;const rosterNameAuthoritative=sheet._rosterNameAuthoritative===!0,bk=(book||"").trim()||"Unsorted",authoritativeId=rosterNameAuthoritative?clKey(bk,name):null,aliasPrev=authoritativeId?await clGet(authoritativeId).catch(()=>null):knownId?await clGet(knownId).catch(()=>null):(await clGetAll().catch(()=>[])).find(r=>clSameIdentity(r,bk,sheet)),id=authoritativeId||(aliasPrev==null?void 0:aliasPrev.id)||knownId||clKey(bk,name),now=new Date,prev=aliasPrev||await clGet(id).catch(()=>null),sanitized=_clSanitize(sheet);delete sanitized._rosterNameAuthoritative;const merged=prev?clMergeSheet(prev.sheet||{},sheet):{...sanitized,name},canonicalName=rosterNameAuthoritative?name:(prev==null?void 0:prev.name)||name;merged.name=canonicalName;const tags=clMergeTags(prev==null?void 0:prev.tags,sheet.tags,bk),color=clNormalizeColor((prev==null?void 0:prev.color)||((_a2=prev==null?void 0:prev.sheet)==null?void 0:_a2.color)||sheet.color,canonicalName);merged.color=color;const rec={id,book:bk,name:canonicalName,tags,sheet:merged,color,analysis:(prev==null?void 0:prev.analysis)||null,voice:sheet.voice||(prev==null?void 0:prev.voice)||null,image:sheet.image||(prev==null?void 0:prev.image)||null,created:(prev==null?void 0:prev.created)||now,updated:now};return await clPut(rec),rec}async function clSetImage(id,dataUrl){const rec=await clGet(id).catch(()=>null);if(rec)return rec.image=dataUrl||null,rec.updated=new Date,await clPut(rec),rec}window.clSetImage=clSetImage;async function clGetAllByTagOrBook(title){const key=String(title||"").trim().toLowerCase();return key?(await clGetAll().catch(()=>[])).filter(r=>String(r.book||"").trim().toLowerCase()===key?!0:String(r.tags||"").split(",").some(t=>t.trim().toLowerCase()===key)):[]}async function clUpsertMany(book,sheets){let n=0;for(const s of sheets||[])await clUpsert(book,s)&&n++;return n}(async function(){try{if((await clGetAll()).length>0)return;const idbRecs=await _clIdbGetAll().catch(()=>[]);if(!idbRecs.length)return;const r=await fetch("/api/characters/migrate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(idbRecs)});if(r.ok){const d=await r.json();console.log(`[characters-library] migrated ${d.imported} records from IndexedDB \u2192 SQLite`)}}catch(e){console.warn("[characters-library] migration skipped:",e)}})();function _clIdbGetAll(){return new Promise((resolve,reject)=>{const req=indexedDB.open("character-library",1);req.onerror=()=>resolve([]),req.onsuccess=e=>{const db=e.target.result;if(!db.objectStoreNames.contains("characters")){db.close(),resolve([]);return}const all=db.transaction("characters","readonly").objectStore("characters").getAll();all.onsuccess=ev=>{db.close(),resolve(ev.target.result||[])},all.onerror=()=>{db.close(),resolve([])}}})}let _clRecords=[];async function clRender(){if(!document.getElementById("cl-grid"))return;const filterEl=document.getElementById("cl-book-filter"),searchEl=document.getElementById("cl-search");filterEl&&!filterEl.dataset.bound&&(filterEl.dataset.bound="1",filterEl.addEventListener("change",clApplyFilter)),searchEl&&!searchEl.dataset.bound&&(searchEl.dataset.bound="1",searchEl.addEventListener("input",clApplyFilter));try{_clRecords=await clGetAll()}catch{_clRecords=[]}const filter=document.getElementById("cl-book-filter"),prods=clAllProductions();if(filter){const cur=filter.value;filter.innerHTML=``+prods.map(b=>``).join(""),cur&&prods.includes(cur)&&(filter.value=cur)}clApplyFilter()}function clAllProductions(){const set=new Set;return _clRecords.forEach(r=>{r.book&&set.add(r.book),String(r.tags||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>set.add(t))}),[...set].sort((a,b)=>a.localeCompare(b))}function clApplyFilter(){var _a2,_b2;const grid=document.getElementById("cl-grid");if(!grid)return;const book=((_a2=document.getElementById("cl-book-filter"))==null?void 0:_a2.value)||"",q=(((_b2=document.getElementById("cl-search"))==null?void 0:_b2.value)||"").trim().toLowerCase();let recs=_clRecords.slice();if(book){const bk=book.toLowerCase();recs=recs.filter(r=>(r.book||"").toLowerCase()===bk||String(r.tags||"").split(",").some(t=>t.trim().toLowerCase()===bk))}if(q&&(recs=recs.filter(r=>{var _a3,_b3,_c2,_d2,_e2;return(r.name||"").toLowerCase().includes(q)||(((_a3=r.sheet)==null?void 0:_a3.aliases)||"").toLowerCase().includes(q)||(((_b3=r.sheet)==null?void 0:_b3.first_name)||"").toLowerCase().includes(q)||(((_c2=r.sheet)==null?void 0:_c2.last_name)||"").toLowerCase().includes(q)||(((_d2=r.sheet)==null?void 0:_d2.title)||"").toLowerCase().includes(q)||(((_e2=r.sheet)==null?void 0:_e2.archetype)||"").toLowerCase().includes(q)||(r.tags||"").toLowerCase().includes(q)||(r.book||"").toLowerCase().includes(q)})),!recs.length){grid.innerHTML=`

${_clRecords.length?"No characters match your filter.":"No characters yet."}

Run Character sheets from Read Aloud or the Script Rehearser to populate your library.

@@ -1823,4 +1823,4 @@ Respond with STRICT JSON only: `)[0].slice(0,60)||"",backstory:data.description||data.char_persona||"",mannerisms:data.personality||"",arc_note:data.scenario||"",voice_pattern:data.mes_example||data.example_dialogue||data.first_mes||data.char_greeting||"",gender:ext.gender||stGuessGender((data.description||"")+" "+(data.personality||"")),note:data.creator_notes||"",tags,voice:ext.tts_voice||null,tier:"supporting",st_card:data}}function stFromRecord(rec){const sh=rec.sheet||{},prev=sh.st_card||{},descParts=[sh.physical,sh.clothing,sh.backstory,sh.alignment].map(x=>String(x||"").trim()).filter(Boolean),persona=[sh.archetype,sh.mannerisms,sh.motivation,sh.fears].map(x=>String(x||"").trim()).filter(Boolean).join(` `);return{spec:"chara_card_v2",spec_version:"2.0",data:{name:rec.name,description:prev.description||descParts.join(` -`),personality:persona||prev.personality||"",scenario:rec.book||prev.scenario||"",first_mes:prev.first_mes||"",mes_example:sh.voice_pattern||prev.mes_example||"",creator_notes:"Exported from TTS Voice Creator"+(rec.book?" \xB7 "+rec.book:""),system_prompt:prev.system_prompt||"",post_history_instructions:prev.post_history_instructions||"",tags:String(rec.tags||rec.book||"").split(",").map(t=>t.trim()).filter(Boolean),creator:prev.creator||"",character_version:prev.character_version||"1.0",extensions:Object.assign({},prev.extensions,{tts_voice:rec.voice||""})}}}function stDownloadJson(card,filename){const blob=new Blob([JSON.stringify(card,null,2)],{type:"application/json"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3)}async function stImportCards(book,fileList){let n=0;for(const file of fileList)try{const data=await stParseFile(file),sheet=stToSheet(data);typeof clUpsert=="function"&&(await clUpsert(book||sheet.name,Object.assign({},sheet,{tags:book||""})),n++)}catch(e){typeof toast=="function"&&toast("\u201C"+(file.name||"card")+"\u201D: "+(e.message||e),"error")}return n}function stImportDialog(book,onDone){const inp=document.createElement("input");inp.type="file",inp.accept=".json,.png",inp.multiple=!0,inp.onchange=async()=>{if(!inp.files.length)return;const n=await stImportCards(book,inp.files);typeof toast=="function"&&toast(n?"Imported "+n+" character"+(n>1?"s":""):"Nothing imported",n?"success":"error"),typeof onDone=="function"&&onDone()},inp.click()}function stExportRecord(rec){const card=stFromRecord(rec),safe=String(rec.name||"character").replace(/[^\w\- ]+/g,"").trim().replace(/\s+/g,"_")||"character";stDownloadJson(card,safe+".card.json")}window.stParseFile=stParseFile,window.stToSheet=stToSheet,window.stFromRecord=stFromRecord,window.stImportCards=stImportCards,window.stImportDialog=stImportDialog,window.stExportRecord=stExportRecord;const LIB_READER_API="/api/reader/docs";function prodKey(title){return String(title||"").trim().toLowerCase()}window._libraryView=function(){try{return localStorage.getItem("ttsvc_library_view")||"books"}catch{return"books"}}(),window.navLibraryView=function(view){typeof navTo=="function"&&navTo("s-library"),window._libraryView=view;try{localStorage.setItem("ttsvc_library_view",view)}catch{}document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="characters"&&typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib"),libraryRender(view)},window.libraryRender=function(view){view=view||window._libraryView||"books",document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="books"?libraryRenderBooks():view==="plays"?libraryRenderPlays():view==="characters"&&typeof window.libraryRenderCharacters=="function"&&window.libraryRenderCharacters()};function _libSkeleton(n){return Array.from({length:n},()=>'
').join("")}async function libraryRenderBooks(){const list=document.getElementById("lib-books-list");if(!list)return;list.innerHTML=_libSkeleton(4);let all=[];try{const r=await fetch(LIB_READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='

No books yet.

Open Read Aloud, import a PDF or text, and save it to your library.

';return}all.sort(function(a,b){return new Date(b.updated||0)-new Date(a.updated||0)}),list.innerHTML=all.map(function(rec){const total=rec.sentenceCount||0,synthPct=total?Math.round((rec.synthCount||0)/total*100):0,readPct=total?Math.round((rec.idx||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"",cov=libBookCover(rec.title||"Untitled"),coverUrl=LIB_READER_API+"/"+rec.id+"/cover?t="+new Date(rec.updated||Date.now()).getTime(),bg=rec.hasCover?`style="background-image:linear-gradient(to bottom,rgba(0,0,0,.3),rgba(0,0,0,.8)),url('`+coverUrl+`');background-size:cover;background-position:center;color:#fff"`:'style="--bk1:'+cov.c1+";--bk2:"+cov.c2+'"';return'
'+escHtml(rec.title||"Untitled")+'
'+total+" sentences"+(rec.pageCount?" \xB7 "+rec.pageCount+" pg":"")+'
'+readPct+"% read \xB7 "+date+"
"}).join(""),list.querySelectorAll(".lib-book").forEach(function(el){const id=el.dataset.id,title=el.dataset.title;el.addEventListener("click",function(e){e.target.closest(".reh-book-act")||(window._readerStartView="main",typeof navTo=="function"&&navTo("s-reader"),typeof readerOpenLibraryDoc=="function"&&readerOpenLibraryDoc(id))});const reh=el.querySelector(".lib-act-rehearse");reh&&reh.addEventListener("click",function(e){e.stopPropagation(),productionOpenInRehearser(title)});const del=el.querySelector(".lib-act-del-book");del&&del.addEventListener("click",function(e){e.stopPropagation(),libConfirmDelete(el,"Delete book?","Audio files will be removed.",async function(){try{if(!(await fetch(LIB_READER_API+"/"+id,{method:"DELETE"})).ok)throw new Error("delete failed");toast("Book deleted","success"),libraryRenderBooks()}catch(err){toast(err.message||"Delete failed","error")}})})})}async function libraryRenderPlays(){const list=document.getElementById("lib-plays-list");if(!list)return;list.innerHTML=_libSkeleton(3);let all=[];try{all=typeof rehDbGetAll=="function"?await rehDbGetAll():[]}catch{all=[]}if(!all.length){list.innerHTML='

No theater plays yet.

Open Script Rehearsal \u2192 Import / Export to add a script, or cast a book as an audiobook.

';return}all.sort(function(a,b){return new Date(b.updated||0)-new Date(a.updated||0)}),list.innerHTML=all.map(function(rec){const speakers=Object.keys(rec.cast||{}),total=typeof parseScript=="function"?parseScript(rec.script||"").filter(function(l){return l.type==="dialog"}).length:0,pct=total?Math.round((rec.lineIndex||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"\u2014",cov=libBookCover(rec.title||"Untitled"),avatars=speakers.slice(0,5).map(function(sp){return''+(sp[0]||"?").toUpperCase()+""}).join("");return'
'+escHtml(rec.title||"Untitled")+'
'+avatars+"
"+total+" lines \xB7 "+speakers.length+' cast
'+pct+"% \xB7 "+date+"
"}).join(""),list.querySelectorAll(".lib-play").forEach(function(el){const id=parseInt(el.dataset.id,10),title=el.dataset.title;el.addEventListener("click",function(e){e.target.closest(".reh-book-act")||openPlayInRehearser(id)});const ra=el.querySelector(".lib-act-readaloud");ra&&ra.addEventListener("click",function(e){e.stopPropagation(),productionOpenInReader(title)});const del=el.querySelector(".lib-act-del-play");del&&del.addEventListener("click",function(e){e.stopPropagation(),libConfirmDelete(el,"Delete rehearsal?","This cannot be undone.",async function(){try{typeof rehDbDelete=="function"&&await rehDbDelete(id),toast("Rehearsal deleted","success"),libraryRenderPlays()}catch(err){toast(err.message||"Delete failed","error")}})})})}async function openPlayInRehearser(id){try{const rec=typeof rehDbGetById=="function"?await rehDbGetById(id):null;rec&&typeof loadRecord=="function"?loadRecord(rec):toast("Rehearsal not found","error")}catch{toast("Could not open rehearsal","error")}}async function productionOpenInRehearser(title){const key=prodKey(title);try{const match=(typeof rehDbGetAll=="function"?await rehDbGetAll():[]).find(function(p){return prodKey(p.title)===key});if(match){openPlayInRehearser(match.id);return}}catch{}let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&book.kind!=="pdf")try{const sr=await fetch(LIB_READER_API+"/"+book.id+"/source"),text=sr.ok?await sr.text():"";if(text&&typeof audiobookOpenInRehearser=="function"){audiobookOpenInRehearser(text,title,[]);return}}catch{}if(book&&book.kind==="pdf"){typeof readerOpenLibraryDoc=="function"&&readerOpenLibraryDoc(book.id),toast('Open this PDF book, then use "Cast as audiobook" to build a rehearsal',"info");return}toast("No source to rehearse for this title yet","error")}async function productionOpenInReader(title){const key=prodKey(title);let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&typeof readerOpenLibraryDoc=="function"){readerOpenLibraryDoc(book.id);return}typeof navTo=="function"&&navTo("s-reader"),toast("No audiobook for this title yet \u2014 import its source in Read Aloud","info")}async function castForProduction(title){const out={};if(typeof clGetAllByTagOrBook!="function")return out;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{recs=[]}return recs.forEach(function(r){const name=(r.name||"").trim();if(!name)return;const voice=r.voice&&r.voice.id?r.voice.id:typeof r.voice=="string"?r.voice:"";out[name.toLowerCase()]={name,voice:voice||"",gender:r.sheet&&r.sheet.gender||"",soul:r.sheet&&(r.sheet.voice_pattern||r.sheet.motivation)||"",tags:r.tags||""}}),out}window.castForProduction=castForProduction;async function castWriteBack(title,castMap){if(!title||!castMap||typeof clGetAllByTagOrBook!="function"||typeof clPut!="function")return;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{return}if(!recs.length)return;const byName={};recs.forEach(function(r){byName[(r.name||"").trim().toLowerCase()]=r});let n=0;for(const sp of Object.keys(castMap)){if(String(sp).includes("NARRATOR"))continue;const voice=(castMap[sp]||{}).voice;if(!voice||voice==="me")continue;const rec=byName[String(sp).trim().toLowerCase()];if(!(!rec||(rec.voice&&rec.voice.id?rec.voice.id:typeof rec.voice=="string"?rec.voice:"")===voice)){rec.voice={id:voice},rec.updated=new Date;try{await clPut(rec),n++}catch{}}}return n}window.castWriteBack=castWriteBack;function libBookCover(title){let h=0;const s=String(title||"Untitled");for(let i=0;i'+heading+'
'+sub+'
',o.addEventListener("click",function(e){e.stopPropagation()}),o.querySelector("[data-lib-cancel]").addEventListener("click",function(e){e.stopPropagation(),o.remove()}),o.querySelector("[data-lib-ok]").addEventListener("click",async function(e){e.stopPropagation(),o.innerHTML='',await onConfirm()}),cardEl.appendChild(o)}window.libraryRenderBooks=libraryRenderBooks,window.libraryRenderPlays=libraryRenderPlays,window.productionOpenInRehearser=productionOpenInRehearser,window.productionOpenInReader=productionOpenInReader,window.prodKey=prodKey;async function libraryRenderCharacters(){var _a2;const container=document.getElementById("lib-chars-list");if(!container)return;let all=[];try{all=typeof clGetAll=="function"?await clGetAll():[]}catch(e){console.warn("[characters] load failed",e),typeof toast=="function"&&toast("Failed to load characters \u2014 keeping the current view","error");return}const mainEl=document.getElementById("main-content"),savedScrollTop=!window._libCharsScrollToBook&&mainEl?mainEl.scrollTop:null;container.innerHTML='
Loading characters\u2026
';const byId=new Map(all.map(function(rec){return[rec.id,rec]}));if(!all.length){container.innerHTML='

No characters yet.

Open a book in Read Aloud, cast it as an audiobook, then click Cast Characters to generate character sheets \u2014 or import an existing cast from SillyTavern.

',container.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),savedScrollTop!=null&&(mainEl.scrollTop=savedScrollTop);return}const byBook={};all.forEach(function(rec){const bk=rec.book||"Unsorted";byBook[bk]||(byBook[bk]=[]),byBook[bk].push(rec)}),Object.keys(byBook).forEach(function(bk){if(!byBook[bk].some(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"})){const narrRec={id:clKey(bk,"Narrator"),book:bk,name:"Narrator",tags:bk,voice:null,image:null,sheet:{}};byId.set(narrRec.id,narrRec),byBook[bk].unshift(narrRec)}}),container.innerHTML="";const viewMode=localStorage.getItem("ttsvc_libchars_view")==="table"?"table":"cards";let returnToReader=!1;try{returnToReader=sessionStorage.getItem("ttsvc_cast_return")==="reader"}catch{}const SORT_OPTIONS=[["tier","Rolle (Haupt zuerst)"],["alpha","Alphabet"],["lines","Anzahl Zeilen"],["gender","Geschlecht"],["voice","Stimme zugewiesen"]],sortMode=SORT_OPTIONS.some(function(o){return o[0]===localStorage.getItem("ttsvc_libchars_sort")})?localStorage.getItem("ttsvc_libchars_sort"):"tier",bar=document.createElement("div");bar.className="lib-chars-toolbar",bar.innerHTML=(returnToReader?'':"")+'
',bar.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),(_a2=bar.querySelector("#lib-chars-back-reader"))==null||_a2.addEventListener("click",function(){try{sessionStorage.removeItem("ttsvc_cast_return")}catch{}typeof navTo=="function"&&navTo("s-reader")}),bar.querySelector("#lib-chars-sort-sel").addEventListener("change",function(){localStorage.setItem("ttsvc_libchars_sort",this.value),libraryRenderCharacters()}),bar.querySelectorAll(".lib-chars-view-toggle button").forEach(function(btn){btn.addEventListener("click",function(){localStorage.setItem("ttsvc_libchars_view",btn.dataset.view),libraryRenderCharacters()})}),container.appendChild(bar);const _charSortCmp={tier:function(a,b){var _a3,_b2,_c2,_d2;const tierOrder={main:0,supporting:1,minor:2},ta=(_b2=tierOrder[String(((_a3=a.sheet)==null?void 0:_a3.tier)||"minor").toLowerCase()])!=null?_b2:2,tb=(_d2=tierOrder[String(((_c2=b.sheet)==null?void 0:_c2.tier)||"minor").toLowerCase()])!=null?_d2:2;return ta-tb||(a.name||"").localeCompare(b.name||"")},alpha:function(a,b){return(a.name||"").localeCompare(b.name||"")},lines:function(a,b){var _a3,_b2;return(((_a3=b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)||(a.name||"").localeCompare(b.name||"")},gender:function(a,b){var _a3,_b2;const ga=String(((_a3=a.sheet)==null?void 0:_a3.gender)||"zzz"),gb=String(((_b2=b.sheet)==null?void 0:_b2.gender)||"zzz");return ga.localeCompare(gb)||(a.name||"").localeCompare(b.name||"")},voice:function(a,b){return(b.voice?1:0)-(a.voice?1:0)||(a.name||"").localeCompare(b.name||"")},age:function(a,b){return _charAgeSortVal(a.sheet)-_charAgeSortVal(b.sheet)||(a.name||"").localeCompare(b.name||"")},language:function(a,b){return _charLangLabel(a).localeCompare(_charLangLabel(b))||(a.name||"").localeCompare(b.name||"")},align:function(a,b){var _a3,_b2,_c2,_d2;return((_b2=(_a3=b.sheet)==null?void 0:_a3.moral_alignment_score)!=null?_b2:-1)-((_d2=(_c2=a.sheet)==null?void 0:_c2.moral_alignment_score)!=null?_d2:-1)||(a.name||"").localeCompare(b.name||"")}},sortDir=localStorage.getItem("ttsvc_libchars_sort_dir")==="desc"?"desc":"asc",productions=document.createDocumentFragment();if(Object.keys(byBook).sort().forEach(function(book){const chars=byBook[book].sort(_charSortCmp[sortMode]||_charSortCmp.tier);sortDir==="desc"&&chars.reverse();const narrIdx=chars.findIndex(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"});narrIdx>0&&chars.unshift(chars.splice(narrIdx,1)[0]);const cov=libBookCover(book),prod=document.createElement("div");prod.className="lib-chars-production",prod.dataset.book=book;const collapseKey="ttsvc_libchars_collapsed::"+book;let isCollapsed=localStorage.getItem(collapseKey)==="1";window._libCharsScrollToBook&&(isCollapsed=book!==window._libCharsScrollToBook),isCollapsed&&prod.classList.add("lib-chars-production-collapsed"),prod.innerHTML='
'+escHtml(book)+'
`+(viewMode==="table"?_charsTableHtml(chars,sortMode,sortDir):'
'+chars.map(function(rec){return _charCardHtml(rec,chars)}).join("")+"
")+"
",prod.querySelector(".lib-chars-prod-collapse-btn").addEventListener("click",function(e){e.stopPropagation();const collapsed=prod.classList.toggle("lib-chars-production-collapsed");localStorage.setItem(collapseKey,collapsed?"1":"0")}),prod.querySelector(".lib-chars-prod-head").addEventListener("click",function(e){e.target.closest("button, select, input, a")||prod.querySelector(".lib-chars-prod-collapse-btn").click()}),prod.querySelector(".lib-chars-bookctx-btn").addEventListener("click",function(){_editBookProfile(book)}),prod.querySelector(".lib-chars-casting-btn").addEventListener("click",function(){typeof navTo=="function"&&navTo("s-reader")}),prod.querySelector(".lib-chars-cast-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book),toast("Open the book in Read Aloud then click Cast Characters","info")}),prod.querySelector(".lib-chars-reh-btn").addEventListener("click",function(){typeof productionOpenInRehearser=="function"&&productionOpenInRehearser(book)}),prod.querySelector(".lib-chars-read-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book)}),prod.querySelector(".lib-chars-imp-btn").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog(book,function(){libraryRenderCharacters()})}),prod.querySelectorAll("[data-sort-key]").forEach(function(th){th.addEventListener("click",function(){const key=th.dataset.sortKey,nextDir=sortMode===key&&sortDir==="asc"?"desc":"asc";localStorage.setItem("ttsvc_libchars_sort",key),localStorage.setItem("ttsvc_libchars_sort_dir",nextDir),libraryRenderCharacters()})});const selectAllBtn=prod.querySelector(".lib-chars-select-all-btn"),bulkBtn=prod.querySelector(".lib-chars-bulk-voice-btn"),bulkCount=prod.querySelector(".lib-chars-bulk-count"),designBtn=prod.querySelector(".lib-chars-bulk-design-btn"),designCount=prod.querySelector(".lib-chars-bulk-count-design"),imageBtn=prod.querySelector(".lib-chars-bulk-image-btn"),imageCount=prod.querySelector(".lib-chars-bulk-count-image"),imageProviderSel=prod.querySelector(".lib-chars-image-provider");imageProviderSel&&(imageProviderSel.value=typeof _appSettings!="undefined"&&_appSettings.image_gen_provider||"");const deleteBtn=prod.querySelector(".lib-chars-bulk-delete-btn"),deleteCount=prod.querySelector(".lib-chars-bulk-count-delete"),tblSelectAllCb=prod.querySelector(".lib-chars-tbl-select-all-cb"),syncTblSelectAllCb=function(){if(!tblSelectAllCb)return;const boxes=[...prod.querySelectorAll(".lib-char-select-cb")],checkedN=boxes.filter(function(cb){return cb.checked}).length;tblSelectAllCb.checked=boxes.length>0&&checkedN===boxes.length,tblSelectAllCb.indeterminate=checkedN>0&&checkedN0&&boxes.every(function(cb){return cb.checked});boxes.forEach(function(cb){cb.checked=!allChecked}),refreshBulkBtn()}),tblSelectAllCb&&tblSelectAllCb.addEventListener("change",function(){[...prod.querySelectorAll(".lib-char-select-cb")].forEach(function(cb){cb.checked=tblSelectAllCb.checked}),refreshBulkBtn()}),syncTblSelectAllCb();const runBulk=async function(btn,ids,verb,fn){btn.disabled=!0;const orig=btn.innerHTML;let done=0,failed=0,lastErrMsg="",repeatErrMsg="",repeatCount=0,aborted=!1;for(const id of ids){const rec=byId.get(id);if(rec){btn.innerHTML=' '+verb+" "+(done+failed+1)+" / "+ids.length+"\u2026";try{await fn(rec),done++,repeatCount=0}catch(e){if(failed++,lastErrMsg=e&&e.message?e.message:String(e),console.error("[bulk "+verb+"]",rec.name,e),lastErrMsg===repeatErrMsg?repeatCount++:(repeatErrMsg=lastErrMsg,repeatCount=1),repeatCount>=3){aborted=!0;break}}}}btn.innerHTML=orig;const remaining=ids.length-done-failed,suffix=failed?` (${failed} failed${aborted&&remaining?`, ${remaining} skipped`:""}${lastErrMsg?": "+lastErrMsg.slice(0,200):""})`:"";toast(`${verb} finished for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),await _flushPendingTtsRestart(),typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary({refresh:!0}).catch(()=>{}),libraryRenderCharacters()};bulkBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(bulkBtn,ids,"Assigning",_autoAssignVoice)}),designBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(designBtn,ids,"Designing",_charAutoDesignVoice)}),imageBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});if(!ids.length)return;const provider=imageProviderSel?imageProviderSel.value:"";runBulk(imageBtn,ids,"Generating images",function(rec){return _charAutoGenerateImage(rec,provider)})});const fixLangBtn=prod.querySelector(".lib-chars-fix-lang-btn");fixLangBtn==null||fixLangBtn.addEventListener("click",function(){const _bookLangCounts={};chars.forEach(function(r){const l=_charLang(r);l&&(_bookLangCounts[l]=(_bookLangCounts[l]||0)+1)});let _bookLang="",_bookLangBest=0;Object.keys(_bookLangCounts).forEach(function(l){_bookLangCounts[l]>_bookLangBest&&(_bookLang=l,_bookLangBest=_bookLangCounts[l])});const bookCode=_bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[_bookLang]:null,mismatched=chars.filter(function(rec){if(!rec.voice||!bookCode)return!1;const voiceId=typeof rec.voice=="object"?rec.voice.id:rec.voice;return _voiceLangCode(voiceId)!==bookCode});if(!mismatched.length){toast("No language-mismatched voices found in this production","info");return}runBulk(fixLangBtn,mismatched.map(function(r){return r.id}),"Redesigning",function(rec){return _charAutoDesignVoice(rec,!0)})}),deleteBtn.addEventListener("click",async function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});!ids.length||!await confirmDialog(`Delete ${ids.length} character${ids.length!==1?"s":""} from the library? This cannot be undone \u2014 use it to clear out stale/corrupted entries before a fresh recast.`,{title:"Delete characters?",okLabel:"Delete",danger:!0})||runBulk(deleteBtn,ids,"Deleting",function(rec){return clDelete(rec.id)})}),_wireCharCards(prod,byId,chars),productions.appendChild(prod)}),container.appendChild(productions),window._libCharsScrollToBook){const target=window._libCharsScrollToBook;window._libCharsScrollToBook=null;const prodEl=[...container.querySelectorAll(".lib-chars-production")].find(function(p){return p.dataset.book===target});prodEl&&(prodEl.scrollIntoView({behavior:"smooth",block:"start"}),prodEl.classList.add("lib-chars-production-highlight"),setTimeout(function(){prodEl.classList.remove("lib-chars-production-highlight")},2200))}else savedScrollTop!=null&&(mainEl.scrollTop=savedScrollTop)}function _charHue(name){return Math.abs((name||"?").split("").reduce(function(h,c){return(h*31+c.charCodeAt(0))%360},0))}function _charAlignHtml(sh){const score=sh.moral_alignment_score;if(score==null)return"";const pct=Math.max(0,Math.min(100,score)),arc=sh.arc_direction||"neutral",arrowMap={"good-to-bad":{ch:"\u2198",color:"#ff7043",tip:"Arc: Descends toward evil"},"bad-to-good":{ch:"\u2197",color:"#66bb6a",tip:"Arc: Redeems toward good"},complex:{ch:"\u2195",color:"#ab47bc",tip:"Arc: Complex / unpredictable"},"stable-good":{ch:"\u2192",color:"#66bb6a",tip:"Arc: Stable good"},"stable-bad":{ch:"\u2192",color:"#888",tip:"Arc: Stable evil"},neutral:{ch:"\u2192",color:"#aaa",tip:"Arc: Neutral"}},a=arrowMap[arc]||arrowMap.neutral;return'
\u25CF
\u25CF'+a.ch+"
"}function _libStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _charAgeLabel(sh){return _libStr((sh==null?void 0:sh.age_estimate)||"").trim()}function _charAgeSortVal(sh){const m=_charAgeLabel(sh).match(/\d+/);return m?parseInt(m[0],10):9999}function _charLangLabel(rec){const sh=(rec==null?void 0:rec.sheet)||{},voiceLang=rec!=null&&rec.voice&&typeof rec.voice=="object"&&rec.voice.language||"";return _libStr(sh.languages||voiceLang).trim()}function _charGenderLabel(sh){const gender=String((sh==null?void 0:sh.gender)||"").trim();return gender?gender.charAt(0).toUpperCase()+gender.slice(1):""}function _charRelsHtml(rec,allChars){var _a2;if(!allChars||allChars.length<2)return"";const relText=_libStr((_a2=rec.sheet)==null?void 0:_a2.relationships).toLowerCase();if(!relText)return"";const hits=allChars.filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,5);return hits.length?'
'+hits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":""}let _avatarHoverEl=null;function _showAvatarHoverPreview(rec,anchorEl){if(!rec.image)return;_avatarHoverEl||(_avatarHoverEl=document.createElement("div"),_avatarHoverEl.className="lib-avatar-hover-preview",_avatarHoverEl.innerHTML="",document.body.appendChild(_avatarHoverEl)),_avatarHoverEl.querySelector("img").src=rec.image;const rect=anchorEl.getBoundingClientRect(),size=512;let left=rect.right+12;left+size>window.innerWidth&&(left=rect.left-size-12);let top=rect.top+rect.height/2-size/2;top=Math.max(8,Math.min(top,window.innerHeight-size-8)),_avatarHoverEl.style.left=Math.max(8,left)+"px",_avatarHoverEl.style.top=top+"px",_avatarHoverEl.hidden=!1}function _hideAvatarHoverPreview(){_avatarHoverEl&&(_avatarHoverEl.hidden=!0)}function _wireCharCards(root,recsById,allRecs,onChange,detailOpts){const refresh=onChange||libraryRenderCharacters;root.querySelectorAll(".lib-char-card").forEach(function(card){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2;const charId=card.dataset.charId,rec=recsById.get?recsById.get(charId):recsById[charId];if(rec){if(card.addEventListener("click",function(e){e.target.closest("button, .lib-char-avatar, .lib-voice-picker-popup")||_charDetailPage(rec,allRecs,detailOpts)}),(_a2=card.querySelector(".lib-char-avatar"))==null||_a2.addEventListener("click",function(e){e.stopPropagation(),_openAvatarLightbox(rec,refresh)}),rec.image){const avatarEl=card.querySelector(".lib-char-avatar");avatarEl==null||avatarEl.addEventListener("mouseenter",function(){_showAvatarHoverPreview(rec,avatarEl)}),avatarEl==null||avatarEl.addEventListener("mouseleave",_hideAvatarHoverPreview)}(_b2=card.querySelector(".lib-char-voice-pill"))==null||_b2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_c2=card.querySelector(".lib-char-pick-voice"))==null||_c2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_d2=card.querySelector(".lib-char-auto-voice"))==null||_d2.addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),refresh()}),(_e2=card.querySelector(".lib-char-redesign-voice"))==null||_e2.addEventListener("click",async function(e){e.stopPropagation();const btn=e.currentTarget;btn.disabled=!0;try{await _charAutoDesignVoice(rec,!0),_schedulePendingTtsRestart(),refresh()}catch(err){toast("Voice design failed: "+(err.message||err),"error")}finally{btn.disabled=!1}}),(_f2=card.querySelector(".lib-char-remove-voice"))==null||_f2.addEventListener("click",async function(e){e.stopPropagation(),await clPut(Object.assign({},rec,{voice:"",updated:new Date})),rec.voice="",_syncVoicePictureFromChar(rec),toast("Voice removed from "+rec.name,"success"),refresh()}),(_g2=card.querySelector(".lib-char-export"))==null||_g2.addEventListener("click",function(e){e.stopPropagation(),typeof stExportRecord=="function"&&stExportRecord(rec)}),(_h2=card.querySelector(".lib-char-online-voice"))==null||_h2.addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),(_i2=card.querySelector(".lib-char-gen-voice"))==null||_i2.addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),(_j2=card.querySelector(".lib-char-voice-play"))==null||_j2.addEventListener("click",function(e){e.stopPropagation(),_libPreviewCharVoice(rec,e.currentTarget)})}})}let _libVoicePreviewEl=null,_libVoicePreviewBtn=null;function _libStopVoicePreview(){if(_libVoicePreviewEl&&(_libVoicePreviewEl.pause(),_libVoicePreviewEl.src=""),_libVoicePreviewBtn){_libVoicePreviewBtn.classList.remove("playing","loading");const icon=_libVoicePreviewBtn.querySelector(".mdi");icon&&(icon.className="mdi mdi-play")}_libVoicePreviewBtn=null}async function _libPreviewCharVoice(rec,btn){const voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"";if(!voiceId){toast("No voice assigned yet","error");return}if(_libVoicePreviewBtn===btn&&_libVoicePreviewEl&&!_libVoicePreviewEl.paused){_libStopVoicePreview();return}_libStopVoicePreview();const icon=btn.querySelector(".mdi"),v=(window._voices||[]).find(x=>x.id===voiceId);if(!v||!v.path||typeof voiceFileUrl!="function"){toast("Voice file not found","error");return}btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{_libVoicePreviewEl||(_libVoicePreviewEl=new Audio,_libVoicePreviewEl.addEventListener("ended",_libStopVoicePreview)),_libVoicePreviewEl.src=voiceFileUrl(v),await _libVoicePreviewEl.play(),btn.classList.remove("loading"),_libVoicePreviewBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _voiceExists(voiceId){if(!voiceId)return!0;const voices=window._voices||[];return voices.length?voices.some(function(v){return v.id===voiceId}):!0}function _charCardHtml(rec,allChars){const sh=rec.sheet||{},hue=_charHue(rec.name),hue2=(hue+40)%360,voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",snippet=_libStr(sh.mannerisms||sh.voice_pattern||sh.motivation||sh.backstory||"").slice(0,190),roleLine=_libStr(sh.profession||sh.archetype).trim(),ageLabel=_charAgeLabel(sh),langLabel=_charLangLabel(rec),genderLabel=_charGenderLabel(sh),bookLabel=_libStr(rec.book||""),lineLabel=sh.line_count!=null?String(sh.line_count)+" Zeilen":"",tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),tagsHtml=tagList.length?'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+"
":"",hasPhoto=!!rec.image,bannerStyle=hasPhoto?'style="background-image:linear-gradient(180deg, rgba(0,0,0,.05) 0%, rgba(0,0,0,.72) 100%), url("'+rec.image+'"); background-size:cover; background-position:center;"':'style="--ch1:hsl('+hue+",52%,35%);--ch2:hsl("+hue2+',56%,26%)"',avatarInner=hasPhoto?'':escHtml((rec.name||"?")[0].toUpperCase()),stat=function(label,value,icon){return value?'
'+escHtml(label)+''+escHtml(value)+"
":""},metaChips=[];return bookLabel&&metaChips.push(' '+escHtml(bookLabel)+""),'
'+avatarInner+'
'+(voiceId?'':"")+'
'+escHtml(rec.name)+''+tierBadge+"
"+(roleLine?'
'+escHtml(roleLine)+"
":"")+(_libStr(sh.title)?'
Titel: '+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
aka '+escHtml(_libStr(sh.aliases))+"
":"")+'
'+metaChips.join("")+'
'+stat("Occupation",_libStr(sh.profession),"mdi-briefcase-outline")+stat("Archetype",_libStr(sh.archetype),"mdi-shape-outline")+stat("Gender",genderLabel,"mdi-gender-male-female")+stat("Age",ageLabel,"mdi-cake-variant")+stat("Lines",lineLabel,"mdi-format-list-numbered")+"
"+_charAlignHtml(sh)+"
"}function _charsTableHtml(chars,sortMode,sortDir){const arrow=function(key){return sortMode===key?' ':""},th=function(key,label,title){return'"+label+arrow(key)+""},rows=chars.map(function(rec){const sh=rec.sheet||{},hue=_charHue(rec.name),voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),voiceLang=_charLangLabel(rec),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":gender?"mdi-gender-non-binary":"",genderLabel=_charGenderLabel(sh),score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),avatarInner=rec.image?''+escHtml(rec.name)+'':escHtml((rec.name||"?")[0].toUpperCase()),occupation=_libStr(sh.profession),ageLabel=_charAgeLabel(sh),bookLabel=_libStr(rec.book||""),aliasLabel=_libStr(sh.aliases),archetype=_libStr(sh.archetype),titleLabel=_libStr(sh.title),tableMeta=[aliasLabel?"aka "+aliasLabel:"",occupation?"Occupation: "+occupation:"",titleLabel?"Title: "+titleLabel:"",archetype?"Archetype: "+archetype:""].filter(Boolean).join(" \xB7 ");return'
'+avatarInner+'
'+tierBadge+escHtml(rec.name)+"
"+(tableMeta?'
'+escHtml(tableMeta)+"
":"")+(bookLabel?'
'+escHtml(bookLabel)+"
":"")+""+(genderLabel?escHtml(genderLabel):'\u2014')+""+(ageLabel?escHtml(ageLabel):'\u2014')+""+(sh.line_count!=null?sh.line_count:'\u2014')+""+(voiceLang?escHtml(voiceLang):'\u2014')+""+(pct!=null?'
':'\u2014')+'
'+(voiceId?'"+(voiceMissing?' ':"")+escHtml(voiceId)+"":'Keine Stimme')+'
'+(voiceId?'':"")+''+(voiceId?'':"")+'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+'
'}).join("");return'
'+th("alpha","Name")+th("gender","Geschlecht")+th("age","Alter","Estimated age")+th("lines","Zeilen","Anzahl Zeilen")+th("language","Sprache")+th("align","Gut/B\xF6se","Moralische Gesinnung")+th("voice","Stimme")+""+rows+"
TagsBook / Script
"}function _lcdSourcesHtml(sources){const list=Array.isArray(sources)?sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[];return list.length?'
'+list.map(function(s){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":""}function _lcdField(label,value,multiline){const v=_libStr(value);return v?'
'+label+'
'+escHtml(v)+"
":""}function _lcdSection(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdSectionFull(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdPromptBox(label,value,sheetKey){const has=!!(value&&String(value).trim());return'
'+escHtml(label)+(has?"":' \u2014 not generated yet')+'
'+escHtml(value||"")+'
"}function _lcdFieldEdit(label,value,sheetKey,sourceIdxs){const v=_libStr(value),links=(sourceIdxs||[]).map(function(idx){return''+(sourceIdxs.indexOf(idx)+1)+""}).join("");return'
'+(label||links?'
'+escHtml(label)+(links?' '+links+"":"")+"
":"")+'
'+escHtml(v)+"
"}function _jumpToReaderPage(pageNum){typeof navTo=="function"&&navTo("s-reader"),setTimeout(function(){var _a2,_b2;const pages=(_a2=window.readerState)==null?void 0:_a2.pages;if(pages&&pages.length>=pageNum){const pg=pages[pageNum-1];if(pg!=null&&pg.pageDiv){pg.pageDiv.scrollIntoView({behavior:"smooth",block:"start"});return}}const sentences=(_b2=window.readerState)==null?void 0:_b2.sentences;if(sentences&&sentences.length){const target0=pageNum-1,idx=sentences.findIndex(function(s){return(s.words||[]).some(function(w){var _a3,_b3;return((_b3=(_a3=w.page)!=null?_a3:w.para)!=null?_b3:0)>=target0})});if(idx>=0&&typeof readerJumpTo=="function"){readerJumpTo(idx);return}}toast('\xD6ffne das Buch in \u201EVorlesen" und klicke nochmal auf die Quelle',"info")},300)}function _charLineCount(c){if(window.rehState&&rehState.lines&&rehState.lines.length){const key=String(c.name||"").toUpperCase().trim(),live=rehState.lines.filter(function(l){return l.type==="dialog"&&String(l.speaker||"").toUpperCase().trim()===key}).length;if(live)return live}return Number(c.sheet&&c.sheet.line_count)||0}async function _charDetailPage(rec,allChars,opts){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2;opts=opts||{};const container=opts.container||document.getElementById("lib-chars-list");if(!container)return;const goBack=typeof opts.onBack=="function"?opts.onBack:libraryRenderCharacters;window._libDetailRec=rec;const sh=rec.sheet||{},hue=_charHue(rec.name),hue2=(hue+40)%360,tier=String(sh.tier||"").toLowerCase(),tierLabel=tier==="main"?"Hauptcharakter":tier==="supporting"?"Nebencharakter":tier==="minor"?"Nebenfigur":"",gender=_libStr(sh.gender),genderIcon=gender.toLowerCase().startsWith("f")?"mdi-gender-female":gender.toLowerCase().startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,arcMap={"good-to-bad":{ch:"\u2198",label:"Entwicklung zum B\xF6sen",color:"#ff7043"},"bad-to-good":{ch:"\u2197",label:"Wandel zum Guten",color:"#66bb6a"},complex:{ch:"\u2195",label:"Komplex / unvorhersehbar",color:"#ab47bc"},"stable-good":{ch:"\u2192",label:"Stabil gut",color:"#66bb6a"},"stable-bad":{ch:"\u2192",label:"Stabil b\xF6se",color:"#888"},neutral:{ch:"\u2192",label:"Neutral / stabil",color:"#aaa"}},arcInfo=arcMap[sh.arc_direction||"neutral"]||arcMap.neutral,avatarHtml='
'+(rec.image?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
")+'
',conceptArtHtml='
"+(sh.concept_art_image?'
Concept art \u2014 '+escHtml(rec.name)+'
':'
'+(_libStr(sh.concept_art_prompt).trim()?"Kein Konzeptbild":"Kein Konzeptbild-Prompt \u2014 erst unten bei Generation Prompts erzeugen")+"
")+"
",alignHtml=pct!=null?'
B\xF6seGut'+pct+'/100
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",promptsHtml='
'+_lcdPromptBox("Voice Design Prompt",sh.voice_design_prompt,"voice_design_prompt")+_lcdPromptBox("Character Image Prompt",sh.image_prompt,"image_prompt")+_lcdPromptBox("SillyTavern Character Prompt",sh.silly_tavern_prompt,"silly_tavern_prompt")+_lcdPromptBox("Concept Art Prompt",sh.concept_art_prompt,"concept_art_prompt")+"
",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",sourcesList=Array.isArray(sh.sources)?sh.sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[],sourcesByField={};sourcesList.forEach(function(s,idx){const key=_libStr(s.line_hint||s.hint||"").trim().toLowerCase();key&&(sourcesByField[key]=sourcesByField[key]||[]).push(idx)});const sourcesHtml=sourcesList.length?'
'+sourcesList.map(function(s,idx){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":"",sidebarHtml=(allChars||[]).slice().sort(function(a,b){return _charLineCount(b)-_charLineCount(a)}).map(function(c){const h=_charHue(c.name),count=_charLineCount(c);return'
'+escHtml((c.name||"?")[0].toUpperCase())+''+escHtml(c.name)+""+(count?''+count+"":"")+"
"}).join("");container.innerHTML="";const pg=document.createElement("div");pg.className="lib-char-page",pg.style.gridColumn="1 / -1",pg.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+'
'+escHtml(_libStr(sh.aliases))+'
'+escHtml(_libStr(sh.archetype))+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+conceptArtHtml+'
'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdFieldEdit("Voller Name",sh.full_name,"full_name",sourcesByField.full_name),_lcdFieldEdit("Vorname",sh.first_name,"first_name",sourcesByField.first_name),_lcdFieldEdit("Nachname",sh.last_name,"last_name",sourcesByField.last_name),_lcdFieldEdit("Geschlecht",sh.gender,"gender",sourcesByField.gender),_lcdFieldEdit("Titel",sh.title,"title",sourcesByField.title),_lcdFieldEdit("Beruf / Rolle",sh.profession,"profession",sourcesByField.profession),_lcdFieldEdit("Auch bekannt als",sh.aliases,"aliases",sourcesByField.aliases)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdFieldEdit("K\xF6rperlich",sh.physical,"physical",sourcesByField.physical),_lcdFieldEdit("Kleidung & Aussehen",sh.clothing,"clothing",sourcesByField.clothing)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdFieldEdit("Eigenheiten & Verhalten",sh.mannerisms,"mannerisms",sourcesByField.mannerisms),_lcdFieldEdit("Stimme & Sprache",sh.voice_pattern,"voice_pattern",sourcesByField.voice_pattern)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdFieldEdit("Hintergrund & Herkunft",sh.backstory,"backstory",sourcesByField.backstory),_lcdFieldEdit("Motivation",sh.motivation,"motivation",sourcesByField.motivation),_lcdFieldEdit("\xC4ngste",sh.fears,"fears",sourcesByField.fears)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdFieldEdit("Fertigkeiten",sh.skills,"skills",sourcesByField.skills),_lcdFieldEdit("Besondere F\xE4higkeiten",sh.capabilities,"capabilities",sourcesByField.capabilities),_lcdFieldEdit("St\xE4rkstes Attribut",sh.attribute_high,"attribute_high"),_lcdFieldEdit("Schw\xE4chstes Attribut",sh.attribute_low,"attribute_low")])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdFieldEdit("",sh.relationships,"relationships",sourcesByField.relationships),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdFieldEdit("Konfliktstil",sh.conflict_style,"conflict_style",sourcesByField.conflict_style),_lcdFieldEdit("Siegbedingung",sh.win_condition,"win_condition",sourcesByField.win_condition)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdFieldEdit("Dunkles Geheimnis / fataler Fehler",sh.secret,"secret"),_lcdFieldEdit("Charakterentwicklung",sh.arc_note,"arc_note")])+promptsHtml+"
"+sourcesHtml+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+'
Charaktere \xB7 '+escHtml(rec.book||"")+"
"+sidebarHtml+"
",container.appendChild(pg),pg.querySelector(".lib-cpg-back").addEventListener("click",function(){goBack()});const _lcdUploadAvatar=function(){const inp=document.createElement("input");inp.type="file",inp.accept="image/*",inp.onchange=async function(){const file=inp.files[0];if(!file)return;const fr=new FileReader;fr.onload=async function(ev){typeof clSetImage=="function"&&await clSetImage(rec.id,ev.target.result),toast("Profilbild gespeichert","success"),rec.image=ev.target.result,_syncVoicePictureFromChar(rec);const av=pg.querySelector(".lcd-avatar-upload");av&&(av.innerHTML=''+escHtml(rec.name)+'')},fr.readAsDataURL(file)},inp.click()};pg.querySelector(".lcd-avatar-upload").addEventListener("click",_lcdUploadAvatar),(_a2=pg.querySelector(".lcd-avatar-upload-btn"))==null||_a2.addEventListener("click",function(e){e.stopPropagation(),_lcdUploadAvatar()}),(_b2=pg.querySelector(".lcd-avatar-online-btn"))==null||_b2.addEventListener("click",function(e){e.stopPropagation();const q=[rec.name,rec.book,sh.archetype,"character art"].filter(Boolean).join(" ");window.open("https://www.google.com/search?tbm=isch&q="+encodeURIComponent(q),"_blank","noopener")}),(_c2=pg.querySelector(".lcd-avatar-gen-btn"))==null||_c2.addEventListener("click",async function(e){e.stopPropagation();const btn=this,bookProfile=typeof _getBookProfile=="function"?await _getBookProfile(rec.book):{},prompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh,bookProfile):"");if(!prompt){toast("No image prompt to work from \u2014 generate the Character Image Prompt below first","error");return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='';try{const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt})});if(!r.ok)throw new Error((await r.json().catch(function(){return{}})).detail||r.statusText);const d=await r.json();typeof clSetImage=="function"&&await clSetImage(rec.id,d.image),rec.image=d.image,_syncVoicePictureFromChar(rec),toast("Profile picture generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Image generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}}),(_d2=pg.querySelector(".lcd-conceptart-gen"))==null||_d2.addEventListener("click",async function(e){e.stopPropagation();const btn=this;if(!_libStr(sh.concept_art_prompt).trim()){toast("No Concept Art Prompt yet \u2014 generate that first in Generation Prompts below","error");return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='';try{await _charAutoGenerateConceptArt(rec),toast("Concept art generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Concept art generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}}),(_e2=pg.querySelector(".lcd-conceptart-img"))==null||_e2.addEventListener("click",function(){var _a3;(_a3=document.getElementById("conceptart-lightbox"))==null||_a3.remove();const ov=document.createElement("div");ov.id="conceptart-lightbox",ov.className="audiobook-overlay",ov.innerHTML='
'+escHtml(rec.name)+' \u2014 Konzeptbild
Concept art \u2014 '+escHtml(rec.name)+'
',document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector("#calb-close").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()})}),pg.querySelectorAll(".lib-cpg-sidebar-item").forEach(function(item){item.addEventListener("click",async function(){const target=(allChars||[]).find(function(c){return c.id===item.dataset.charId});target&&_charDetailPage(target,allChars,opts)})}),pg.querySelectorAll(".lcd-source-clickable").forEach(function(item){item.addEventListener("click",function(){const n=parseInt(item.dataset.page,10);isNaN(n)||_jumpToReaderPage(n)})}),(_f2=pg.querySelector(".lcd-pick-voice"))==null||_f2.addEventListener("click",async function(){_openVoicePicker(pg.querySelector(".lcd-voice-top"),rec,async function(){const all=await clGetAll().catch(()=>allChars),up=all.find(function(r){return r.id===rec.id})||rec;_charDetailPage(up,all.filter(function(r){return r.book===rec.book}),opts)})}),(_g2=pg.querySelector(".lcd-auto-voice"))==null||_g2.addEventListener("click",async function(){await _autoAssignVoice(rec);const all=await clGetAll().catch(()=>allChars),up=all.find(function(r){return r.id===rec.id})||rec;_charDetailPage(up,all.filter(function(r){return r.book===rec.book}),opts)}),(_h2=pg.querySelector(".lcd-online-voice"))==null||_h2.addEventListener("click",function(){_charSearchOnline(rec)}),(_i2=pg.querySelector(".lcd-gen-voice"))==null||_i2.addEventListener("click",function(){_charDesignVoiceInline(rec)}),(_j2=pg.querySelector(".lcd-clone-voice"))==null||_j2.addEventListener("click",function(){_charCloneVoice(rec)}),pg.querySelectorAll(".lcd-prompt-copy").forEach(function(btn){btn.addEventListener("click",async function(){var _a3;const box=btn.closest(".lcd-prompt-body"),text=((_a3=box==null?void 0:box.querySelector(".lcd-prompt-text"))==null?void 0:_a3.textContent.trim())||"";if(!text){toast("Nothing to copy yet \u2014 click Generate first","error");return}typeof copyText=="function"&&await copyText(text),toast("Prompt copied","success")})}),pg.querySelectorAll(".lcd-gen-prompt").forEach(function(btn){btn.addEventListener("click",async function(e){e.preventDefault();const key=btn.dataset.sheetKey,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026';try{const sh2=rec.sheet||{},sample=[sh2.physical,sh2.backstory,sh2.motivation].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""},r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:rec.name,book:rec.book||"",sheet:sh2,language,llm_url:target.url,model:target.model,fields:[key]})});if(!r.ok)throw new Error((await r.json().catch(function(){return{}})).detail||r.statusText);const d=await r.json();if(!d[key])throw new Error("Empty response \u2014 try again");rec.sheet||(rec.sheet={}),rec.sheet[key]=d[key],rec.updated=new Date,typeof clPut=="function"&&await clPut(rec),toast("Prompt generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Prompt generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}})});const slider=pg.querySelector(".lcd-align-slider"),sliderVal=pg.querySelector(".lcd-align-slider-val"),arcEl=pg.querySelector(".lcd-align-arc");slider&&slider.addEventListener("input",async function(){const val=parseInt(slider.value,10);sliderVal&&(sliderVal.textContent=val+"/100"),arcEl&&(arcEl.textContent=arcInfo.ch+" "+arcInfo.label+(val>=70?" \xB7 Rechtschaffen ("+val+"/100)":val<=30?" \xB7 B\xF6se ("+val+"/100)":" \xB7 Moralisch ambivalent ("+val+"/100)")),arcEl&&(arcEl.style.color=arcInfo.color),rec.sheet.moral_alignment_score=val,rec.updated=new Date,typeof clPut=="function"&&await clPut(rec)});const _saveTimers=new Map;function _schedSave(key,value,isRecKey){clearTimeout(_saveTimers.get(key)),_saveTimers.set(key,setTimeout(async function(){isRecKey?rec[key]=value:(rec.sheet||(rec.sheet={}),rec.sheet[key]=value),rec.updated=new Date,typeof clPut=="function"&&await clPut(rec)},900))}pg.querySelectorAll("[contenteditable][data-sheet-key]").forEach(function(el){el.addEventListener("input",function(){_schedSave(el.dataset.sheetKey,el.textContent.trim(),!1)})}),pg.querySelectorAll("[contenteditable][data-rec-key]").forEach(function(el){el.addEventListener("input",function(){_schedSave(el.dataset.recKey,el.textContent.trim(),!0)})})}window._charDetailPage=_charDetailPage;function _charDetailModal(rec,allChars){const sh=rec.sheet||{},cov=libBookCover(rec.name),hue=_charHue(rec.name),tier=String(sh.tier||"").toLowerCase(),tierLabel=tier==="main"?"Hauptcharakter":tier==="supporting"?"Nebencharakter":tier==="minor"?"Nebenfigur":"",gender=_libStr(sh.gender),genderIcon=gender.toLowerCase().startsWith("f")?"mdi-gender-female":gender.toLowerCase().startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,arc=sh.arc_direction||"neutral",arcMap={"good-to-bad":{ch:"\u2198",label:"Entwicklung zum B\xF6sen",color:"#ff7043"},"bad-to-good":{ch:"\u2197",label:"Wandel zum Guten",color:"#66bb6a"},complex:{ch:"\u2195",label:"Komplex / unvorhersehbar",color:"#ab47bc"},"stable-good":{ch:"\u2192",label:"Stabil gut",color:"#66bb6a"},"stable-bad":{ch:"\u2192",label:"Stabil b\xF6se",color:"#888"},neutral:{ch:"\u2192",label:"Neutral / stabil",color:"#aaa"}},arcInfo=arcMap[arc]||arcMap.neutral,voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",avatarHtml=rec.image?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
",alignHtml=pct!=null?'
B\xF6se
Gut
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.arc_note)?'
'+escHtml(_libStr(sh.arc_note))+"
":"")+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",ov=document.createElement("div");ov.className="lib-char-detail-ov",ov.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
auch bekannt als '+escHtml(_libStr(sh.aliases))+"
":"")+(_libStr(sh.archetype)?'
'+escHtml(_libStr(sh.archetype))+"
":"")+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdField("Voller Name",sh.full_name,!0),_lcdField("Vorname",sh.first_name,!0),_lcdField("Nachname",sh.last_name,!0),_lcdField("Geschlecht",sh.gender,!0),_lcdField("Titel",sh.title,!0),_lcdField("Beruf / Rolle",sh.profession,!0),_lcdField("Auch bekannt als",sh.aliases,!0)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdField("K\xF6rperlich",sh.physical,!0),_lcdField("Kleidung & Aussehen",sh.clothing,!0)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdField("Eigenheiten & Verhalten",sh.mannerisms,!0),_lcdField("Stimme & Sprache",sh.voice_pattern,!0)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdField("Hintergrund & Herkunft",sh.backstory,!0),_lcdField("Motivation",sh.motivation,!0),_lcdField("\xC4ngste",sh.fears,!0)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdField("Fertigkeiten",sh.skills,!0),_lcdField("Besondere F\xE4higkeiten",sh.capabilities,!0),_lcdField("St\xE4rkstes Attribut",sh.attribute_high,!1),_lcdField("Schw\xE4chstes Attribut",sh.attribute_low,!1)])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdField("",sh.relationships,!0),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdField("Konfliktstil",sh.conflict_style,!0),_lcdField("Siegbedingung",sh.win_condition,!0)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdField("Dunkles Geheimnis / fataler Fehler",sh.secret,!0),_lcdField("Charakterentwicklung",sh.arc_note,!0)])+"
"+_lcdSourcesHtml(sh.sources)+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+"
",document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector(".lcd-close-btn").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector(".lcd-edit-btn").addEventListener("click",function(){close(),typeof clEdit=="function"&&clEdit(rec.id)}),ov.querySelector(".lcd-pick-voice").addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){close(),libraryRenderCharacters()})}),ov.querySelector(".lcd-auto-voice").addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),close(),libraryRenderCharacters()}),ov.querySelector(".lcd-online-voice").addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),ov.querySelector(".lcd-gen-voice").addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),ov.querySelector(".lcd-avatar").addEventListener("click",function(){const inp=document.createElement("input");inp.type="file",inp.accept="image/*",inp.onchange=async function(){const file=inp.files[0];if(!file)return;const fr=new FileReader;fr.onload=async function(ev){typeof clSetImage=="function"&&await clSetImage(rec.id,ev.target.result),toast("Profile picture saved","success"),close(),libraryRenderCharacters()},fr.readAsDataURL(file)},inp.click()})}window._charDetailModal=_charDetailModal;function _openAvatarLightbox(rec,onSaved){var _a2;(_a2=document.getElementById("avatar-lightbox"))==null||_a2.remove();const sh=rec.sheet||{},currentPrompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh,_getBookProfileSync(rec.book)):""),ov=document.createElement("div");ov.id="avatar-lightbox",ov.className="audiobook-overlay",ov.innerHTML='
'+escHtml(rec.name)+' \u2014 Profilbild
'+(rec.image?''+escHtml(rec.name)+'':'
')+'
',document.body.appendChild(ov),ov.addEventListener("click",function(e){e.target===ov&&ov.remove()}),ov.querySelector("#alb-close").addEventListener("click",function(){ov.remove()});const setPreview=function(src){ov.querySelector(".alb-preview").innerHTML=''+escHtml(rec.name)+''},setStatus=function(msg,cls){const el=ov.querySelector("#alb-status");el.textContent=msg||"",el.className="llm-active-status"+(cls?" "+cls:"")},commitImage=async function(dataUri){typeof clSetImage=="function"&&await clSetImage(rec.id,dataUri),rec.image=dataUri,setPreview(dataUri),document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),toast("Profilbild gespeichert","success"),_syncVoicePictureFromChar(rec),typeof onSaved=="function"&&onSaved()};ov.querySelector("#alb-file-input").addEventListener("change",function(){const file=this.files[0];if(!file)return;const fr=new FileReader;fr.onload=function(ev){commitImage(ev.target.result)},fr.readAsDataURL(file)}),ov.querySelector("#alb-url-btn").addEventListener("click",async function(){const url=ov.querySelector("#alb-url-input").value.trim();if(!url){toast("Bild-URL eingeben","error");return}const btn=this;btn.disabled=!0,setStatus("Wird heruntergeladen\u2026");try{const r=await fetch("/api/character-image-from-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),setStatus("\u2713 Heruntergeladen","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Download fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1}}),ov.querySelector("#alb-gen-btn").addEventListener("click",async function(){const prompt=ov.querySelector("#alb-prompt").value.trim();if(!prompt){toast("Prompt eingeben","error");return}const provider=ov.querySelector("#alb-provider").value,btn=this,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generiere\u2026',setStatus("Generiere\u2026 (kann bei lokalen Modellen etwas dauern)");try{const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),rec.sheet||(rec.sheet={}),rec.sheet.image_prompt!==prompt&&(rec.sheet.image_prompt=prompt,typeof clUpsert=="function"&&await clUpsert(rec.book,Object.assign({},rec.sheet,{name:rec.name}),rec.id)),setStatus("\u2713 Generiert","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Generierung fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}window._openAvatarLightbox=_openAvatarLightbox;async function _syncVoicePictureFromChar(rec){if(!rec||!rec.image||!rec.voice)return;const voiceId=typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice||"");if(voiceId)try{const existing=(window._voices||[]).find(function(v){return v.id===voiceId});if(existing&&existing.has_picture)return;const blob=await(await fetch(rec.image)).blob(),fd=new FormData;fd.append("voice_id",voiceId),fd.append("file",blob,"character.jpg"),await fetch("/api/voice/picture",{method:"POST",body:fd})}catch(e){console.warn("[voice picture sync]",e)}}window._syncVoicePictureFromChar=_syncVoicePictureFromChar;function _openVoicePicker(cardEl,rec,onDone){var _a2;document.querySelectorAll(".lib-voice-picker-popup").forEach(function(p){p.remove()});let voices=window._voices||[];const gender=String(((_a2=rec.sheet)==null?void 0:_a2.gender)||"").toLowerCase(),genderMatch=gender.startsWith("f")?"f":gender.startsWith("m")?"m":"",popup=document.createElement("div");popup.className="lib-voice-picker-popup",popup.innerHTML='
',popup.querySelector(".lib-vp-design-btn").addEventListener("click",function(){popup.remove(),typeof _charDesignVoiceInline=="function"&&_charDesignVoiceInline(rec)});function renderList(filter){let list=voices.filter(function(v){return v.enabled!==!1});if(filter){const f=filter.toLowerCase();list=list.filter(function(v){return(v.id||"").toLowerCase().includes(f)||(v.name||"").toLowerCase().includes(f)})}else genderMatch&&(list=list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg.startsWith(genderMatch)||!vg}).concat(list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg&&!vg.startsWith(genderMatch)})));const ul=popup.querySelector(".lib-vp-list");ul.innerHTML=list.slice(0,500).map(function(v){return'
'+escHtml(v.id||v.name||"")+(v.gender?' \xB7 '+escHtml(v.gender)+"":"")+"
"}).join("")+(list.length===0?'
No voices found
':""),ul.querySelectorAll(".lib-vp-item").forEach(function(item){item.addEventListener("click",async function(){const vid=item.dataset.vid;await clPut(Object.assign({},rec,{voice:vid,updated:new Date})),rec.voice=vid,_syncVoicePictureFromChar(rec),popup.remove(),onDone()})})}renderList(""),popup.querySelector(".lib-vp-input").addEventListener("input",function(e){renderList(e.target.value)}),voices.length===0&&typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().then(function(){popup.isConnected&&(voices=window._voices||[],renderList(popup.querySelector(".lib-vp-input").value||""))}).catch(function(){}),document.body.appendChild(popup);const rect=cardEl.getBoundingClientRect(),popupWidth=260;popup.style.position="fixed",popup.style.left=Math.max(8,Math.min(rect.left,window.innerWidth-popupWidth-8))+"px",popup.style.width=popupWidth+"px";const spaceBelow=window.innerHeight-rect.bottom;spaceBelow>300||spaceBelow>rect.top?popup.style.top=rect.bottom+4+"px":popup.style.bottom=window.innerHeight-rect.top+4+"px",setTimeout(function(){function close(e){popup.contains(e.target)||(popup.remove(),document.removeEventListener("click",close))}document.addEventListener("click",close)},0),popup.querySelector(".lib-vp-input").focus()}async function _findVoiceFromSameCharacterElsewhere(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey)return null;let all=[];try{all=await clGetAll()}catch{return null}const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null,match=all.find(function(r){if(r.id===rec.id||!r.voice||String(r.name||"").trim().toLowerCase()!==nameKey)return!1;const vId=typeof r.voice=="object"?r.voice.id:r.voice;return!(!_voiceExists(vId)||bookCode&&_voiceLangCode(vId)!==bookCode)});return match?{voiceId:typeof match.voice=="object"?match.voice.id:match.voice,book:match.book}:null}function _voiceLangCode(voiceId){const m=/^([A-Za-z]{2,3})_/.exec(String(voiceId||""));return m?m[1].toUpperCase():null}async function _findVoiceByCharacterName(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey||nameKey.length<3)return null;const hits=(window._voices||[]).filter(function(v){return v.enabled!==!1}).filter(function(v){return String(v.id||v.name||"").toLowerCase().includes(nameKey)});if(!hits.length)return null;const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null;if(bookCode){const langHits=hits.filter(function(v){return _voiceLangCode(v.id)===bookCode});return langHits.length?langHits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]:null}return hits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]}async function _autoAssignVoice(rec){const reuse=await _findVoiceFromSameCharacterElsewhere(rec);if(reuse){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec),toast(reuse.voiceId+" \u2192 "+rec.name+' (reused from "'+reuse.book+'" for series consistency)',"success");return}const named=await _findVoiceByCharacterName(rec);if(named){await clPut(Object.assign({},rec,{voice:named.id,updated:new Date})),rec.voice=named.id,_syncVoicePictureFromChar(rec),toast(named.id+" \u2192 "+rec.name+" (matching voice already in the library)","success");return}if(typeof _charAutoDesignVoice=="function"){await _charAutoDesignVoice(rec);return}toast("No matching voice found","error")}function _charLang(rec){const sh=rec.sheet||{},text=[sh.backstory,sh.voice_pattern,sh.mannerisms,sh.relationships,sh.motivation,sh.archetype].filter(Boolean).join(" ");return typeof detectLang=="function"?detectLang(text):""}const _bookProfileCache=new Map;async function _getBookProfile(book){const key=String(book||"").trim();if(!key)return{};if(_bookProfileCache.has(key))return _bookProfileCache.get(key);let profile={};try{const r=await fetch("/api/book-profile?book="+encodeURIComponent(key));r.ok&&(profile=await r.json())}catch(e){console.warn("[book profile]",e)}return _bookProfileCache.set(key,profile),profile}function _getBookProfileSync(book){return _bookProfileCache.get(String(book||"").trim())||{}}async function _saveBookProfile(book,profile){const key=String(book||"").trim(),r=await fetch("/api/book-profile",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Object.assign({book:key},profile))});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json();return _bookProfileCache.set(key,d.profile||profile),d.profile}const _bookLangCache=new Map;async function _resolveBookLang(rec){const book=rec.book||"",profile=await _getBookProfile(book);if(profile&&profile.language)return profile.language;const direct=_charLang(rec);if(direct)return direct;if(_bookLangCache.has(book))return _bookLangCache.get(book);let lang="";try{const siblings=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(book):[],counts={};siblings.forEach(function(s){const l=_charLang(s);l&&(counts[l]=(counts[l]||0)+1)});let best="",bestN=0;Object.keys(counts).forEach(function(l){counts[l]>bestN&&(best=l,bestN=counts[l])}),lang=best}catch(e){console.warn("[book lang]",e)}return _bookLangCache.set(book,lang),lang}const _VOICE_TEXTURE_POOL=["a warm, breathy timbre","a bright, clear timbre","a low, husky timbre","a crisp, silvery timbre","a soft, velvety timbre","a slightly nasal, reedy timbre","a rich, resonant timbre","a light, airy timbre"],_VOICE_PACE_POOL=["an unhurried, deliberate pace","a quick, energetic pace","a measured, even pace","a pace that quickens when excited or nervous"];function _hashPick(str,pool){let h=0;for(let i=0;i>>0;return pool[h%pool.length]}function _buildVoicePrompt(rec,profile,langName){const sh=rec.sheet||{},g=String(sh.gender||"").toLowerCase(),genderWord=g.startsWith("f")?"female":g.startsWith("m")?"male":"",bits=[],lang=String(langName||"").trim();lang&&lang.toLowerCase()!=="english"?bits.push("Speak with an authentic native "+lang+" accent \u2014 not American-accented, not an English speaker doing "+lang+"."):lang&&bits.push("English with a neutral British or international accent, explicitly not American/US-accented.");const settingBits=[profile&&profile.genre,profile&&profile.setting,profile&&profile.era].filter(Boolean);return settingBits.length&&bits.push("Setting: "+settingBits.join(", ")+"."),bits.push("A "+(genderWord?genderWord+" ":"")+"voice"+(sh.archetype?" for "+sh.archetype.toLowerCase():"")+","),bits.push("with "+_hashPick(rec.name||rec.id||"",_VOICE_TEXTURE_POOL)+" and "+_hashPick((rec.name||rec.id||"")+"_pace",_VOICE_PACE_POOL)+"."),sh.voice_pattern&&bits.push(sh.voice_pattern),sh.mannerisms&&bits.push("Mannerisms: "+sh.mannerisms),sh.physical&&bits.push(sh.physical),sh.alignment&&bits.push("Disposition: "+sh.alignment),bits.join(" ").slice(0,600)}function _selectLoose(sel,val){if(!sel||!val)return;const v=String(val).toLowerCase(),opt=[...sel.options].find(function(o){const ov=o.value.toLowerCase(),ot=o.textContent.toLowerCase();return ov===v||ot===v||ov.startsWith(v)||ot.startsWith(v)||v.startsWith(ov)});opt&&(sel.value=opt.value,sel.dispatchEvent(new Event("change")))}function _charSearchOnline(rec){typeof navTo=="function"&&navTo("s-studio");const lang=_charLang(rec);setTimeout(function(){const fishTab=document.querySelector('#gvo-tabs .gvo-tab[data-src="fish"]');fishTab&&fishTab.click(),setTimeout(function(){const langSel=document.getElementById("fa-lang");langSel&&_selectLoose(langSel,lang);const search=document.getElementById("fa-search");search&&(search.value=rec.name,search.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter",bubbles:!0})))},120)},120),toast("Searching online voices for "+rec.name+(lang?" ("+lang+")":""),"info")}function _editBookProfile(book){_getBookProfile(book).then(function(profile){const ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Book context \u2014 '+escHtml(book)+`

Used in every voice design (and image) prompt for this book, so a fantasy story doesn't end up with 1920s-general portraits or English voices in a German book just because one character's own sheet was too sparse to tell.

',document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector("#bctx-cancel").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#bctx-save").addEventListener("click",async function(){const btn=this;btn.disabled=!0;try{await _saveBookProfile(book,{genre:ov.querySelector("#bctx-genre").value,setting:ov.querySelector("#bctx-setting").value,era:ov.querySelector("#bctx-era").value,language:ov.querySelector("#bctx-lang").value}),toast("Book context saved for "+book,"success"),close()}catch(e){toast("Failed to save: "+(e.message||e),"error"),btn.disabled=!1}})})}function _confirmVoiceReuse(rec,reuse){return new Promise(function(resolve){const ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Existing voice found for '+escHtml(rec.name)+'

"'+escHtml(reuse.voiceId)+'" is already used for '+escHtml(rec.name)+' in "'+escHtml(reuse.book)+'". Reuse it for series consistency, or design a brand-new voice just for this book?

',document.body.appendChild(ov);let audioEl=null;ov.querySelector("#cvr-play").addEventListener("click",async function(e){const btn=e.currentTarget,icon=btn.querySelector(".mdi");if(audioEl&&!audioEl.paused){audioEl.pause(),icon.className="mdi mdi-play";return}btn.disabled=!0,icon.className="mdi mdi-loading mdi-spin";try{const langHint=typeof _resolveBookLang=="function"?await _resolveBookLang(rec).catch(function(){return""}):"",text=typeof _charSampleTextFor=="function"&&_charSampleTextFor(rec,langHint)||"Hallo, ich bin "+rec.name+".",rv=(window._voices||[]).find(x=>x.id===reuse.voiceId),rBackend=rv&&!rv.has_ref?"voice_design":"voice_clone",blob=await fetchTtsPreviewBlob(reuse.voiceId,text,"wav","",rBackend);audioEl||(audioEl=new Audio,audioEl.addEventListener("ended",function(){icon.className="mdi mdi-play"})),audioEl.src=URL.createObjectURL(blob),await audioEl.play(),icon.className="mdi mdi-pause"}catch(err){toast("Could not play sample: "+(err.message||err),"error"),icon.className="mdi mdi-play"}finally{btn.disabled=!1}});const cleanup=function(result){audioEl&&audioEl.pause(),ov.remove(),resolve(result)};ov.querySelector("#cvr-cancel").addEventListener("click",function(){cleanup("cancel")}),ov.querySelector("#cvr-use").addEventListener("click",function(){cleanup("use")}),ov.querySelector("#cvr-new").addEventListener("click",function(){cleanup("new")}),ov.addEventListener("click",function(e){e.target===ov&&cleanup("cancel")})})}async function _charDesignVoice(rec){const reuse=await _findVoiceFromSameCharacterElsewhere(rec);if(reuse){const choice=await _confirmVoiceReuse(rec,reuse);if(choice==="cancel")return;if(choice==="use"){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec),toast(reuse.voiceId+" \u2192 "+rec.name+' (reused from "'+reuse.book+'" for series consistency)',"success");return}}typeof navTo=="function"&&navTo("s-design");const sh=rec.sheet||{},lang=_charLang(rec),savedPrompt=_libStr(sh.voice_design_prompt).trim();setTimeout(function(){_selectLoose(document.getElementById("design-gender"),sh.gender),_selectLoose(document.getElementById("design-language"),lang);const instruct=document.getElementById("design-instruct");instruct&&(instruct.value=savedPrompt||_buildVoicePrompt(rec,null,lang));const nm=document.getElementById("design-preset-name");nm&&(nm.value=rec.name)},140),toast("Voice design prepared for "+rec.name+(lang?" \xB7 "+lang:""),"info")}function _charDesignVoiceInline(rec){const sh=rec.sheet||{},lang=_charLang(rec),instruct=_libStr(sh.voice_design_prompt).trim()||_buildVoicePrompt(rec,null,lang),ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Voice design prompt \u2014 '+escHtml(rec.name)+'

Edit the description, then generate a new voice from it. This replaces '+(rec.voice?"the current voice":"this character\u2019s voice")+'.

',document.body.appendChild(ov);const close=function(){ov.remove()};ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#cdi-cancel").addEventListener("click",close),ov.querySelector("#cdi-generate").addEventListener("click",async function(e){const btn=e.currentTarget,text=ov.querySelector("#cdi-instruct").value.trim();if(!text){toast("Prompt is empty","error");return}btn.disabled=!0;const icon=btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-loading mdi-spin");try{await _charAutoDesignVoice(rec,!0,text),_schedulePendingTtsRestart(),close(),toast("New voice designed for "+rec.name,"success"),typeof libraryRenderCharacters=="function"&&libraryRenderCharacters(),typeof _libRefreshDetailModal=="function"&&_libRefreshDetailModal(rec)}catch(err){toast("Voice design failed: "+(err.message||err),"error"),btn.disabled=!1,icon&&(icon.className="mdi mdi-creation")}})}function _charCloneVoice(rec){typeof navTo=="function"&&navTo("s-clone"),setTimeout(function(){const nm=document.getElementById("clone-your-name");nm&&(nm.value=rec.name)},140),toast("Clone a Voice prepared for "+rec.name+" \u2014 pick a mic take, file, or YouTube URL","info")}const _DESIGN_SAMPLE_FALLBACK={German:"Ich habe lange auf diesen Moment gewartet, und jetzt, da er da ist, wei\xDF ich genau, was zu tun ist.",English:"I have waited a long time for this moment, and now that it is here, I know exactly what to do."};function _charRealLine(rec){const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook,nameLower=String(rec.name||"").trim().toLowerCase(),line=(ab&&ab.segments||[]).find(function(s){return s&&s.type==="dialogue"&&String(s.speaker||"").trim().toLowerCase()===nameLower&&s.text&&s.text.trim().length>=20&&s.text.trim().length<=200});if(line)return line.text.trim();const quotes=(Array.isArray(rec.sheet&&rec.sheet.sources)?rec.sheet.sources:[]).map(function(s){return s&&s.quote?String(s.quote).trim():""}).filter(function(q){return q.length>=20&&q.length<=240}),spoken=quotes.find(function(q){return/[""„"]/.test(q)});return spoken||(quotes.length?quotes[0]:null)}function _charSampleTextFor(rec,langNameHint){const lang=_charLang(rec)||langNameHint||"English",greeting=lang==="German"?`Hallo, ich bin ${rec.name}.`:`Hello, I am ${rec.name}.`,line=_charRealLine(rec);return line?`${greeting} ${line}`:_DESIGN_SAMPLE_FALLBACK[lang]||_DESIGN_SAMPLE_FALLBACK.English}const _GENERIC_NAME_GENDER={frau:"female",dame:"female",junge_frau:"female",m\u00E4dchen:"female",maedchen:"female",mann:"male",herr:"male",junge:"male",knabe:"male"};function _genderFromGenericName(name){const key=String(name||"").trim().toLowerCase().replace(/\s+/g,"_");return _GENERIC_NAME_GENDER[key]||""}let _voiceRestartPending=!1;async function _flushPendingTtsRestart(){if(_voiceRestartPending){_voiceRestartPending=!1;try{const r=await fetch("/api/tts/restart",{method:"POST"});r.ok?toast("TTS backend restarted to pick up the newly designed voice(s)","success"):console.warn("[tts restart] failed:",r.status)}catch(e){console.warn("[tts restart]",e)}}}let _voiceRestartDebounceTimer=null;function _schedulePendingTtsRestart(){clearTimeout(_voiceRestartDebounceTimer),_voiceRestartDebounceTimer=setTimeout(_flushPendingTtsRestart,4e3)}async function _fetchRetryingNetworkErrors(url,opts,tries){tries=tries||3;for(let i=1;i<=tries;i++)try{return await fetch(url,opts)}catch(e){if(i===tries)throw e;await new Promise(function(r){setTimeout(r,2500*i)})}}function _designBenchmarkWpmBad(b){if(!b||!b.ok||!b.audio_sec||!b.text)return!1;const wpm=String(b.text).trim().split(/\s+/).length/(b.audio_sec/60);return wpm<80||wpm>400}async function _charAutoDesignVoice(rec,force,instructOverride){const reuse=force||instructOverride?null:await _findVoiceFromSameCharacterElsewhere(rec);if(reuse&&reuse.voiceId!==rec.voice){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec);return}const sh=rec.sheet||{},langName=await _resolveBookLang(rec)||"English",instruct=instructOverride||_buildVoicePrompt(rec,await _getBookProfile(rec.book),langName);if(!instruct.trim())throw new Error("No character description to design a voice from yet");const langCode=typeof DESIGN_LANG_CODE!="undefined"&&DESIGN_LANG_CODE[langName]||"EN",genderWord=String(sh.gender||"").toLowerCase()||_genderFromGenericName(rec.name),genderLetter=genderWord.startsWith("f")?"F":genderWord.startsWith("m")?"M":"N",sampleText=_charSampleTextFor(rec,langName),dialogue=typeof isDialogueDesign=="function"?isDialogueDesign(instruct,sampleText,null):!1,baseName=typeof designSafeName=="function"?designSafeName(rec.name):(typeof _umlautSafe=="function"?_umlautSafe(rec.name||"VoiceDesign"):String(rec.name||"VoiceDesign")).replace(/[^A-Za-z0-9]+/g,"_"),voiceId=(langCode+"_"+genderLetter+"_"+baseName).slice(0,96),maxAttempts=3;let saved=null;for(let attempt=1;attempt<=maxAttempts;attempt++){const r1=await _fetchRetryingNetworkErrors("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({instruct,sample_text:sampleText,language:langName,gender:genderLetter,dialogue})});if(!r1.ok){const e=await r1.json().catch(function(){return{}});throw new Error(e.detail||r1.statusText)}const designed=await r1.json(),tryId=(voiceId+"__try"+attempt).slice(0,96),r2=await _fetchRetryingNetworkErrors("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designed.id,voice_id:tryId,transcript:sampleText})});if(!r2.ok){const e=await r2.json().catch(function(){return{}});throw new Error(e.detail||r2.statusText)}await r2.json();let bad=!1;if(typeof runVoiceBenchmark=="function")try{const d=await runVoiceBenchmark(tryId,{text:sampleText}),hit=(d&&d.voices||[]).find(function(x){return x.voice_id===tryId}),b=hit&&hit.benchmark;!b||!b.ok&&/connection (refused|reset|aborted)|max retries exceeded|newconnectionerror|econnrefused|timed? ?out/i.test(String(b.error||""))?console.warn("[voice design] benchmark unreachable, accepting unverified:",b&&b.error):bad=!!(b.clipped||_designBenchmarkWpmBad(b))}catch(e){console.warn("[voice benchmark]",e)}if(!bad&&typeof _voiceRoundtripCheck=="function")try{const rt=await _voiceRoundtripCheck(tryId,sampleText,"voice_design");rt.score<.5&&(bad=!0,console.warn("[voice design] STT roundtrip mismatch (score "+rt.score.toFixed(2)+'): said "'+rt.transcript+'"'))}catch(e){console.warn("[voice design roundtrip]",e)}if(!bad){const r3=await _fetchRetryingNetworkErrors("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designed.id,voice_id:voiceId,transcript:sampleText})});if(!r3.ok){const e=await r3.json().catch(function(){return{}});throw new Error(e.detail||r3.statusText)}saved=await r3.json(),saved.needs_tts_restart&&(_voiceRestartPending=!0),await fetch("/api/voice/"+encodeURIComponent(tryId),{method:"DELETE"}).catch(function(){});break}if(await fetch("/api/voice/"+encodeURIComponent(tryId),{method:"DELETE"}).catch(function(){}),attempt===maxAttempts)throw new Error('Voice design for "'+voiceId+'" produced broken audio after '+maxAttempts+" attempts \u2014 left the previous voice in place, try again later")}return typeof saveMeta=="function"&&await saveMeta(saved.voice_id,{gender:genderLetter,flag:typeof LANG_FLAG_DEFAULT!="undefined"?LANG_FLAG_DEFAULT[langCode]:void 0,origin:"designed",group:rec.book||void 0,tag:rec.book||void 0,transcript:sampleText,note:"Voice Design: "+instruct.slice(0,240),voice_design_prompt:instruct}).catch(function(){}),rec.voice=saved.voice_id,await clUpsert(rec.book,Object.assign({},rec.sheet,{name:rec.name,voice:saved.voice_id}),rec.id),_syncVoicePictureFromChar(rec),saved.voice_id}async function _charAutoGenerateImage(rec,provider){const sh=rec.sheet||{},hasExplicitPrompt=!!_libStr(sh.image_prompt).trim();if(!hasExplicitPrompt&&[sh.archetype,sh.physical,sh.clothing].filter(Boolean).join(" ").trim().length<20)throw new Error("Not enough character detail to generate a meaningful portrait \u2014 skipped instead of using a generic placeholder");const bookProfile=typeof _getBookProfile=="function"?await _getBookProfile(rec.book):{},prompt=hasExplicitPrompt?_libStr(sh.image_prompt).trim():typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh,bookProfile):"";if(!prompt)throw new Error("No image prompt to work from yet");const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json();return typeof clSetImage=="function"&&await clSetImage(rec.id,d.image),rec.image=d.image,document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),_syncVoicePictureFromChar(rec),d.image}async function _charAutoGenerateConceptArt(rec,provider){const sh=rec.sheet||{},prompt=_libStr(sh.concept_art_prompt).trim();if(!prompt)throw new Error("No concept art prompt to work from yet \u2014 generate the prompt first");const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json(),target=(typeof clGet=="function"?await clGet(rec.id).catch(function(){return null}):null)||rec;return target.sheet=Object.assign({},target.sheet||{},{concept_art_image:d.image}),target.updated=new Date,typeof clPut=="function"&&await clPut(target),sh.concept_art_image=d.image,rec.sheet=sh,d.image}window._charSearchOnline=_charSearchOnline,window._charDesignVoice=_charDesignVoice,window._charAutoDesignVoice=_charAutoDesignVoice,window._charAutoGenerateImage=_charAutoGenerateImage,window._charAutoGenerateConceptArt=_charAutoGenerateConceptArt,window.libraryRenderCharacters=libraryRenderCharacters;let _stuActive=1;const _stuHomes=new Map;function _stuBorrow(id,slotId){const el=document.getElementById(id),slot=document.getElementById(slotId);!el||!slot||(_stuHomes.has(id)||_stuHomes.set(id,{parent:el.parentNode,next:el.nextSibling}),slot.appendChild(el))}function _stuReturnAll(){_stuIsActive=!1,typeof _stuRestoreCastFoot=="function"&&_stuRestoreCastFoot(),_stuHomes.forEach(function(home,id){const el=document.getElementById(id);el&&home.parent&&home.parent.insertBefore(el,home.next)}),_stuHomes.clear()}window._stuReturnAll=_stuReturnAll;let _stuIsActive=!1,_stuAllowNextNav=!1;const _STU_BORROWED_FROM={"s-reader":1,"s-library":1,"s-rehearser":1};let _stuNavGuardInstalled=!1;function _stuInstallNavGuardOnce(){if(_stuNavGuardInstalled)return;_stuNavGuardInstalled=!0;const realNavTo=window.navTo;window.navTo=function(id){if(!(_stuIsActive&&!_stuAllowNextNav&&_STU_BORROWED_FROM[id]))return _stuAllowNextNav=!1,realNavTo(id)};const realShowReaderView=window.showReaderView;let _stuInShowReaderView=!1;window.showReaderView=function(view){const result=typeof realShowReaderView=="function"?realShowReaderView(view):void 0;if(_stuIsActive&&!_stuInShowReaderView&&(view==="cast"||view==="chars")){const target=view==="chars"?"sheets":"identify";if(_stuCastView!==target){_stuInShowReaderView=!0;try{_stuShowCastView(target)}finally{_stuInShowReaderView=!1}}}return result},document.querySelectorAll('[data-nav-section="s-reader"], #nav-reader-tree, [data-nav-section="s-library"], #nav-library-tree, [data-nav-section="s-rehearser"], #nav-rehearser-tree').forEach(function(el){el.addEventListener("click",function(){_stuAllowNextNav=!0},!0)})}function _stuCallSuppressingNav(fn){return fn()}function _stuEnterPhase(n){if(n===1)_stuBorrow("reader-main-view","stu-source-slot"),typeof window.readerOnShow=="function"&&_stuCallSuppressingNav(window.readerOnShow),_stuBorrow("lib-books-list","stu-books-slot"),_stuCallSuppressingNav(function(){typeof window.libraryRenderBooks=="function"&&window.libraryRenderBooks()});else if(n===2)_stuShowCastView(_stuCastView);else if(n===3)_stuBorrow("lib-chars-list","stu-voices-slot"),(async()=>{let title=null;if(window._audiobook&&window._audiobook.bookId)try{const r=await fetch("/api/reader/docs/"+encodeURIComponent(window._audiobook.bookId));r.ok&&(title=(await r.json()).title||null)}catch{}window._libCharsScrollToBook=title||window.readerState&&readerState.title||null,_stuCallSuppressingNav(function(){typeof window.libraryRender=="function"&&window.libraryRender("characters")})})();else if(n===4){_stuBorrow("reh-phase-3","stu-stage-slot"),_stuBorrow("reh-cast-list","stu-mecast-slot"),_stuBorrow("reh-tone-warn","stu-tone-warn-slot"),typeof _abInitPauseUI=="function"&&_abInitPauseUI(),_stuCallSuppressingNav(async function(){if(window.rehState&&rehState.lines&&rehState.lines.length){typeof buildScriptPage=="function"&&buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine();return}if(!(window._audiobook&&window._audiobook.segments&&window._audiobook.segments.length)&&typeof _abLoadDraftServer=="function"&&typeof _abBookId=="function"){const bookId=_abBookId(),draft=bookId?await _abLoadDraftServer(bookId):null;draft&&(_audiobook.segments=draft.segments||[],_audiobook.roster=draft.roster||[],_audiobook.pageMarks=draft.pageMarks||[],_audiobook.rehId=draft.rehId||_audiobook.rehId||null)}if(typeof window.audiobookOpenCurrentInRehearser=="function"&&(window._audiobook&&window._audiobook.segments||[]).length)return window.audiobookOpenCurrentInRehearser()});const rp3=document.getElementById("reh-phase-3");rp3&&(rp3.hidden=!1),_stuSyncModeToggle()}}function _stuSyncModeToggle(){const cb=document.getElementById("stu-mode-audiobook"),details=document.getElementById("stu-mecast-details");!cb||!window.rehState||(cb.checked=!rehState.skipDescriptions,details&&(details.hidden=cb.checked))}(_tc=document.getElementById("stu-mode-audiobook"))==null||_tc.addEventListener("change",function(){const audiobookMode=this.checked;if(window.rehState){rehState.skipDescriptions=!audiobookMode;const t=document.getElementById("reh-skip-desc-toggle");t&&(t.checked=rehState.skipDescriptions),audiobookMode&&!rehState.narratorVoice&&typeof toast=="function"&&toast("No narrator voice assigned yet \u2014 assign one in the Voices phase so narration actually plays","error")}const details=document.getElementById("stu-mecast-details");details&&(details.hidden=audiobookMode)});let _stuCastView="identify";function _stuShowCastView(view){_stuCastView=view,document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(t){t.classList.toggle("active",t.dataset.stuCastView===view)});const identifySlot=document.getElementById("stu-cast-slot"),sheetsSlot=document.getElementById("stu-castchars-slot");if(identifySlot&&(identifySlot.hidden=view!=="identify"),sheetsSlot&&(sheetsSlot.hidden=view!=="sheets"),view==="identify")_stuBorrow("reader-audiobook-panel","stu-cast-slot"),_stuCallSuppressingNav(function(){if(typeof window.audiobookOpenCastView=="function")return window.audiobookOpenCastView();typeof window.showReaderView=="function"&&window.showReaderView("cast")}),_stuRelocateCastFoot();else if(view==="sheets"){_stuBorrow("reader-charsheets-panel","stu-castchars-slot");const panel=document.getElementById("reader-charsheets-panel");if(panel&&!panel.innerHTML.trim()){panel.innerHTML='
Character sheets

Optional \u2014 let the AI fill out full character profiles (appearance, backstory, voice notes) for reference. Skip this if you just want to cast voices quickly.

';const goBtn=document.getElementById("stu-goto-cast-menu");goBtn&&goBtn.addEventListener("click",function(){typeof window.csForReader=="function"&&window.csForReader()})}}}document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(tab){tab.addEventListener("click",function(){_stuShowCastView(tab.dataset.stuCastView)})});let _stuCastFootObserver=null;function _stuRelocateCastFoot(){if(_stuTryRelocateCastFoot(),_stuCastFootObserver)return;const slot=document.getElementById("stu-cast-slot");slot&&(_stuCastFootObserver=new MutationObserver(function(){_stuTryRelocateCastFoot()}),_stuCastFootObserver.observe(slot,{childList:!0,subtree:!0}))}function _stuTryRelocateCastFoot(){const slot=document.getElementById("stu-cast-slot"),tabs=document.getElementById("stu-cast-inner-tabs");if(!tabs)return;const freshFoot=slot?slot.querySelector("#ab-cv-foot"):null,alreadyRelocated=tabs.querySelector("#ab-cv-foot");if(!freshFoot&&!alreadyRelocated){document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!1});return}if(!freshFoot||freshFoot.parentElement===tabs)return;tabs.querySelectorAll("#ab-cv-foot").forEach(function(stale){stale.remove()}),freshFoot.style.borderTop="none",freshFoot.style.padding="0",freshFoot.style.justifyContent="flex-start",tabs.appendChild(freshFoot),document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!0});const openReh=freshFoot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!0)}function _stuRestoreCastFoot(){_stuCastFootObserver&&(_stuCastFootObserver.disconnect(),_stuCastFootObserver=null);const tabs=document.getElementById("stu-cast-inner-tabs"),panel=document.getElementById("reader-audiobook-panel"),foot=tabs?tabs.querySelector("#ab-cv-foot"):null;if(foot&&panel){foot.style.borderTop="",foot.style.padding="",foot.style.justifyContent="";const openReh=foot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!1),panel.appendChild(foot)}document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!1})}function showStudioPhase(n){_stuActive=n;for(let i=1;i<=4;i++){const el=document.getElementById("stu-phase-"+i);el&&(el.hidden=i!==n)}document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.classList.toggle("active",parseInt(tab.dataset.stuPhase,10)===n)}),document.querySelectorAll("#nav-caststudio-tree [data-stu-phase]").forEach(function(item){item.classList.toggle("is-active",parseInt(item.dataset.stuPhase,10)===n)});const prevBtn=document.getElementById("stu-phase-prev"),nextBtn=document.getElementById("stu-phase-next");prevBtn&&(prevBtn.disabled=n<=1),nextBtn&&(nextBtn.disabled=n>=4),typeof _stuEnterPhase=="function"&&_stuEnterPhase(n)}window.showStudioPhase=showStudioPhase,document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.addEventListener("click",function(){showStudioPhase(parseInt(tab.dataset.stuPhase,10))})}),(_uc=document.getElementById("stu-phase-prev"))==null||_uc.addEventListener("click",function(){_stuActive>1&&showStudioPhase(_stuActive-1)}),(_vc=document.getElementById("stu-phase-next"))==null||_vc.addEventListener("click",function(){_stuActive<4&&showStudioPhase(_stuActive+1)});function studioOnShow(){_stuInstallNavGuardOnce(),_stuIsActive=!0,showStudioPhase(_stuActive)}window.studioOnShow=studioOnShow; +`),personality:persona||prev.personality||"",scenario:rec.book||prev.scenario||"",first_mes:prev.first_mes||"",mes_example:sh.voice_pattern||prev.mes_example||"",creator_notes:"Exported from TTS Voice Creator"+(rec.book?" \xB7 "+rec.book:""),system_prompt:prev.system_prompt||"",post_history_instructions:prev.post_history_instructions||"",tags:String(rec.tags||rec.book||"").split(",").map(t=>t.trim()).filter(Boolean),creator:prev.creator||"",character_version:prev.character_version||"1.0",extensions:Object.assign({},prev.extensions,{tts_voice:rec.voice||""})}}}function stDownloadJson(card,filename){const blob=new Blob([JSON.stringify(card,null,2)],{type:"application/json"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3)}async function stImportCards(book,fileList){let n=0;for(const file of fileList)try{const data=await stParseFile(file),sheet=stToSheet(data);typeof clUpsert=="function"&&(await clUpsert(book||sheet.name,Object.assign({},sheet,{tags:book||""})),n++)}catch(e){typeof toast=="function"&&toast("\u201C"+(file.name||"card")+"\u201D: "+(e.message||e),"error")}return n}function stImportDialog(book,onDone){const inp=document.createElement("input");inp.type="file",inp.accept=".json,.png",inp.multiple=!0,inp.onchange=async()=>{if(!inp.files.length)return;const n=await stImportCards(book,inp.files);typeof toast=="function"&&toast(n?"Imported "+n+" character"+(n>1?"s":""):"Nothing imported",n?"success":"error"),typeof onDone=="function"&&onDone()},inp.click()}function stExportRecord(rec){const card=stFromRecord(rec),safe=String(rec.name||"character").replace(/[^\w\- ]+/g,"").trim().replace(/\s+/g,"_")||"character";stDownloadJson(card,safe+".card.json")}window.stParseFile=stParseFile,window.stToSheet=stToSheet,window.stFromRecord=stFromRecord,window.stImportCards=stImportCards,window.stImportDialog=stImportDialog,window.stExportRecord=stExportRecord;const LIB_READER_API="/api/reader/docs";function prodKey(title){return String(title||"").trim().toLowerCase()}window._libraryView=function(){try{return localStorage.getItem("ttsvc_library_view")||"books"}catch{return"books"}}(),window.navLibraryView=function(view){typeof navTo=="function"&&navTo("s-library"),window._libraryView=view;try{localStorage.setItem("ttsvc_library_view",view)}catch{}document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="characters"&&typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib"),libraryRender(view)},window.libraryRender=function(view){view=view||window._libraryView||"books",document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="books"?libraryRenderBooks():view==="plays"?libraryRenderPlays():view==="characters"&&typeof window.libraryRenderCharacters=="function"&&window.libraryRenderCharacters()};function _libSkeleton(n){return Array.from({length:n},()=>'
').join("")}async function libraryRenderBooks(){const list=document.getElementById("lib-books-list");if(!list)return;list.innerHTML=_libSkeleton(4);let all=[];try{const r=await fetch(LIB_READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='

No books yet.

Open Read Aloud, import a PDF or text, and save it to your library.

';return}all.sort(function(a,b){return new Date(b.updated||0)-new Date(a.updated||0)}),list.innerHTML=all.map(function(rec){const total=rec.sentenceCount||0,synthPct=total?Math.round((rec.synthCount||0)/total*100):0,readPct=total?Math.round((rec.idx||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"",cov=libBookCover(rec.title||"Untitled"),coverUrl=LIB_READER_API+"/"+rec.id+"/cover?t="+new Date(rec.updated||Date.now()).getTime(),bg=rec.hasCover?`style="background-image:linear-gradient(to bottom,rgba(0,0,0,.3),rgba(0,0,0,.8)),url('`+coverUrl+`');background-size:cover;background-position:center;color:#fff"`:'style="--bk1:'+cov.c1+";--bk2:"+cov.c2+'"';return'
'+escHtml(rec.title||"Untitled")+'
'+total+" sentences"+(rec.pageCount?" \xB7 "+rec.pageCount+" pg":"")+'
'+readPct+"% read \xB7 "+date+"
"}).join(""),list.querySelectorAll(".lib-book").forEach(function(el){const id=el.dataset.id,title=el.dataset.title;el.addEventListener("click",function(e){e.target.closest(".reh-book-act")||(window._readerStartView="main",typeof navTo=="function"&&navTo("s-reader"),typeof readerOpenLibraryDoc=="function"&&readerOpenLibraryDoc(id))});const reh=el.querySelector(".lib-act-rehearse");reh&&reh.addEventListener("click",function(e){e.stopPropagation(),productionOpenInRehearser(title)});const del=el.querySelector(".lib-act-del-book");del&&del.addEventListener("click",function(e){e.stopPropagation(),libConfirmDelete(el,"Delete book?","Audio files will be removed.",async function(){try{if(!(await fetch(LIB_READER_API+"/"+id,{method:"DELETE"})).ok)throw new Error("delete failed");toast("Book deleted","success"),libraryRenderBooks()}catch(err){toast(err.message||"Delete failed","error")}})})})}async function libraryRenderPlays(){const list=document.getElementById("lib-plays-list");if(!list)return;list.innerHTML=_libSkeleton(3);let all=[];try{all=typeof rehDbGetAll=="function"?await rehDbGetAll():[]}catch{all=[]}if(!all.length){list.innerHTML='

No theater plays yet.

Open Script Rehearsal \u2192 Import / Export to add a script, or cast a book as an audiobook.

';return}all.sort(function(a,b){return new Date(b.updated||0)-new Date(a.updated||0)}),list.innerHTML=all.map(function(rec){const speakers=Object.keys(rec.cast||{}),total=typeof parseScript=="function"?parseScript(rec.script||"").filter(function(l){return l.type==="dialog"}).length:0,pct=total?Math.round((rec.lineIndex||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"\u2014",cov=libBookCover(rec.title||"Untitled"),avatars=speakers.slice(0,5).map(function(sp){return''+(sp[0]||"?").toUpperCase()+""}).join("");return'
'+escHtml(rec.title||"Untitled")+'
'+avatars+"
"+total+" lines \xB7 "+speakers.length+' cast
'+pct+"% \xB7 "+date+"
"}).join(""),list.querySelectorAll(".lib-play").forEach(function(el){const id=parseInt(el.dataset.id,10),title=el.dataset.title;el.addEventListener("click",function(e){e.target.closest(".reh-book-act")||openPlayInRehearser(id)});const ra=el.querySelector(".lib-act-readaloud");ra&&ra.addEventListener("click",function(e){e.stopPropagation(),productionOpenInReader(title)});const del=el.querySelector(".lib-act-del-play");del&&del.addEventListener("click",function(e){e.stopPropagation(),libConfirmDelete(el,"Delete rehearsal?","This cannot be undone.",async function(){try{typeof rehDbDelete=="function"&&await rehDbDelete(id),toast("Rehearsal deleted","success"),libraryRenderPlays()}catch(err){toast(err.message||"Delete failed","error")}})})})}async function openPlayInRehearser(id){try{const rec=typeof rehDbGetById=="function"?await rehDbGetById(id):null;rec&&typeof loadRecord=="function"?loadRecord(rec):toast("Rehearsal not found","error")}catch{toast("Could not open rehearsal","error")}}async function productionOpenInRehearser(title){const key=prodKey(title);try{const match=(typeof rehDbGetAll=="function"?await rehDbGetAll():[]).find(function(p){return prodKey(p.title)===key});if(match){openPlayInRehearser(match.id);return}}catch{}let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&book.kind!=="pdf")try{const sr=await fetch(LIB_READER_API+"/"+book.id+"/source"),text=sr.ok?await sr.text():"";if(text&&typeof audiobookOpenInRehearser=="function"){audiobookOpenInRehearser(text,title,[]);return}}catch{}if(book&&book.kind==="pdf"){typeof readerOpenLibraryDoc=="function"&&readerOpenLibraryDoc(book.id),toast('Open this PDF book, then use "Cast as audiobook" to build a rehearsal',"info");return}toast("No source to rehearse for this title yet","error")}async function productionOpenInReader(title){const key=prodKey(title);let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&typeof readerOpenLibraryDoc=="function"){readerOpenLibraryDoc(book.id);return}typeof navTo=="function"&&navTo("s-reader"),toast("No audiobook for this title yet \u2014 import its source in Read Aloud","info")}async function castForProduction(title){const out={};if(typeof clGetAllByTagOrBook!="function")return out;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{recs=[]}return recs.forEach(function(r){const name=(r.name||"").trim();if(!name)return;const voice=r.voice&&r.voice.id?r.voice.id:typeof r.voice=="string"?r.voice:"";out[name.toLowerCase()]={name,voice:voice||"",gender:r.sheet&&r.sheet.gender||"",soul:r.sheet&&(r.sheet.voice_pattern||r.sheet.motivation)||"",tags:r.tags||""}}),out}window.castForProduction=castForProduction;async function castWriteBack(title,castMap){if(!title||!castMap||typeof clGetAllByTagOrBook!="function"||typeof clPut!="function")return;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{return}if(!recs.length)return;const byName={};recs.forEach(function(r){byName[(r.name||"").trim().toLowerCase()]=r});let n=0;for(const sp of Object.keys(castMap)){if(String(sp).includes("NARRATOR"))continue;const voice=(castMap[sp]||{}).voice;if(!voice||voice==="me")continue;const rec=byName[String(sp).trim().toLowerCase()];if(!(!rec||(rec.voice&&rec.voice.id?rec.voice.id:typeof rec.voice=="string"?rec.voice:"")===voice)){rec.voice={id:voice},rec.updated=new Date;try{await clPut(rec),n++}catch{}}}return n}window.castWriteBack=castWriteBack;function libBookCover(title){let h=0;const s=String(title||"Untitled");for(let i=0;i'+heading+'
'+sub+'
',o.addEventListener("click",function(e){e.stopPropagation()}),o.querySelector("[data-lib-cancel]").addEventListener("click",function(e){e.stopPropagation(),o.remove()}),o.querySelector("[data-lib-ok]").addEventListener("click",async function(e){e.stopPropagation(),o.innerHTML='',await onConfirm()}),cardEl.appendChild(o)}window.libraryRenderBooks=libraryRenderBooks,window.libraryRenderPlays=libraryRenderPlays,window.productionOpenInRehearser=productionOpenInRehearser,window.productionOpenInReader=productionOpenInReader,window.prodKey=prodKey;async function libraryRenderCharacters(){var _a2;const container=document.getElementById("lib-chars-list");if(!container)return;let all=[];try{all=typeof clGetAll=="function"?await clGetAll():[]}catch(e){console.warn("[characters] load failed",e),typeof toast=="function"&&toast("Failed to load characters \u2014 keeping the current view","error");return}const mainEl=document.getElementById("main-content"),savedScrollTop=!window._libCharsScrollToBook&&mainEl?mainEl.scrollTop:null;container.innerHTML='
Loading characters\u2026
';const byId=new Map(all.map(function(rec){return[rec.id,rec]}));if(!all.length){container.innerHTML='

No characters yet.

Open a book in Read Aloud, cast it as an audiobook, then click Cast Characters to generate character sheets \u2014 or import an existing cast from SillyTavern.

',container.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),savedScrollTop!=null&&(mainEl.scrollTop=savedScrollTop);return}const byBook={};all.forEach(function(rec){const bk=rec.book||"Unsorted";byBook[bk]||(byBook[bk]=[]),byBook[bk].push(rec)}),Object.keys(byBook).forEach(function(bk){if(!byBook[bk].some(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"})){const narrRec={id:clKey(bk,"Narrator"),book:bk,name:"Narrator",tags:bk,voice:null,image:null,sheet:{}};byId.set(narrRec.id,narrRec),byBook[bk].unshift(narrRec)}}),container.innerHTML="";const viewMode=localStorage.getItem("ttsvc_libchars_view")==="table"?"table":"cards";let returnToReader=!1;try{returnToReader=sessionStorage.getItem("ttsvc_cast_return")==="reader"}catch{}const SORT_OPTIONS=[["tier","Rolle (Haupt zuerst)"],["alpha","Alphabet"],["lines","Anzahl Zeilen"],["gender","Geschlecht"],["voice","Stimme zugewiesen"],["rating","Stimmenbewertung"]],sortMode=SORT_OPTIONS.some(function(o){return o[0]===localStorage.getItem("ttsvc_libchars_sort")})?localStorage.getItem("ttsvc_libchars_sort"):"tier",bar=document.createElement("div");bar.className="lib-chars-toolbar",bar.innerHTML=(returnToReader?'':"")+'
',bar.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),(_a2=bar.querySelector("#lib-chars-back-reader"))==null||_a2.addEventListener("click",function(){try{sessionStorage.removeItem("ttsvc_cast_return")}catch{}typeof navTo=="function"&&navTo("s-reader")}),bar.querySelector("#lib-chars-sort-sel").addEventListener("change",function(){localStorage.setItem("ttsvc_libchars_sort",this.value),libraryRenderCharacters()}),bar.querySelectorAll(".lib-chars-view-toggle button").forEach(function(btn){btn.addEventListener("click",function(){localStorage.setItem("ttsvc_libchars_view",btn.dataset.view),libraryRenderCharacters()})}),container.appendChild(bar);const _charSortCmp={tier:function(a,b){var _a3,_b2,_c2,_d2;const tierOrder={main:0,supporting:1,minor:2},ta=(_b2=tierOrder[String(((_a3=a.sheet)==null?void 0:_a3.tier)||"minor").toLowerCase()])!=null?_b2:2,tb=(_d2=tierOrder[String(((_c2=b.sheet)==null?void 0:_c2.tier)||"minor").toLowerCase()])!=null?_d2:2;return ta-tb||(a.name||"").localeCompare(b.name||"")},alpha:function(a,b){return(a.name||"").localeCompare(b.name||"")},lines:function(a,b){var _a3,_b2;return(((_a3=b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)||(a.name||"").localeCompare(b.name||"")},gender:function(a,b){var _a3,_b2;const ga=String(((_a3=a.sheet)==null?void 0:_a3.gender)||"zzz"),gb=String(((_b2=b.sheet)==null?void 0:_b2.gender)||"zzz");return ga.localeCompare(gb)||(a.name||"").localeCompare(b.name||"")},voice:function(a,b){return(b.voice?1:0)-(a.voice?1:0)||(a.name||"").localeCompare(b.name||"")},rating:function(a,b){const idOf=function(r){return r.voice?typeof r.voice=="object"?r.voice.id:r.voice:""},ratingOf=function(r){var _a3;return Number(((_a3=(window._voices||[]).find(function(v){return v.id===idOf(r)}))==null?void 0:_a3.rating)||0)};return ratingOf(b)-ratingOf(a)||(a.name||"").localeCompare(b.name||"")},age:function(a,b){return _charAgeSortVal(a.sheet)-_charAgeSortVal(b.sheet)||(a.name||"").localeCompare(b.name||"")},language:function(a,b){return _charLangLabel(a).localeCompare(_charLangLabel(b))||(a.name||"").localeCompare(b.name||"")},align:function(a,b){var _a3,_b2,_c2,_d2;return((_b2=(_a3=b.sheet)==null?void 0:_a3.moral_alignment_score)!=null?_b2:-1)-((_d2=(_c2=a.sheet)==null?void 0:_c2.moral_alignment_score)!=null?_d2:-1)||(a.name||"").localeCompare(b.name||"")}},sortDir=localStorage.getItem("ttsvc_libchars_sort_dir")==="desc"?"desc":"asc",productions=document.createDocumentFragment();if(Object.keys(byBook).sort().forEach(function(book){const chars=byBook[book].sort(_charSortCmp[sortMode]||_charSortCmp.tier);sortDir==="desc"&&chars.reverse();const narrIdx=chars.findIndex(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"});narrIdx>0&&chars.unshift(chars.splice(narrIdx,1)[0]);const cov=libBookCover(book),prod=document.createElement("div");prod.className="lib-chars-production",prod.dataset.book=book;const collapseKey="ttsvc_libchars_collapsed::"+book;let isCollapsed=localStorage.getItem(collapseKey)==="1";window._libCharsScrollToBook&&(isCollapsed=book!==window._libCharsScrollToBook),isCollapsed&&prod.classList.add("lib-chars-production-collapsed"),prod.innerHTML='
'+escHtml(book)+'
`+(viewMode==="table"?_charsTableHtml(chars,sortMode,sortDir):'
'+chars.map(function(rec){return _charCardHtml(rec,chars)}).join("")+"
")+"
",prod.querySelector(".lib-chars-prod-collapse-btn").addEventListener("click",function(e){e.stopPropagation();const collapsed=prod.classList.toggle("lib-chars-production-collapsed");localStorage.setItem(collapseKey,collapsed?"1":"0")}),prod.querySelector(".lib-chars-prod-head").addEventListener("click",function(e){e.target.closest("button, select, input, a")||prod.querySelector(".lib-chars-prod-collapse-btn").click()}),prod.querySelector(".lib-chars-bookctx-btn").addEventListener("click",function(){_editBookProfile(book)}),prod.querySelector(".lib-chars-casting-btn").addEventListener("click",function(){typeof navTo=="function"&&navTo("s-reader")}),prod.querySelector(".lib-chars-cast-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book),toast("Open the book in Read Aloud then click Cast Characters","info")}),prod.querySelector(".lib-chars-reh-btn").addEventListener("click",function(){typeof productionOpenInRehearser=="function"&&productionOpenInRehearser(book)}),prod.querySelector(".lib-chars-read-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book)}),prod.querySelector(".lib-chars-imp-btn").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog(book,function(){libraryRenderCharacters()})}),prod.querySelectorAll("[data-sort-key]").forEach(function(th){th.addEventListener("click",function(){const key=th.dataset.sortKey,nextDir=sortMode===key&&sortDir==="asc"?"desc":"asc";localStorage.setItem("ttsvc_libchars_sort",key),localStorage.setItem("ttsvc_libchars_sort_dir",nextDir),libraryRenderCharacters()})});const selectAllBtn=prod.querySelector(".lib-chars-select-all-btn"),bulkBtn=prod.querySelector(".lib-chars-bulk-voice-btn"),bulkCount=prod.querySelector(".lib-chars-bulk-count"),designBtn=prod.querySelector(".lib-chars-bulk-design-btn"),designCount=prod.querySelector(".lib-chars-bulk-count-design"),imageBtn=prod.querySelector(".lib-chars-bulk-image-btn"),imageCount=prod.querySelector(".lib-chars-bulk-count-image"),imageProviderSel=prod.querySelector(".lib-chars-image-provider");imageProviderSel&&(imageProviderSel.value=typeof _appSettings!="undefined"&&_appSettings.image_gen_provider||"");const deleteBtn=prod.querySelector(".lib-chars-bulk-delete-btn"),deleteCount=prod.querySelector(".lib-chars-bulk-count-delete"),tblSelectAllCb=prod.querySelector(".lib-chars-tbl-select-all-cb"),syncTblSelectAllCb=function(){if(!tblSelectAllCb)return;const boxes=[...prod.querySelectorAll(".lib-char-select-cb")],checkedN=boxes.filter(function(cb){return cb.checked}).length;tblSelectAllCb.checked=boxes.length>0&&checkedN===boxes.length,tblSelectAllCb.indeterminate=checkedN>0&&checkedN0&&boxes.every(function(cb){return cb.checked});boxes.forEach(function(cb){cb.checked=!allChecked}),refreshBulkBtn()}),tblSelectAllCb&&tblSelectAllCb.addEventListener("change",function(){[...prod.querySelectorAll(".lib-char-select-cb")].forEach(function(cb){cb.checked=tblSelectAllCb.checked}),refreshBulkBtn()}),syncTblSelectAllCb();const runBulk=async function(btn,ids,verb,fn){btn.disabled=!0;const orig=btn.innerHTML;let done=0,failed=0,lastErrMsg="",repeatErrMsg="",repeatCount=0,aborted=!1;for(const id of ids){const rec=byId.get(id);if(rec){btn.innerHTML=' '+verb+" "+(done+failed+1)+" / "+ids.length+"\u2026";try{await fn(rec),done++,repeatCount=0}catch(e){if(failed++,lastErrMsg=e&&e.message?e.message:String(e),console.error("[bulk "+verb+"]",rec.name,e),lastErrMsg===repeatErrMsg?repeatCount++:(repeatErrMsg=lastErrMsg,repeatCount=1),repeatCount>=3){aborted=!0;break}}}}btn.innerHTML=orig;const remaining=ids.length-done-failed,suffix=failed?` (${failed} failed${aborted&&remaining?`, ${remaining} skipped`:""}${lastErrMsg?": "+lastErrMsg.slice(0,200):""})`:"";toast(`${verb} finished for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),await _flushPendingTtsRestart(),typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary({refresh:!0}).catch(()=>{}),libraryRenderCharacters()};bulkBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(bulkBtn,ids,"Assigning",_autoAssignVoice)}),designBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(designBtn,ids,"Designing",_charAutoDesignVoice)}),imageBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});if(!ids.length)return;const provider=imageProviderSel?imageProviderSel.value:"";runBulk(imageBtn,ids,"Generating images",function(rec){return _charAutoGenerateImage(rec,provider)})});const fixLangBtn=prod.querySelector(".lib-chars-fix-lang-btn");fixLangBtn==null||fixLangBtn.addEventListener("click",function(){const _bookLangCounts={};chars.forEach(function(r){const l=_charLang(r);l&&(_bookLangCounts[l]=(_bookLangCounts[l]||0)+1)});let _bookLang="",_bookLangBest=0;Object.keys(_bookLangCounts).forEach(function(l){_bookLangCounts[l]>_bookLangBest&&(_bookLang=l,_bookLangBest=_bookLangCounts[l])});const bookCode=_bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[_bookLang]:null,mismatched=chars.filter(function(rec){if(!rec.voice||!bookCode)return!1;const voiceId=typeof rec.voice=="object"?rec.voice.id:rec.voice;return _voiceLangCode(voiceId)!==bookCode});if(!mismatched.length){toast("No language-mismatched voices found in this production","info");return}runBulk(fixLangBtn,mismatched.map(function(r){return r.id}),"Redesigning",function(rec){return _charAutoDesignVoice(rec,!0)})}),deleteBtn.addEventListener("click",async function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});!ids.length||!await confirmDialog(`Delete ${ids.length} character${ids.length!==1?"s":""} from the library? This cannot be undone \u2014 use it to clear out stale/corrupted entries before a fresh recast.`,{title:"Delete characters?",okLabel:"Delete",danger:!0})||runBulk(deleteBtn,ids,"Deleting",function(rec){return clDelete(rec.id)})}),_wireCharCards(prod,byId,chars),prod.querySelectorAll(".lib-chars-tbl-rating-star").forEach(function(star){star.addEventListener("click",async function(e){e.stopPropagation();const voiceId=star.dataset.voiceId,voice=(window._voices||[]).find(function(v){return v.id===voiceId});if(!voice||typeof saveMeta!="function")return;const value=Number(star.dataset.rating||0);voice.rating=value===Number(voice.rating||0)?0:value,prod.querySelectorAll(".lib-chars-tbl-rating-star").forEach(function(btn){btn.dataset.voiceId===voiceId&&btn.classList.toggle("is-on",Number(btn.dataset.rating)<=voice.rating)});try{await saveMeta(voiceId,{rating:voice.rating})}catch(err){toast("Could not save voice rating: "+(err.message||err),"error")}})}),prod.querySelectorAll(".lib-chars-tbl-voice-note").forEach(function(input){let timer=null;input.addEventListener("click",function(e){e.stopPropagation()}),input.addEventListener("input",function(){clearTimeout(timer),timer=setTimeout(async function(){const voiceId=input.dataset.voiceId,voice=(window._voices||[]).find(function(v){return v.id===voiceId});if(!(!voice||typeof saveMeta!="function")){voice.note=input.value;try{await saveMeta(voiceId,{note:voice.note})}catch(err){toast("Could not save voice note: "+(err.message||err),"error")}}},600)})}),productions.appendChild(prod)}),container.appendChild(productions),window._libCharsScrollToBook){const target=window._libCharsScrollToBook;window._libCharsScrollToBook=null;const prodEl=[...container.querySelectorAll(".lib-chars-production")].find(function(p){return p.dataset.book===target});prodEl&&(prodEl.scrollIntoView({behavior:"smooth",block:"start"}),prodEl.classList.add("lib-chars-production-highlight"),setTimeout(function(){prodEl.classList.remove("lib-chars-production-highlight")},2200))}else savedScrollTop!=null&&(mainEl.scrollTop=savedScrollTop)}function _charHue(name){return Math.abs((name||"?").split("").reduce(function(h,c){return(h*31+c.charCodeAt(0))%360},0))}function _charAlignHtml(sh){const score=sh.moral_alignment_score;if(score==null)return"";const pct=Math.max(0,Math.min(100,score)),arc=sh.arc_direction||"neutral",arrowMap={"good-to-bad":{ch:"\u2198",color:"#ff7043",tip:"Arc: Descends toward evil"},"bad-to-good":{ch:"\u2197",color:"#66bb6a",tip:"Arc: Redeems toward good"},complex:{ch:"\u2195",color:"#ab47bc",tip:"Arc: Complex / unpredictable"},"stable-good":{ch:"\u2192",color:"#66bb6a",tip:"Arc: Stable good"},"stable-bad":{ch:"\u2192",color:"#888",tip:"Arc: Stable evil"},neutral:{ch:"\u2192",color:"#aaa",tip:"Arc: Neutral"}},a=arrowMap[arc]||arrowMap.neutral;return'
\u25CF
\u25CF'+a.ch+"
"}function _libStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _charAgeLabel(sh){return _libStr((sh==null?void 0:sh.age_estimate)||"").trim()}function _charAgeSortVal(sh){const m=_charAgeLabel(sh).match(/\d+/);return m?parseInt(m[0],10):9999}function _charLangLabel(rec){const sh=(rec==null?void 0:rec.sheet)||{},voiceLang=rec!=null&&rec.voice&&typeof rec.voice=="object"&&rec.voice.language||"";return _libStr(sh.languages||voiceLang).trim()}function _charGenderLabel(sh){const gender=String((sh==null?void 0:sh.gender)||"").trim();return gender?gender.charAt(0).toUpperCase()+gender.slice(1):""}function _charRelsHtml(rec,allChars){var _a2;if(!allChars||allChars.length<2)return"";const relText=_libStr((_a2=rec.sheet)==null?void 0:_a2.relationships).toLowerCase();if(!relText)return"";const hits=allChars.filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,5);return hits.length?'
'+hits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":""}let _avatarHoverEl=null;function _showAvatarHoverPreview(rec,anchorEl){if(!rec.image)return;_avatarHoverEl||(_avatarHoverEl=document.createElement("div"),_avatarHoverEl.className="lib-avatar-hover-preview",_avatarHoverEl.innerHTML="",document.body.appendChild(_avatarHoverEl)),_avatarHoverEl.querySelector("img").src=rec.image;const rect=anchorEl.getBoundingClientRect(),size=512;let left=rect.right+12;left+size>window.innerWidth&&(left=rect.left-size-12);let top=rect.top+rect.height/2-size/2;top=Math.max(8,Math.min(top,window.innerHeight-size-8)),_avatarHoverEl.style.left=Math.max(8,left)+"px",_avatarHoverEl.style.top=top+"px",_avatarHoverEl.hidden=!1}function _hideAvatarHoverPreview(){_avatarHoverEl&&(_avatarHoverEl.hidden=!0)}function _wireCharCards(root,recsById,allRecs,onChange,detailOpts){const refresh=onChange||libraryRenderCharacters;root.querySelectorAll(".lib-char-card").forEach(function(card){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2;const charId=card.dataset.charId,rec=recsById.get?recsById.get(charId):recsById[charId];if(rec){if(card.addEventListener("click",function(e){e.target.closest("button, .lib-char-avatar, .lib-voice-picker-popup")||_charDetailPage(rec,allRecs,detailOpts)}),(_a2=card.querySelector(".lib-char-avatar"))==null||_a2.addEventListener("click",function(e){e.stopPropagation(),_openAvatarLightbox(rec,refresh)}),rec.image){const avatarEl=card.querySelector(".lib-char-avatar");avatarEl==null||avatarEl.addEventListener("mouseenter",function(){_showAvatarHoverPreview(rec,avatarEl)}),avatarEl==null||avatarEl.addEventListener("mouseleave",_hideAvatarHoverPreview)}(_b2=card.querySelector(".lib-char-voice-pill"))==null||_b2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_c2=card.querySelector(".lib-char-pick-voice"))==null||_c2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_d2=card.querySelector(".lib-char-auto-voice"))==null||_d2.addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),refresh()}),(_e2=card.querySelector(".lib-char-redesign-voice"))==null||_e2.addEventListener("click",async function(e){e.stopPropagation();const btn=e.currentTarget;btn.disabled=!0;try{await _charAutoDesignVoice(rec,!0),_schedulePendingTtsRestart(),refresh()}catch(err){toast("Voice design failed: "+(err.message||err),"error")}finally{btn.disabled=!1}}),(_f2=card.querySelector(".lib-char-remove-voice"))==null||_f2.addEventListener("click",async function(e){e.stopPropagation(),await clPut(Object.assign({},rec,{voice:"",updated:new Date})),rec.voice="",_syncVoicePictureFromChar(rec),toast("Voice removed from "+rec.name,"success"),refresh()}),(_g2=card.querySelector(".lib-char-export"))==null||_g2.addEventListener("click",function(e){e.stopPropagation(),typeof stExportRecord=="function"&&stExportRecord(rec)}),(_h2=card.querySelector(".lib-char-online-voice"))==null||_h2.addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),(_i2=card.querySelector(".lib-char-gen-voice"))==null||_i2.addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),(_j2=card.querySelector(".lib-char-voice-play"))==null||_j2.addEventListener("click",function(e){e.stopPropagation(),_libPreviewCharVoice(rec,e.currentTarget)})}})}let _libVoicePreviewEl=null,_libVoicePreviewBtn=null;function _libStopVoicePreview(){if(_libVoicePreviewEl&&(_libVoicePreviewEl.pause(),_libVoicePreviewEl.src=""),_libVoicePreviewBtn){_libVoicePreviewBtn.classList.remove("playing","loading");const icon=_libVoicePreviewBtn.querySelector(".mdi");icon&&(icon.className="mdi mdi-play")}_libVoicePreviewBtn=null}async function _libPreviewCharVoice(rec,btn){const voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"";if(!voiceId){toast("No voice assigned yet","error");return}if(_libVoicePreviewBtn===btn&&_libVoicePreviewEl&&!_libVoicePreviewEl.paused){_libStopVoicePreview();return}_libStopVoicePreview();const icon=btn.querySelector(".mdi"),v=(window._voices||[]).find(x=>x.id===voiceId);if(!v||!v.path||typeof voiceFileUrl!="function"){toast("Voice file not found","error");return}btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{_libVoicePreviewEl||(_libVoicePreviewEl=new Audio,_libVoicePreviewEl.addEventListener("ended",_libStopVoicePreview)),_libVoicePreviewEl.src=voiceFileUrl(v),await _libVoicePreviewEl.play(),btn.classList.remove("loading"),_libVoicePreviewBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _voiceExists(voiceId){if(!voiceId)return!0;const voices=window._voices||[];return voices.length?voices.some(function(v){return v.id===voiceId}):!0}function _charCardHtml(rec,allChars){const sh=rec.sheet||{},hue=_charHue(rec.name),hue2=(hue+40)%360,voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",snippet=_libStr(sh.mannerisms||sh.voice_pattern||sh.motivation||sh.backstory||"").slice(0,190),roleLine=_libStr(sh.profession||sh.archetype).trim(),ageLabel=_charAgeLabel(sh),langLabel=_charLangLabel(rec),genderLabel=_charGenderLabel(sh),bookLabel=_libStr(rec.book||""),lineLabel=sh.line_count!=null?String(sh.line_count)+" Zeilen":"",tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),tagsHtml=tagList.length?'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+"
":"",hasPhoto=!!rec.image,bannerStyle=hasPhoto?'style="background-image:linear-gradient(180deg, rgba(0,0,0,.05) 0%, rgba(0,0,0,.72) 100%), url("'+rec.image+'"); background-size:cover; background-position:center;"':'style="--ch1:hsl('+hue+",52%,35%);--ch2:hsl("+hue2+',56%,26%)"',avatarInner=hasPhoto?'':escHtml((rec.name||"?")[0].toUpperCase()),stat=function(label,value,icon){return value?'
'+escHtml(label)+''+escHtml(value)+"
":""},metaChips=[];return bookLabel&&metaChips.push(' '+escHtml(bookLabel)+""),'
'+avatarInner+'
'+(voiceId?'':"")+'
'+escHtml(rec.name)+''+tierBadge+"
"+(roleLine?'
'+escHtml(roleLine)+"
":"")+(_libStr(sh.title)?'
Titel: '+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
aka '+escHtml(_libStr(sh.aliases))+"
":"")+'
'+metaChips.join("")+'
'+stat("Occupation",_libStr(sh.profession),"mdi-briefcase-outline")+stat("Archetype",_libStr(sh.archetype),"mdi-shape-outline")+stat("Gender",genderLabel,"mdi-gender-male-female")+stat("Age",ageLabel,"mdi-cake-variant")+stat("Lines",lineLabel,"mdi-format-list-numbered")+"
"+_charAlignHtml(sh)+"
"}function _charsTableHtml(chars,sortMode,sortDir){const arrow=function(key){return sortMode===key?' ':""},th=function(key,label,title){return'"+label+arrow(key)+""},rows=chars.map(function(rec){const sh=rec.sheet||{},hue=_charHue(rec.name),voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMeta=voiceId?(window._voices||[]).find(function(v){return v.id===voiceId}):null,voiceMissing=!!voiceId&&!_voiceExists(voiceId),voiceRating=Math.max(0,Math.min(5,Number((voiceMeta==null?void 0:voiceMeta.rating)||0))),voiceNote=_libStr(voiceMeta==null?void 0:voiceMeta.note),voicePrompt=_libStr((voiceMeta==null?void 0:voiceMeta.voice_design_prompt)||sh.voice_design_prompt),stars=voiceId?'
'+[1,2,3,4,5].map(function(n){return''}).join("")+"
":'\u2014',voiceLang=_charLangLabel(rec),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":gender?"mdi-gender-non-binary":"",genderLabel=_charGenderLabel(sh),score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),avatarInner=rec.image?''+escHtml(rec.name)+'':escHtml((rec.name||"?")[0].toUpperCase()),occupation=_libStr(sh.profession),ageLabel=_charAgeLabel(sh),bookLabel=_libStr(rec.book||""),aliasLabel=_libStr(sh.aliases),archetype=_libStr(sh.archetype),titleLabel=_libStr(sh.title),tableMeta=[aliasLabel?"aka "+aliasLabel:"",occupation?"Occupation: "+occupation:"",titleLabel?"Title: "+titleLabel:"",archetype?"Archetype: "+archetype:""].filter(Boolean).join(" \xB7 ");return'
'+avatarInner+'
'+tierBadge+escHtml(rec.name)+"
"+(tableMeta?'
'+escHtml(tableMeta)+"
":"")+(bookLabel?'
'+escHtml(bookLabel)+"
":"")+""+(genderLabel?escHtml(genderLabel):'\u2014')+""+(ageLabel?escHtml(ageLabel):'\u2014')+""+(sh.line_count!=null?sh.line_count:'\u2014')+""+(voiceLang?escHtml(voiceLang):'\u2014')+""+(pct!=null?'
':'\u2014')+'
'+(voiceId?'"+(voiceMissing?' ':"")+escHtml(voiceId)+"":'Keine Stimme')+'
'+(voiceId?'':"")+''+(voiceId?'':"")+'
'+stars+''+(voicePrompt?escHtml(voicePrompt):'\u2014')+''+(voiceId?'':'\u2014')+'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+'
'}).join("");return'
'+th("alpha","Name")+th("gender","Geschlecht")+th("age","Alter","Estimated age")+th("lines","Zeilen","Anzahl Zeilen")+th("language","Sprache")+th("align","Gut/B\xF6se","Moralische Gesinnung")+th("voice","Stimme")+th("rating","Rating","Rate the assigned voice")+''+rows+"
Voice Design PromptNotesTagsBook / Script
"}function _lcdSourcesHtml(sources){const list=Array.isArray(sources)?sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[];return list.length?'
'+list.map(function(s){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":""}function _lcdField(label,value,multiline){const v=_libStr(value);return v?'
'+label+'
'+escHtml(v)+"
":""}function _lcdSection(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdSectionFull(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdPromptBox(label,value,sheetKey){const has=!!(value&&String(value).trim());return'
'+escHtml(label)+(has?"":' \u2014 not generated yet')+'
'+escHtml(value||"")+'
"}function _lcdFieldEdit(label,value,sheetKey,sourceIdxs){const v=_libStr(value),links=(sourceIdxs||[]).map(function(idx){return''+(sourceIdxs.indexOf(idx)+1)+""}).join("");return'
'+(label||links?'
'+escHtml(label)+(links?' '+links+"":"")+"
":"")+'
'+escHtml(v)+"
"}function _jumpToReaderPage(pageNum){typeof navTo=="function"&&navTo("s-reader"),setTimeout(function(){var _a2,_b2;const pages=(_a2=window.readerState)==null?void 0:_a2.pages;if(pages&&pages.length>=pageNum){const pg=pages[pageNum-1];if(pg!=null&&pg.pageDiv){pg.pageDiv.scrollIntoView({behavior:"smooth",block:"start"});return}}const sentences=(_b2=window.readerState)==null?void 0:_b2.sentences;if(sentences&&sentences.length){const target0=pageNum-1,idx=sentences.findIndex(function(s){return(s.words||[]).some(function(w){var _a3,_b3;return((_b3=(_a3=w.page)!=null?_a3:w.para)!=null?_b3:0)>=target0})});if(idx>=0&&typeof readerJumpTo=="function"){readerJumpTo(idx);return}}toast('\xD6ffne das Buch in \u201EVorlesen" und klicke nochmal auf die Quelle',"info")},300)}function _charLineCount(c){if(window.rehState&&rehState.lines&&rehState.lines.length){const key=String(c.name||"").toUpperCase().trim(),live=rehState.lines.filter(function(l){return l.type==="dialog"&&String(l.speaker||"").toUpperCase().trim()===key}).length;if(live)return live}return Number(c.sheet&&c.sheet.line_count)||0}async function _charDetailPage(rec,allChars,opts){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2;opts=opts||{},window._libDetailOpts=opts;const container=opts.container||document.getElementById("lib-chars-list");if(!container)return;const goBack=typeof opts.onBack=="function"?opts.onBack:libraryRenderCharacters;window._libDetailRec=rec;const sh=rec.sheet||{},hue=_charHue(rec.name),hue2=(hue+40)%360,tier=String(sh.tier||"").toLowerCase(),tierLabel=tier==="main"?"Hauptcharakter":tier==="supporting"?"Nebencharakter":tier==="minor"?"Nebenfigur":"",gender=_libStr(sh.gender),genderIcon=gender.toLowerCase().startsWith("f")?"mdi-gender-female":gender.toLowerCase().startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,arcMap={"good-to-bad":{ch:"\u2198",label:"Entwicklung zum B\xF6sen",color:"#ff7043"},"bad-to-good":{ch:"\u2197",label:"Wandel zum Guten",color:"#66bb6a"},complex:{ch:"\u2195",label:"Komplex / unvorhersehbar",color:"#ab47bc"},"stable-good":{ch:"\u2192",label:"Stabil gut",color:"#66bb6a"},"stable-bad":{ch:"\u2192",label:"Stabil b\xF6se",color:"#888"},neutral:{ch:"\u2192",label:"Neutral / stabil",color:"#aaa"}},arcInfo=arcMap[sh.arc_direction||"neutral"]||arcMap.neutral,avatarHtml='
'+(rec.image?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
")+"
",conceptArtHtml='
"+(sh.concept_art_image?'
Concept art \u2014 '+escHtml(rec.name)+'
':'
'+(_libStr(sh.concept_art_prompt).trim()?"Kein Konzeptbild":"Kein Konzeptbild-Prompt \u2014 erst unten bei Generation Prompts erzeugen")+"
")+"
",alignHtml=pct!=null?'
B\xF6seGut'+pct+'/100
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",promptsHtml='
'+_lcdPromptBox("Voice Design Prompt",sh.voice_design_prompt,"voice_design_prompt")+_lcdPromptBox("Character Image Prompt",sh.image_prompt,"image_prompt")+_lcdPromptBox("SillyTavern Character Prompt",sh.silly_tavern_prompt,"silly_tavern_prompt")+_lcdPromptBox("Concept Art Prompt",sh.concept_art_prompt,"concept_art_prompt")+"
",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",sourcesList=Array.isArray(sh.sources)?sh.sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[],sourcesByField={};sourcesList.forEach(function(s,idx){const key=_libStr(s.line_hint||s.hint||"").trim().toLowerCase();key&&(sourcesByField[key]=sourcesByField[key]||[]).push(idx)});const sourcesHtml=sourcesList.length?'
'+sourcesList.map(function(s,idx){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":"",sidebarHtml=(allChars||[]).slice().sort(function(a,b){return _charLineCount(b)-_charLineCount(a)}).map(function(c){const h=_charHue(c.name),count=_charLineCount(c);return'
'+escHtml((c.name||"?")[0].toUpperCase())+''+escHtml(c.name)+""+(count?''+count+"":"")+"
"}).join("");container.innerHTML="";const pg=document.createElement("div");pg.className="lib-char-page",pg.style.gridColumn="1 / -1",pg.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+'
'+escHtml(_libStr(sh.aliases))+'
'+escHtml(_libStr(sh.archetype))+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+conceptArtHtml+'
'+(voiceId?'':"")+''+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdFieldEdit("Voller Name",sh.full_name,"full_name",sourcesByField.full_name),_lcdFieldEdit("Vorname",sh.first_name,"first_name",sourcesByField.first_name),_lcdFieldEdit("Nachname",sh.last_name,"last_name",sourcesByField.last_name),_lcdFieldEdit("Geschlecht",sh.gender,"gender",sourcesByField.gender),_lcdFieldEdit("Titel",sh.title,"title",sourcesByField.title),_lcdFieldEdit("Beruf / Rolle",sh.profession,"profession",sourcesByField.profession),_lcdFieldEdit("Auch bekannt als",sh.aliases,"aliases",sourcesByField.aliases)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdFieldEdit("K\xF6rperlich",sh.physical,"physical",sourcesByField.physical),_lcdFieldEdit("Kleidung & Aussehen",sh.clothing,"clothing",sourcesByField.clothing)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdFieldEdit("Eigenheiten & Verhalten",sh.mannerisms,"mannerisms",sourcesByField.mannerisms),_lcdFieldEdit("Stimme & Sprache",sh.voice_pattern,"voice_pattern",sourcesByField.voice_pattern)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdFieldEdit("Hintergrund & Herkunft",sh.backstory,"backstory",sourcesByField.backstory),_lcdFieldEdit("Motivation",sh.motivation,"motivation",sourcesByField.motivation),_lcdFieldEdit("\xC4ngste",sh.fears,"fears",sourcesByField.fears)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdFieldEdit("Fertigkeiten",sh.skills,"skills",sourcesByField.skills),_lcdFieldEdit("Besondere F\xE4higkeiten",sh.capabilities,"capabilities",sourcesByField.capabilities),_lcdFieldEdit("St\xE4rkstes Attribut",sh.attribute_high,"attribute_high"),_lcdFieldEdit("Schw\xE4chstes Attribut",sh.attribute_low,"attribute_low")])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdFieldEdit("",sh.relationships,"relationships",sourcesByField.relationships),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdFieldEdit("Konfliktstil",sh.conflict_style,"conflict_style",sourcesByField.conflict_style),_lcdFieldEdit("Siegbedingung",sh.win_condition,"win_condition",sourcesByField.win_condition)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdFieldEdit("Dunkles Geheimnis / fataler Fehler",sh.secret,"secret"),_lcdFieldEdit("Charakterentwicklung",sh.arc_note,"arc_note")])+promptsHtml+"
"+sourcesHtml+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+'
Charaktere \xB7 '+escHtml(rec.book||"")+"
"+sidebarHtml+"
",container.appendChild(pg),pg.querySelector(".lib-cpg-back").addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),Promise.resolve(goBack()).catch(function(err){console.error("[character detail] back navigation failed",err),toast("Character list could not be opened","error")})}),(_a2=pg.querySelector(".lcd-avatar-upload"))==null||_a2.addEventListener("click",function(){_openAvatarLightbox(rec,function(){_charDetailPage(rec,allChars,opts)})}),(_b2=pg.querySelector(".lcd-conceptart-gen"))==null||_b2.addEventListener("click",async function(e){e.stopPropagation();const btn=this;if(!_libStr(sh.concept_art_prompt).trim()){toast("No Concept Art Prompt yet \u2014 generate that first in Generation Prompts below","error");return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='';try{await _charAutoGenerateConceptArt(rec),toast("Concept art generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Concept art generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}}),(_c2=pg.querySelector(".lcd-conceptart-img, .lcd-conceptart-empty"))==null||_c2.addEventListener("click",function(){_openConceptArtLightbox(rec,function(){_charDetailPage(rec,allChars,opts)})}),pg.querySelectorAll(".lib-cpg-sidebar-item").forEach(function(item){item.addEventListener("click",async function(){const target=(allChars||[]).find(function(c){return c.id===item.dataset.charId});target&&_charDetailPage(target,allChars,opts)})}),pg.querySelectorAll(".lcd-source-clickable").forEach(function(item){item.addEventListener("click",function(){const n=parseInt(item.dataset.page,10);isNaN(n)||_jumpToReaderPage(n)})}),(_d2=pg.querySelector(".lcd-pick-voice"))==null||_d2.addEventListener("click",async function(){_openVoicePicker(pg.querySelector(".lcd-voice-top"),rec,async function(){const all=await clGetAll().catch(()=>allChars),up=all.find(function(r){return r.id===rec.id})||rec;_charDetailPage(up,all.filter(function(r){return r.book===rec.book}),opts)})}),(_e2=pg.querySelector(".lcd-auto-voice"))==null||_e2.addEventListener("click",async function(){await _autoAssignVoice(rec);const all=await clGetAll().catch(()=>allChars),up=all.find(function(r){return r.id===rec.id})||rec;_charDetailPage(up,all.filter(function(r){return r.book===rec.book}),opts)}),(_f2=pg.querySelector(".lcd-online-voice"))==null||_f2.addEventListener("click",function(){_charSearchOnline(rec)}),(_g2=pg.querySelector(".lcd-gen-voice"))==null||_g2.addEventListener("click",function(){_charDesignVoiceInline(rec)}),(_h2=pg.querySelector(".lcd-clone-voice"))==null||_h2.addEventListener("click",function(){_charCloneVoice(rec)}),(_i2=pg.querySelector(".lcd-preview-voice"))==null||_i2.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),_libPreviewCharVoice(rec,e.currentTarget)}),pg.querySelectorAll(".lcd-prompt-copy").forEach(function(btn){btn.addEventListener("click",async function(){var _a3;const box=btn.closest(".lcd-prompt-body"),text=((_a3=box==null?void 0:box.querySelector(".lcd-prompt-text"))==null?void 0:_a3.textContent.trim())||"";if(!text){toast("Nothing to copy yet \u2014 click Generate first","error");return}typeof copyText=="function"&&await copyText(text),toast("Prompt copied","success")})}),pg.querySelectorAll(".lcd-gen-prompt").forEach(function(btn){btn.addEventListener("click",async function(e){e.preventDefault();const key=btn.dataset.sheetKey,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026';try{const sh2=rec.sheet||{},sample=[sh2.physical,sh2.backstory,sh2.motivation].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""},r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:rec.name,book:rec.book||"",sheet:sh2,language,llm_url:target.url,model:target.model,fields:[key]})});if(!r.ok)throw new Error((await r.json().catch(function(){return{}})).detail||r.statusText);const d=await r.json();if(!d[key])throw new Error("Empty response \u2014 try again");rec.sheet||(rec.sheet={}),rec.sheet[key]=d[key],rec.updated=new Date,typeof clPut=="function"&&await clPut(rec),toast("Prompt generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Prompt generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}})});const slider=pg.querySelector(".lcd-align-slider"),sliderVal=pg.querySelector(".lcd-align-slider-val"),arcEl=pg.querySelector(".lcd-align-arc");slider&&slider.addEventListener("input",async function(){const val=parseInt(slider.value,10);sliderVal&&(sliderVal.textContent=val+"/100"),arcEl&&(arcEl.textContent=arcInfo.ch+" "+arcInfo.label+(val>=70?" \xB7 Rechtschaffen ("+val+"/100)":val<=30?" \xB7 B\xF6se ("+val+"/100)":" \xB7 Moralisch ambivalent ("+val+"/100)")),arcEl&&(arcEl.style.color=arcInfo.color),rec.sheet.moral_alignment_score=val,rec.updated=new Date,typeof clPut=="function"&&await clPut(rec)});const _saveTimers=new Map;function _schedSave(key,value,isRecKey){clearTimeout(_saveTimers.get(key)),_saveTimers.set(key,setTimeout(async function(){isRecKey?rec[key]=value:(rec.sheet||(rec.sheet={}),rec.sheet[key]=value),rec.updated=new Date,typeof clPut=="function"&&await clPut(rec)},900))}pg.querySelectorAll("[contenteditable][data-sheet-key]").forEach(function(el){el.addEventListener("input",function(){_schedSave(el.dataset.sheetKey,el.textContent.trim(),!1)})}),pg.querySelectorAll("[contenteditable][data-rec-key]").forEach(function(el){el.addEventListener("input",function(){_schedSave(el.dataset.recKey,el.textContent.trim(),!0)})})}window._charDetailPage=_charDetailPage;function _charDetailModal(rec,allChars){const sh=rec.sheet||{},cov=libBookCover(rec.name),hue=_charHue(rec.name),tier=String(sh.tier||"").toLowerCase(),tierLabel=tier==="main"?"Hauptcharakter":tier==="supporting"?"Nebencharakter":tier==="minor"?"Nebenfigur":"",gender=_libStr(sh.gender),genderIcon=gender.toLowerCase().startsWith("f")?"mdi-gender-female":gender.toLowerCase().startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,arc=sh.arc_direction||"neutral",arcMap={"good-to-bad":{ch:"\u2198",label:"Entwicklung zum B\xF6sen",color:"#ff7043"},"bad-to-good":{ch:"\u2197",label:"Wandel zum Guten",color:"#66bb6a"},complex:{ch:"\u2195",label:"Komplex / unvorhersehbar",color:"#ab47bc"},"stable-good":{ch:"\u2192",label:"Stabil gut",color:"#66bb6a"},"stable-bad":{ch:"\u2192",label:"Stabil b\xF6se",color:"#888"},neutral:{ch:"\u2192",label:"Neutral / stabil",color:"#aaa"}},arcInfo=arcMap[arc]||arcMap.neutral,voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",avatarHtml=rec.image?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
",alignHtml=pct!=null?'
B\xF6se
Gut
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.arc_note)?'
'+escHtml(_libStr(sh.arc_note))+"
":"")+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",ov=document.createElement("div");ov.className="lib-char-detail-ov",ov.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
auch bekannt als '+escHtml(_libStr(sh.aliases))+"
":"")+(_libStr(sh.archetype)?'
'+escHtml(_libStr(sh.archetype))+"
":"")+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdField("Voller Name",sh.full_name,!0),_lcdField("Vorname",sh.first_name,!0),_lcdField("Nachname",sh.last_name,!0),_lcdField("Geschlecht",sh.gender,!0),_lcdField("Titel",sh.title,!0),_lcdField("Beruf / Rolle",sh.profession,!0),_lcdField("Auch bekannt als",sh.aliases,!0)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdField("K\xF6rperlich",sh.physical,!0),_lcdField("Kleidung & Aussehen",sh.clothing,!0)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdField("Eigenheiten & Verhalten",sh.mannerisms,!0),_lcdField("Stimme & Sprache",sh.voice_pattern,!0)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdField("Hintergrund & Herkunft",sh.backstory,!0),_lcdField("Motivation",sh.motivation,!0),_lcdField("\xC4ngste",sh.fears,!0)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdField("Fertigkeiten",sh.skills,!0),_lcdField("Besondere F\xE4higkeiten",sh.capabilities,!0),_lcdField("St\xE4rkstes Attribut",sh.attribute_high,!1),_lcdField("Schw\xE4chstes Attribut",sh.attribute_low,!1)])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdField("",sh.relationships,!0),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdField("Konfliktstil",sh.conflict_style,!0),_lcdField("Siegbedingung",sh.win_condition,!0)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdField("Dunkles Geheimnis / fataler Fehler",sh.secret,!0),_lcdField("Charakterentwicklung",sh.arc_note,!0)])+"
"+_lcdSourcesHtml(sh.sources)+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+"
",document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector(".lcd-close-btn").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector(".lcd-edit-btn").addEventListener("click",function(){close(),typeof clEdit=="function"&&clEdit(rec.id)}),ov.querySelector(".lcd-pick-voice").addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){close(),libraryRenderCharacters()})}),ov.querySelector(".lcd-auto-voice").addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),close(),libraryRenderCharacters()}),ov.querySelector(".lcd-online-voice").addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),ov.querySelector(".lcd-gen-voice").addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),ov.querySelector(".lcd-avatar").addEventListener("click",function(){const inp=document.createElement("input");inp.type="file",inp.accept="image/*",inp.onchange=async function(){const file=inp.files[0];if(!file)return;const fr=new FileReader;fr.onload=async function(ev){typeof clSetImage=="function"&&await clSetImage(rec.id,ev.target.result),toast("Profile picture saved","success"),close(),libraryRenderCharacters()},fr.readAsDataURL(file)},inp.click()})}window._charDetailModal=_charDetailModal;function _openAvatarLightbox(rec,onSaved){var _a2;(_a2=document.getElementById("avatar-lightbox"))==null||_a2.remove();const sh=rec.sheet||{},currentPrompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh,_getBookProfileSync(rec.book)):""),ov=document.createElement("div");ov.id="avatar-lightbox",ov.className="audiobook-overlay",ov.innerHTML='
'+escHtml(rec.name)+' \u2014 Profilbild
'+(rec.image?''+escHtml(rec.name)+'':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
")+'
',document.body.appendChild(ov),ov.addEventListener("click",function(e){e.target===ov&&ov.remove()}),ov.querySelector("#alb-close").addEventListener("click",function(){ov.remove()}),ov.querySelector("#alb-upload-btn").addEventListener("click",function(){ov.querySelector("#alb-file-input").click()}),ov.querySelector("#alb-online-focus-btn").addEventListener("click",function(){ov.querySelector("#alb-url-input").focus(),ov.querySelector("#alb-url-input").scrollIntoView({behavior:"smooth",block:"center"})});const setPreview=function(src){ov.querySelector(".alb-preview").innerHTML=''+escHtml(rec.name)+''},setStatus=function(msg,cls){const el=ov.querySelector("#alb-status");el.textContent=msg||"",el.className="llm-active-status"+(cls?" "+cls:"")},commitImage=async function(dataUri){typeof clSetImage=="function"&&await clSetImage(rec.id,dataUri),rec.image=dataUri,setPreview(dataUri),document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),toast("Profilbild gespeichert","success"),_syncVoicePictureFromChar(rec),typeof onSaved=="function"&&onSaved()};ov.querySelector("#alb-file-input").addEventListener("change",function(){const file=this.files[0];if(!file)return;const fr=new FileReader;fr.onload=function(ev){commitImage(ev.target.result)},fr.readAsDataURL(file)}),ov.querySelector("#alb-url-btn").addEventListener("click",async function(){const url=ov.querySelector("#alb-url-input").value.trim();if(!url){toast("Bild-URL eingeben","error");return}const btn=this;btn.disabled=!0,setStatus("Wird heruntergeladen\u2026");try{const r=await fetch("/api/character-image-from-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),setStatus("\u2713 Heruntergeladen","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Download fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1}}),ov.querySelector("#alb-gen-btn").addEventListener("click",async function(){const prompt=ov.querySelector("#alb-prompt").value.trim();if(!prompt){toast("Prompt eingeben","error");return}const provider=ov.querySelector("#alb-provider").value,btn=this,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generiere\u2026',setStatus("Generiere\u2026 (kann bei lokalen Modellen etwas dauern)");try{const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),rec.sheet||(rec.sheet={}),rec.sheet.image_prompt!==prompt&&(rec.sheet.image_prompt=prompt,typeof clUpsert=="function"&&await clUpsert(rec.book,Object.assign({},rec.sheet,{name:rec.name}),rec.id)),setStatus("\u2713 Generiert","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Generierung fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}window._openAvatarLightbox=_openAvatarLightbox;async function _syncVoicePictureFromChar(rec){if(!rec||!rec.image||!rec.voice)return;const voiceId=typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice||"");if(voiceId)try{const existing=(window._voices||[]).find(function(v){return v.id===voiceId});if(existing&&existing.has_picture)return;const blob=await(await fetch(rec.image)).blob(),fd=new FormData;fd.append("voice_id",voiceId),fd.append("file",blob,"character.jpg"),await fetch("/api/voice/picture",{method:"POST",body:fd})}catch(e){console.warn("[voice picture sync]",e)}}window._syncVoicePictureFromChar=_syncVoicePictureFromChar;function _openVoicePicker(cardEl,rec,onDone){var _a2;document.querySelectorAll(".lib-voice-picker-popup").forEach(function(p){p.remove()});let voices=window._voices||[];const gender=String(((_a2=rec.sheet)==null?void 0:_a2.gender)||"").toLowerCase(),genderMatch=gender.startsWith("f")?"f":gender.startsWith("m")?"m":"",popup=document.createElement("div");popup.className="lib-voice-picker-popup",popup.innerHTML='
',popup.querySelector(".lib-vp-design-btn").addEventListener("click",function(){popup.remove(),typeof _charDesignVoiceInline=="function"&&_charDesignVoiceInline(rec)});function renderList(filter){let list=voices.filter(function(v){return v.enabled!==!1});if(filter){const f=filter.toLowerCase();list=list.filter(function(v){return(v.id||"").toLowerCase().includes(f)||(v.name||"").toLowerCase().includes(f)})}else genderMatch&&(list=list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg.startsWith(genderMatch)||!vg}).concat(list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg&&!vg.startsWith(genderMatch)})));const ul=popup.querySelector(".lib-vp-list");ul.innerHTML=list.slice(0,500).map(function(v){return'
'+escHtml(v.id||v.name||"")+(v.gender?' \xB7 '+escHtml(v.gender)+"":"")+"
"}).join("")+(list.length===0?'
No voices found
':""),ul.querySelectorAll(".lib-vp-item").forEach(function(item){item.addEventListener("click",async function(){const vid=item.dataset.vid;await clPut(Object.assign({},rec,{voice:vid,updated:new Date})),rec.voice=vid,_syncVoicePictureFromChar(rec),popup.remove(),onDone()})})}renderList(""),popup.querySelector(".lib-vp-input").addEventListener("input",function(e){renderList(e.target.value)}),voices.length===0&&typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().then(function(){popup.isConnected&&(voices=window._voices||[],renderList(popup.querySelector(".lib-vp-input").value||""))}).catch(function(){}),document.body.appendChild(popup);const rect=cardEl.getBoundingClientRect(),popupWidth=260;popup.style.position="fixed",popup.style.left=Math.max(8,Math.min(rect.left,window.innerWidth-popupWidth-8))+"px",popup.style.width=popupWidth+"px";const spaceBelow=window.innerHeight-rect.bottom;spaceBelow>300||spaceBelow>rect.top?popup.style.top=rect.bottom+4+"px":popup.style.bottom=window.innerHeight-rect.top+4+"px",setTimeout(function(){function close(e){popup.contains(e.target)||(popup.remove(),document.removeEventListener("click",close))}document.addEventListener("click",close)},0),popup.querySelector(".lib-vp-input").focus()}async function _findVoiceFromSameCharacterElsewhere(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey)return null;let all=[];try{all=await clGetAll()}catch{return null}const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null,match=all.find(function(r){if(r.id===rec.id||!r.voice||String(r.name||"").trim().toLowerCase()!==nameKey)return!1;const vId=typeof r.voice=="object"?r.voice.id:r.voice;return!(!_voiceExists(vId)||bookCode&&_voiceLangCode(vId)!==bookCode)});return match?{voiceId:typeof match.voice=="object"?match.voice.id:match.voice,book:match.book}:null}function _voiceLangCode(voiceId){const m=/^([A-Za-z]{2,3})_/.exec(String(voiceId||""));return m?m[1].toUpperCase():null}async function _findVoiceByCharacterName(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey||nameKey.length<3)return null;const hits=(window._voices||[]).filter(function(v){return v.enabled!==!1}).filter(function(v){return String(v.id||v.name||"").toLowerCase().includes(nameKey)});if(!hits.length)return null;const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null;if(bookCode){const langHits=hits.filter(function(v){return _voiceLangCode(v.id)===bookCode});return langHits.length?langHits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]:null}return hits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]}async function _autoAssignVoice(rec){const reuse=await _findVoiceFromSameCharacterElsewhere(rec);if(reuse){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec),toast(reuse.voiceId+" \u2192 "+rec.name+' (reused from "'+reuse.book+'" for series consistency)',"success");return}const named=await _findVoiceByCharacterName(rec);if(named){await clPut(Object.assign({},rec,{voice:named.id,updated:new Date})),rec.voice=named.id,_syncVoicePictureFromChar(rec),toast(named.id+" \u2192 "+rec.name+" (matching voice already in the library)","success");return}if(typeof _charAutoDesignVoice=="function"){await _charAutoDesignVoice(rec);return}toast("No matching voice found","error")}function _charLang(rec){const sh=rec.sheet||{},text=[sh.backstory,sh.voice_pattern,sh.mannerisms,sh.relationships,sh.motivation,sh.archetype].filter(Boolean).join(" ");return typeof detectLang=="function"?detectLang(text):""}const _bookProfileCache=new Map;async function _getBookProfile(book){const key=String(book||"").trim();if(!key)return{};if(_bookProfileCache.has(key))return _bookProfileCache.get(key);let profile={};try{const r=await fetch("/api/book-profile?book="+encodeURIComponent(key));r.ok&&(profile=await r.json())}catch(e){console.warn("[book profile]",e)}return _bookProfileCache.set(key,profile),profile}function _getBookProfileSync(book){return _bookProfileCache.get(String(book||"").trim())||{}}async function _saveBookProfile(book,profile){const key=String(book||"").trim(),r=await fetch("/api/book-profile",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Object.assign({book:key},profile))});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json();return _bookProfileCache.set(key,d.profile||profile),d.profile}const _bookLangCache=new Map;async function _resolveBookLang(rec){const book=rec.book||"",profile=await _getBookProfile(book);if(profile&&profile.language)return profile.language;const direct=_charLang(rec);if(direct)return direct;if(_bookLangCache.has(book))return _bookLangCache.get(book);let lang="";try{const siblings=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(book):[],counts={};siblings.forEach(function(s){const l=_charLang(s);l&&(counts[l]=(counts[l]||0)+1)});let best="",bestN=0;Object.keys(counts).forEach(function(l){counts[l]>bestN&&(best=l,bestN=counts[l])}),lang=best}catch(e){console.warn("[book lang]",e)}return _bookLangCache.set(book,lang),lang}const _VOICE_TEXTURE_POOL=["a warm, breathy timbre","a bright, clear timbre","a low, husky timbre","a crisp, silvery timbre","a soft, velvety timbre","a slightly nasal, reedy timbre","a rich, resonant timbre","a light, airy timbre"],_VOICE_PACE_POOL=["an unhurried, deliberate pace","a quick, energetic pace","a measured, even pace","a pace that quickens when excited or nervous"];function _hashPick(str,pool){let h=0;for(let i=0;i>>0;return pool[h%pool.length]}function _voiceAudibleSpeciesCue(value){const v=String(value||"").toLowerCase();return/\b(ork|orc|orcs)\b/.test(v)?"Orc: chest-led, gravel-edged resonance; clipped consonants, forceful breath, fully intelligible.":/\b(vampir|vampire)\b/.test(v)?"Vampire: cool velvety timbre, precise diction, restrained predatory calm.":/\b(elf|elv|elfe)\b/.test(v)?"Elf: clear airy resonance, precise musical diction, adult and grounded.":/\b(d[aä]mon|demon|teufel|devil)\b/.test(v)?"Demon: dark resonant timbre with a subtle otherworldly undertone; fully intelligible.":/\b(zwerg|dwarf)\b/.test(v)?"Dwarf: compact earthy resonance, sturdy cadence, deliberate articulation.":/\b(fee|fae|geist|spirit|untot|undead)\b/.test(v)?"Otherworldly: unusual natural resonance, measured breath, emotionally believable.":""}function _voiceAudibleRoleCue(sheet){const v=[sheet.profession,sheet.title,sheet.archetype,sheet.social_class,sheet.capabilities].filter(Boolean).join(" ").toLowerCase();return/krieger|soldat|ritter|wache|kämpfer|warrior|soldier|knight|guard|general|anführer|commander/.test(v)?"Warrior/commander: grounded projection, disciplined breath, concise command cadence.":/schmied|blacksmith|handwerk|artisan|craft/.test(v)?"Craftsperson: practical work-worn texture, measured pace, plain-spoken certainty.":/magier|zauber|hexe|hexer|wizard|mage|sorcer|druid/.test(v)?"Magic-user: measured phrasing, precise diction, contained intensity.":/priester|inquis|mönch|monk|priest|cleric|prophet/.test(v)?"Religious authority: formal diction, calm conviction, weighted pauses.":/könig|königin|baron|graf|adel|noble|lord|lady|herrscher|herrscherin/.test(v)?"High-status authority: polished diction, composed pace, effortless command.":/dieb|spion|assassin|schurke|rogue|thief/.test(v)?"Covert operator: quiet confidence, economical phrasing, alert timing.":/gelehrt|scholar|lehrer|teacher|arzt|heiler|healer/.test(v)?"Learned professional: articulate, patient pace, observant and assured.":""}function _voiceIdentityPrompt(rec,sourcePrompt,langName){const sh=rec.sheet||{},rawGender=String(sh.gender||_genderFromGenericName(rec.name)||"").toLowerCase(),gender=rawGender.startsWith("f")?"female":rawGender.startsWith("m")?"male":rawGender.startsWith("non")?"androgynous/nonbinary":"",hardGender=gender?"Adult "+gender+" voice, clearly "+(gender==="female"?"feminine and never masculine-coded":gender==="male"?"masculine and never feminine-coded":"androgynous")+".":"",age=String(sh.age_estimate||"").trim(),ageCue=age?"Age: "+age.slice(0,36)+".":"",lang=String(langName||"").trim(),languageCue=lang?lang.toLowerCase()==="english"?"Neutral international English.":"Native "+lang+" delivery, never American-accented.":"",species=_voiceAudibleSpeciesCue([sh.race_species,sh.archetype,sh.profession,sh.aliases].filter(Boolean).join(" ")),role=_voiceAudibleRoleCue(sh),prefix=[hardGender,ageCue,languageCue,species,role,"Distinct natural audiobook voice; no imitation."].filter(Boolean).join(" "),base=String(sourcePrompt||"").replace(/\s+/g," ").trim(),room=Math.max(0,480-prefix.length-(prefix&&base?1:0));return(prefix+(base?" "+base.slice(0,room):"")).slice(0,480)}function _buildVoicePrompt(rec,profile,langName){const sh=rec.sheet||{},g=String(sh.gender||"").toLowerCase(),genderWord=g.startsWith("f")?"female":g.startsWith("m")?"male":"",bits=[],lang=String(langName||"").trim();lang&&lang.toLowerCase()!=="english"?bits.push("Speak with an authentic native "+lang+" accent \u2014 not American-accented, not an English speaker doing "+lang+"."):lang&&bits.push("English with a neutral British or international accent, explicitly not American/US-accented.");const settingBits=[profile&&profile.genre,profile&&profile.setting,profile&&profile.era].filter(Boolean);return settingBits.length&&bits.push("Setting: "+settingBits.join(", ")+"."),bits.push("A "+(genderWord?genderWord+" ":"")+"voice"+(sh.archetype?" for "+sh.archetype.toLowerCase():"")+","),bits.push("with "+_hashPick(rec.name||rec.id||"",_VOICE_TEXTURE_POOL)+" and "+_hashPick((rec.name||rec.id||"")+"_pace",_VOICE_PACE_POOL)+"."),sh.voice_pattern&&bits.push(sh.voice_pattern),sh.mannerisms&&bits.push("Mannerisms: "+sh.mannerisms),sh.physical&&bits.push(sh.physical),sh.alignment&&bits.push("Disposition: "+sh.alignment),bits.join(" ").slice(0,600)}function _selectLoose(sel,val){if(!sel||!val)return;const v=String(val).toLowerCase(),opt=[...sel.options].find(function(o){const ov=o.value.toLowerCase(),ot=o.textContent.toLowerCase();return ov===v||ot===v||ov.startsWith(v)||ot.startsWith(v)||v.startsWith(ov)});opt&&(sel.value=opt.value,sel.dispatchEvent(new Event("change")))}function _charSearchOnline(rec){typeof navTo=="function"&&navTo("s-studio");const lang=_charLang(rec);setTimeout(function(){const fishTab=document.querySelector('#gvo-tabs .gvo-tab[data-src="fish"]');fishTab&&fishTab.click(),setTimeout(function(){const langSel=document.getElementById("fa-lang");langSel&&_selectLoose(langSel,lang);const search=document.getElementById("fa-search");search&&(search.value=rec.name,search.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter",bubbles:!0})))},120)},120),toast("Searching online voices for "+rec.name+(lang?" ("+lang+")":""),"info")}function _editBookProfile(book){_getBookProfile(book).then(function(profile){const ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Book context \u2014 '+escHtml(book)+`

Used in every voice design (and image) prompt for this book, so a fantasy story doesn't end up with 1920s-general portraits or English voices in a German book just because one character's own sheet was too sparse to tell.

',document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector("#bctx-cancel").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#bctx-save").addEventListener("click",async function(){const btn=this;btn.disabled=!0;try{await _saveBookProfile(book,{genre:ov.querySelector("#bctx-genre").value,setting:ov.querySelector("#bctx-setting").value,era:ov.querySelector("#bctx-era").value,language:ov.querySelector("#bctx-lang").value}),toast("Book context saved for "+book,"success"),close()}catch(e){toast("Failed to save: "+(e.message||e),"error"),btn.disabled=!1}})})}function _confirmVoiceReuse(rec,reuse){return new Promise(function(resolve){const ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Existing voice found for '+escHtml(rec.name)+'

"'+escHtml(reuse.voiceId)+'" is already used for '+escHtml(rec.name)+' in "'+escHtml(reuse.book)+'". Reuse it for series consistency, or design a brand-new voice just for this book?

',document.body.appendChild(ov);let audioEl=null;ov.querySelector("#cvr-play").addEventListener("click",async function(e){const btn=e.currentTarget,icon=btn.querySelector(".mdi");if(audioEl&&!audioEl.paused){audioEl.pause(),icon.className="mdi mdi-play";return}btn.disabled=!0,icon.className="mdi mdi-loading mdi-spin";try{const langHint=typeof _resolveBookLang=="function"?await _resolveBookLang(rec).catch(function(){return""}):"",text=typeof _charSampleTextFor=="function"&&_charSampleTextFor(rec,langHint)||"Hallo, ich bin "+rec.name+".",rv=(window._voices||[]).find(x=>x.id===reuse.voiceId),rBackend=rv&&!rv.has_ref?"voice_design":"voice_clone",blob=await fetchTtsPreviewBlob(reuse.voiceId,text,"wav","",rBackend);audioEl||(audioEl=new Audio,audioEl.addEventListener("ended",function(){icon.className="mdi mdi-play"})),audioEl.src=URL.createObjectURL(blob),await audioEl.play(),icon.className="mdi mdi-pause"}catch(err){toast("Could not play sample: "+(err.message||err),"error"),icon.className="mdi mdi-play"}finally{btn.disabled=!1}});const cleanup=function(result){audioEl&&audioEl.pause(),ov.remove(),resolve(result)};ov.querySelector("#cvr-cancel").addEventListener("click",function(){cleanup("cancel")}),ov.querySelector("#cvr-use").addEventListener("click",function(){cleanup("use")}),ov.querySelector("#cvr-new").addEventListener("click",function(){cleanup("new")}),ov.addEventListener("click",function(e){e.target===ov&&cleanup("cancel")})})}async function _charDesignVoice(rec){const reuse=await _findVoiceFromSameCharacterElsewhere(rec);if(reuse){const choice=await _confirmVoiceReuse(rec,reuse);if(choice==="cancel")return;if(choice==="use"){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec),toast(reuse.voiceId+" \u2192 "+rec.name+' (reused from "'+reuse.book+'" for series consistency)',"success");return}}typeof navTo=="function"&&navTo("s-design");const sh=rec.sheet||{},lang=_charLang(rec),savedPrompt=_libStr(sh.voice_design_prompt).trim();setTimeout(function(){_selectLoose(document.getElementById("design-gender"),sh.gender),_selectLoose(document.getElementById("design-language"),lang);const instruct=document.getElementById("design-instruct");instruct&&(instruct.value=_voiceIdentityPrompt(rec,savedPrompt||_buildVoicePrompt(rec,null,lang),lang));const nm=document.getElementById("design-preset-name");nm&&(nm.value=rec.name)},140),toast("Voice design prepared for "+rec.name+(lang?" \xB7 "+lang:""),"info")}function _charDesignVoiceInline(rec){const sh=rec.sheet||{},lang=_charLang(rec),savedPrompt=_libStr(sh.voice_design_prompt).trim(),instruct=_voiceIdentityPrompt(rec,savedPrompt||_buildVoicePrompt(rec,null,lang),lang),ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Voice design prompt \u2014 '+escHtml(rec.name)+'

Edit the description, then generate a new voice from it. This replaces '+(rec.voice?"the current voice":"this character\u2019s voice")+'.

',document.body.appendChild(ov);const close=function(){ov.remove()};ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#cdi-cancel").addEventListener("click",close),ov.querySelector("#cdi-generate").addEventListener("click",async function(e){const btn=e.currentTarget,text=ov.querySelector("#cdi-instruct").value.trim();if(!text){toast("Prompt is empty","error");return}btn.disabled=!0;const icon=btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-loading mdi-spin");try{await _charAutoDesignVoice(rec,!0,text),_schedulePendingTtsRestart(),rec.sheet||(rec.sheet={}),rec.sheet.voice_design_prompt=text,close(),toast("New voice designed for "+rec.name,"success");const detailPage=document.querySelector(".lib-char-page"),detailOpts=window._libDetailOpts;if(detailPage&&detailOpts&&typeof _charDetailPage=="function"){const all=typeof clGetAll=="function"?await clGetAll().catch(function(){return[]}):[],updated=all.find(function(r){return r.id===rec.id})||rec;_charDetailPage(updated,all.filter(function(r){return r.book===updated.book}),detailOpts)}else typeof libraryRenderCharacters=="function"&&await libraryRenderCharacters();typeof _libRefreshDetailModal=="function"&&_libRefreshDetailModal(rec)}catch(err){toast("Voice design failed: "+(err.message||err),"error"),btn.disabled=!1,icon&&(icon.className="mdi mdi-creation")}})}function _charCloneVoice(rec){typeof navTo=="function"&&navTo("s-clone"),setTimeout(function(){const nm=document.getElementById("clone-your-name");nm&&(nm.value=rec.name)},140),toast("Clone a Voice prepared for "+rec.name+" \u2014 pick a mic take, file, or YouTube URL","info")}const _DESIGN_SAMPLE_FALLBACK={German:"Ich habe lange auf diesen Moment gewartet, und jetzt, da er da ist, wei\xDF ich genau, was zu tun ist.",English:"I have waited a long time for this moment, and now that it is here, I know exactly what to do."};function _charRealLine(rec){const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook,nameLower=String(rec.name||"").trim().toLowerCase(),line=(ab&&ab.segments||[]).find(function(s){return s&&s.type==="dialogue"&&String(s.speaker||"").trim().toLowerCase()===nameLower&&s.text&&s.text.trim().length>=20&&s.text.trim().length<=200});if(line)return line.text.trim();const quotes=(Array.isArray(rec.sheet&&rec.sheet.sources)?rec.sheet.sources:[]).map(function(s){return s&&s.quote?String(s.quote).trim():""}).filter(function(q){return q.length>=20&&q.length<=240}),spoken=quotes.find(function(q){return/[""„"]/.test(q)});return spoken||(quotes.length?quotes[0]:null)}function _charSampleTextFor(rec,langNameHint){const lang=_charLang(rec)||langNameHint||"English",greeting=lang==="German"?`Hallo, ich bin ${rec.name}.`:`Hello, I am ${rec.name}.`,line=_charRealLine(rec);return line?`${greeting} ${line}`:_DESIGN_SAMPLE_FALLBACK[lang]||_DESIGN_SAMPLE_FALLBACK.English}const _GENERIC_NAME_GENDER={frau:"female",dame:"female",junge_frau:"female",m\u00E4dchen:"female",maedchen:"female",mann:"male",herr:"male",junge:"male",knabe:"male"};function _genderFromGenericName(name){const key=String(name||"").trim().toLowerCase().replace(/\s+/g,"_");return _GENERIC_NAME_GENDER[key]||""}let _voiceRestartPending=!1;async function _flushPendingTtsRestart(){if(_voiceRestartPending){_voiceRestartPending=!1;try{const r=await fetch("/api/tts/restart",{method:"POST"});r.ok?toast("TTS backend restarted to pick up the newly designed voice(s)","success"):console.warn("[tts restart] failed:",r.status)}catch(e){console.warn("[tts restart]",e)}}}let _voiceRestartDebounceTimer=null;function _schedulePendingTtsRestart(){clearTimeout(_voiceRestartDebounceTimer),_voiceRestartDebounceTimer=setTimeout(_flushPendingTtsRestart,4e3)}async function _fetchRetryingNetworkErrors(url,opts,tries){tries=tries||3;for(let i=1;i<=tries;i++)try{return await fetch(url,opts)}catch(e){if(i===tries)throw e;await new Promise(function(r){setTimeout(r,2500*i)})}}function _designBenchmarkWpmBad(b){if(!b||!b.ok||!b.audio_sec||!b.text)return!1;const wpm=String(b.text).trim().split(/\s+/).length/(b.audio_sec/60);return wpm<80||wpm>400}async function _charAutoDesignVoice(rec,force,instructOverride){const reuse=force||instructOverride?null:await _findVoiceFromSameCharacterElsewhere(rec);if(reuse&&reuse.voiceId!==rec.voice){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec);return}const sh=rec.sheet||{},langName=await _resolveBookLang(rec)||"English",instruct=_voiceIdentityPrompt(rec,instructOverride||_libStr(sh.voice_design_prompt).trim()||_buildVoicePrompt(rec,await _getBookProfile(rec.book),langName),langName);if(!instruct.trim())throw new Error("No character description to design a voice from yet");instructOverride&&(sh.voice_design_prompt=instruct);const langCode=typeof DESIGN_LANG_CODE!="undefined"&&DESIGN_LANG_CODE[langName]||"EN",genderWord=String(sh.gender||"").toLowerCase()||_genderFromGenericName(rec.name),genderLetter=genderWord.startsWith("f")?"F":genderWord.startsWith("m")?"M":"N",sampleText=_charSampleTextFor(rec,langName),dialogue=typeof isDialogueDesign=="function"?isDialogueDesign(instruct,sampleText,null):!1,baseName=typeof designSafeName=="function"?designSafeName(rec.name):(typeof _umlautSafe=="function"?_umlautSafe(rec.name||"VoiceDesign"):String(rec.name||"VoiceDesign")).replace(/[^A-Za-z0-9]+/g,"_"),voiceId=(langCode+"_"+genderLetter+"_"+baseName).slice(0,96),maxAttempts=3;let saved=null;for(let attempt=1;attempt<=maxAttempts;attempt++){const r1=await _fetchRetryingNetworkErrors("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({instruct,sample_text:sampleText,language:langName,gender:genderLetter,dialogue})});if(!r1.ok){const e=await r1.json().catch(function(){return{}});throw new Error(e.detail||r1.statusText)}const designed=await r1.json(),tryId=(voiceId+"__try"+attempt).slice(0,96),r2=await _fetchRetryingNetworkErrors("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designed.id,voice_id:tryId,transcript:sampleText})});if(!r2.ok){const e=await r2.json().catch(function(){return{}});throw new Error(e.detail||r2.statusText)}await r2.json();let bad=!1;if(typeof runVoiceBenchmark=="function")try{const d=await runVoiceBenchmark(tryId,{text:sampleText}),hit=(d&&d.voices||[]).find(function(x){return x.voice_id===tryId}),b=hit&&hit.benchmark;!b||!b.ok&&/connection (refused|reset|aborted)|max retries exceeded|newconnectionerror|econnrefused|timed? ?out/i.test(String(b.error||""))?console.warn("[voice design] benchmark unreachable, accepting unverified:",b&&b.error):bad=!!(b.clipped||_designBenchmarkWpmBad(b))}catch(e){console.warn("[voice benchmark]",e)}if(!bad&&typeof _voiceRoundtripCheck=="function")try{const rt=await _voiceRoundtripCheck(tryId,sampleText,"voice_design");rt.score<.5&&(bad=!0,console.warn("[voice design] STT roundtrip mismatch (score "+rt.score.toFixed(2)+'): said "'+rt.transcript+'"'))}catch(e){console.warn("[voice design roundtrip]",e)}if(!bad){const r3=await _fetchRetryingNetworkErrors("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designed.id,voice_id:voiceId,transcript:sampleText})});if(!r3.ok){const e=await r3.json().catch(function(){return{}});throw new Error(e.detail||r3.statusText)}saved=await r3.json(),saved.needs_tts_restart&&(_voiceRestartPending=!0),await fetch("/api/voice/"+encodeURIComponent(tryId),{method:"DELETE"}).catch(function(){});break}if(await fetch("/api/voice/"+encodeURIComponent(tryId),{method:"DELETE"}).catch(function(){}),attempt===maxAttempts)throw new Error('Voice design for "'+voiceId+'" produced broken audio after '+maxAttempts+" attempts \u2014 left the previous voice in place, try again later")}return typeof saveMeta=="function"&&await saveMeta(saved.voice_id,{gender:genderLetter,flag:typeof LANG_FLAG_DEFAULT!="undefined"?LANG_FLAG_DEFAULT[langCode]:void 0,origin:"designed",group:rec.book||void 0,tag:rec.book||void 0,transcript:sampleText,note:"Voice Design: "+instruct.slice(0,240),voice_design_prompt:instruct}).catch(function(){}),rec.voice=saved.voice_id,await clUpsert(rec.book,Object.assign({},rec.sheet,{name:rec.name,voice:saved.voice_id}),rec.id),_syncVoicePictureFromChar(rec),saved.voice_id}async function _charAutoGenerateImage(rec,provider){const sh=rec.sheet||{},hasExplicitPrompt=!!_libStr(sh.image_prompt).trim();if(!hasExplicitPrompt&&[sh.archetype,sh.physical,sh.clothing].filter(Boolean).join(" ").trim().length<20)throw new Error("Not enough character detail to generate a meaningful portrait \u2014 skipped instead of using a generic placeholder");const bookProfile=typeof _getBookProfile=="function"?await _getBookProfile(rec.book):{},prompt=hasExplicitPrompt?_libStr(sh.image_prompt).trim():typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh,bookProfile):"";if(!prompt)throw new Error("No image prompt to work from yet");const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json();return typeof clSetImage=="function"&&await clSetImage(rec.id,d.image),rec.image=d.image,document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),_syncVoicePictureFromChar(rec),d.image}async function _charAutoGenerateConceptArt(rec,provider){const sh=rec.sheet||{},prompt=_libStr(sh.concept_art_prompt).trim();if(!prompt)throw new Error("No concept art prompt to work from yet \u2014 generate the prompt first");const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json(),target=(typeof clGet=="function"?await clGet(rec.id).catch(function(){return null}):null)||rec;return target.sheet=Object.assign({},target.sheet||{},{concept_art_image:d.image}),target.updated=new Date,typeof clPut=="function"&&await clPut(target),sh.concept_art_image=d.image,rec.sheet=sh,d.image}function _openConceptArtLightbox(rec,onSaved){var _a2;(_a2=document.getElementById("conceptart-lightbox"))==null||_a2.remove();const sh=rec.sheet||{},prompt=_libStr(sh.concept_art_prompt).trim(),ov=document.createElement("div");ov.id="conceptart-lightbox",ov.className="audiobook-overlay",ov.innerHTML='
'+escHtml(rec.name)+' \u2014 Konzeptbild
'+(sh.concept_art_image?'Concept art \u2014 '+escHtml(rec.name)+'':'
No concept art yet
')+'
',document.body.appendChild(ov);const close=function(){ov.remove()},status2=function(msg,cls){const el=ov.querySelector("#calb-status");el.textContent=msg||"",el.className="llm-active-status"+(cls?" "+cls:"")},setPreview=function(src){ov.querySelector(".conceptart-editor-preview").innerHTML='Concept art \u2014 '+escHtml(rec.name)+''},commit=async function(dataUri){const target=(typeof clGet=="function"?await clGet(rec.id).catch(function(){return null}):null)||rec;target.sheet=Object.assign({},target.sheet||{},{concept_art_image:dataUri}),target.updated=new Date,typeof clPut=="function"&&await clPut(target),rec.sheet=target.sheet,setPreview(dataUri),toast("Concept art saved","success"),typeof onSaved=="function"&&onSaved()};ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#calb-close").addEventListener("click",close),ov.querySelector("#calb-upload").addEventListener("click",function(){ov.querySelector("#calb-file").click()}),ov.querySelector("#calb-online").addEventListener("click",function(){ov.querySelector("#calb-url").focus(),ov.querySelector("#calb-url").scrollIntoView({behavior:"smooth",block:"center"})}),ov.querySelector("#calb-file").addEventListener("change",function(){const file=this.files[0];if(!file)return;const fr=new FileReader;fr.onload=function(e){commit(e.target.result).catch(function(err){toast("Upload failed: "+err.message,"error")})},fr.readAsDataURL(file)}),ov.querySelector("#calb-url-btn").addEventListener("click",async function(){const url=ov.querySelector("#calb-url").value.trim();if(!url){toast("Enter an image URL","error");return}const btn=this;btn.disabled=!0,status2("Downloading\u2026");try{const r=await fetch("/api/character-image-from-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commit(d.image),status2("\u2713 Downloaded","ok")}catch(e){status2("Failed","err"),toast("Download failed: "+e.message,"error")}finally{btn.disabled=!1}}),ov.querySelector("#calb-regenerate").addEventListener("click",async function(){const nextPrompt=ov.querySelector("#calb-prompt").value.trim();if(!nextPrompt){toast("Generate or enter a concept-art prompt first","error");return}const btn=this,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026',status2("Generating\u2026");try{sh.concept_art_prompt=nextPrompt;const provider=ov.querySelector("#calb-provider").value,img=await _charAutoGenerateConceptArt(rec,provider);await commit(img),status2("\u2713 Generated","ok")}catch(e){status2("Failed","err"),toast("Generation failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}window._charSearchOnline=_charSearchOnline,window._charDesignVoice=_charDesignVoice,window._charAutoDesignVoice=_charAutoDesignVoice,window._charAutoGenerateImage=_charAutoGenerateImage,window._charAutoGenerateConceptArt=_charAutoGenerateConceptArt,window._openConceptArtLightbox=_openConceptArtLightbox,window.libraryRenderCharacters=libraryRenderCharacters;let _stuActive=1;const _stuHomes=new Map;function _stuBorrow(id,slotId){const el=document.getElementById(id),slot=document.getElementById(slotId);!el||!slot||(_stuHomes.has(id)||_stuHomes.set(id,{parent:el.parentNode,next:el.nextSibling}),slot.appendChild(el))}function _stuReturnAll(){_stuIsActive=!1,typeof _stuRestoreCastFoot=="function"&&_stuRestoreCastFoot(),_stuHomes.forEach(function(home,id){const el=document.getElementById(id);el&&home.parent&&home.parent.insertBefore(el,home.next)}),_stuHomes.clear()}window._stuReturnAll=_stuReturnAll;let _stuIsActive=!1,_stuAllowNextNav=!1;const _STU_BORROWED_FROM={"s-reader":1,"s-library":1,"s-rehearser":1};let _stuNavGuardInstalled=!1;function _stuInstallNavGuardOnce(){if(_stuNavGuardInstalled)return;_stuNavGuardInstalled=!0;const realNavTo=window.navTo;window.navTo=function(id){if(!(_stuIsActive&&!_stuAllowNextNav&&_STU_BORROWED_FROM[id]))return _stuAllowNextNav=!1,realNavTo(id)};const realShowReaderView=window.showReaderView;let _stuInShowReaderView=!1;window.showReaderView=function(view){const result=typeof realShowReaderView=="function"?realShowReaderView(view):void 0;if(_stuIsActive&&!_stuInShowReaderView&&(view==="cast"||view==="chars")){const target=view==="chars"?"sheets":"identify";if(_stuCastView!==target){_stuInShowReaderView=!0;try{_stuShowCastView(target)}finally{_stuInShowReaderView=!1}}}return result},document.querySelectorAll('[data-nav-section="s-reader"], #nav-reader-tree, [data-nav-section="s-library"], #nav-library-tree, [data-nav-section="s-rehearser"], #nav-rehearser-tree').forEach(function(el){el.addEventListener("click",function(){_stuAllowNextNav=!0},!0)})}function _stuCallSuppressingNav(fn){return fn()}function _stuEnterPhase(n){if(n===1)_stuBorrow("reader-main-view","stu-source-slot"),typeof window.readerOnShow=="function"&&_stuCallSuppressingNav(window.readerOnShow),_stuBorrow("lib-books-list","stu-books-slot"),_stuCallSuppressingNav(function(){typeof window.libraryRenderBooks=="function"&&window.libraryRenderBooks()});else if(n===2)_stuShowCastView(_stuCastView);else if(n===3)_stuBorrow("lib-chars-list","stu-voices-slot"),(async()=>{let title=null;if(window._audiobook&&window._audiobook.bookId)try{const r=await fetch("/api/reader/docs/"+encodeURIComponent(window._audiobook.bookId));r.ok&&(title=(await r.json()).title||null)}catch{}window._libCharsScrollToBook=title||window.readerState&&readerState.title||null,_stuCallSuppressingNav(function(){typeof window.libraryRender=="function"&&window.libraryRender("characters")})})();else if(n===4){_stuBorrow("reh-phase-3","stu-stage-slot"),_stuBorrow("reh-cast-list","stu-mecast-slot"),_stuBorrow("reh-tone-warn","stu-tone-warn-slot"),typeof _abInitPauseUI=="function"&&_abInitPauseUI(),_stuCallSuppressingNav(async function(){if(window.rehState&&rehState.lines&&rehState.lines.length){typeof buildScriptPage=="function"&&buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine();return}if(!(window._audiobook&&window._audiobook.segments&&window._audiobook.segments.length)&&typeof _abLoadDraftServer=="function"&&typeof _abBookId=="function"){const bookId=_abBookId(),draft=bookId?await _abLoadDraftServer(bookId):null;draft&&(_audiobook.segments=draft.segments||[],_audiobook.roster=draft.roster||[],_audiobook.pageMarks=draft.pageMarks||[],_audiobook.rehId=draft.rehId||_audiobook.rehId||null)}if(typeof window.audiobookOpenCurrentInRehearser=="function"&&(window._audiobook&&window._audiobook.segments||[]).length)return window.audiobookOpenCurrentInRehearser()});const rp3=document.getElementById("reh-phase-3");rp3&&(rp3.hidden=!1),_stuSyncModeToggle()}}function _stuSyncModeToggle(){const cb=document.getElementById("stu-mode-audiobook"),details=document.getElementById("stu-mecast-details");!cb||!window.rehState||(cb.checked=!rehState.skipDescriptions,details&&(details.hidden=cb.checked))}(_tc=document.getElementById("stu-mode-audiobook"))==null||_tc.addEventListener("change",function(){const audiobookMode=this.checked;if(window.rehState){rehState.skipDescriptions=!audiobookMode;const t=document.getElementById("reh-skip-desc-toggle");t&&(t.checked=rehState.skipDescriptions),audiobookMode&&!rehState.narratorVoice&&typeof toast=="function"&&toast("No narrator voice assigned yet \u2014 assign one in the Voices phase so narration actually plays","error")}const details=document.getElementById("stu-mecast-details");details&&(details.hidden=audiobookMode)});let _stuCastView="identify";function _stuShowCastView(view){_stuCastView=view,document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(t){t.classList.toggle("active",t.dataset.stuCastView===view)});const identifySlot=document.getElementById("stu-cast-slot"),sheetsSlot=document.getElementById("stu-castchars-slot");if(identifySlot&&(identifySlot.hidden=view!=="identify"),sheetsSlot&&(sheetsSlot.hidden=view!=="sheets"),view==="identify")_stuBorrow("reader-audiobook-panel","stu-cast-slot"),_stuCallSuppressingNav(function(){if(typeof window.audiobookOpenCastView=="function")return window.audiobookOpenCastView();typeof window.showReaderView=="function"&&window.showReaderView("cast")}),_stuRelocateCastFoot();else if(view==="sheets"){_stuBorrow("reader-charsheets-panel","stu-castchars-slot");const panel=document.getElementById("reader-charsheets-panel");if(panel&&!panel.innerHTML.trim()){panel.innerHTML='
Character sheets

Optional \u2014 let the AI fill out full character profiles (appearance, backstory, voice notes) for reference. Skip this if you just want to cast voices quickly.

';const goBtn=document.getElementById("stu-goto-cast-menu");goBtn&&goBtn.addEventListener("click",function(){typeof window.csForReader=="function"&&window.csForReader()})}}}document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(tab){tab.addEventListener("click",function(){_stuShowCastView(tab.dataset.stuCastView)})});let _stuCastFootObserver=null,_stuCastFootHealthTimer=null;function _stuCastIsRunning(){return typeof _audiobook!="undefined"&&!!_audiobook.running}function _stuSyncCastFallbackControls(tabs,innerTabs,hasActionFooter){const running=_stuCastIsRunning();tabs.hidden=!1,tabs.style.display="flex",innerTabs.forEach(function(tab){const hideFallback=running||hasActionFooter;tab.hidden=hideFallback,tab.style.display=hideFallback?"none":""})}function _stuRelocateCastFoot(){_stuTryRelocateCastFoot(),!_stuCastFootObserver&&(_stuCastFootObserver=new MutationObserver(function(){_stuTryRelocateCastFoot()}),_stuCastFootObserver.observe(document.body,{childList:!0,subtree:!0}),[0,100,300,800,1500].forEach(ms=>setTimeout(_stuTryRelocateCastFoot,ms)),_stuCastFootHealthTimer=setInterval(function(){_stuIsActive&&_stuActive===2&&_stuTryRelocateCastFoot()},500))}function _stuTryRelocateCastFoot(){const slot=document.getElementById("stu-cast-slot"),tabs=document.getElementById("stu-cast-inner-tabs");if(!tabs)return;const freshFoot=slot&&slot.querySelector("#ab-cv-foot")||document.querySelector("#ab-cv-foot"),innerTabs=Array.from(tabs.querySelectorAll(":scope > .stu-inner-tab")),footerHasActions=freshFoot&&freshFoot.querySelector("#ab-cv-menu-identify, #ab-cv-menu-cast, #ab-cv-menu-viewcast");if(_stuSyncCastFallbackControls(tabs,innerTabs,!!footerHasActions),!freshFoot||!footerHasActions)return;if(freshFoot.parentElement===tabs){freshFoot.hidden=!1,freshFoot.style.display="flex";return}tabs.querySelectorAll("#ab-cv-foot").forEach(function(stale){stale.remove()}),tabs.hidden=!1,tabs.style.display="flex",freshFoot.style.borderTop="none",freshFoot.style.padding="0",freshFoot.style.justifyContent="flex-start",freshFoot.hidden=!1,freshFoot.style.display="flex",tabs.appendChild(freshFoot);const openReh=freshFoot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!0)}function _stuRestoreCastFoot(){_stuCastFootObserver&&(_stuCastFootObserver.disconnect(),_stuCastFootObserver=null),_stuCastFootHealthTimer&&(clearInterval(_stuCastFootHealthTimer),_stuCastFootHealthTimer=null);const tabs=document.getElementById("stu-cast-inner-tabs"),panel=document.getElementById("reader-audiobook-panel"),foot=tabs?tabs.querySelector("#ab-cv-foot"):null;if(foot&&panel){foot.style.borderTop="",foot.style.padding="",foot.style.justifyContent="";const openReh=foot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!1),panel.appendChild(foot)}document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!1})}function showStudioPhase(n){_stuActive=n;for(let i=1;i<=4;i++){const el=document.getElementById("stu-phase-"+i);el&&(el.hidden=i!==n)}document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.classList.toggle("active",parseInt(tab.dataset.stuPhase,10)===n)}),document.querySelectorAll("#nav-caststudio-tree [data-stu-phase]").forEach(function(item){item.classList.toggle("is-active",parseInt(item.dataset.stuPhase,10)===n)});const prevBtn=document.getElementById("stu-phase-prev"),nextBtn=document.getElementById("stu-phase-next");prevBtn&&(prevBtn.disabled=n<=1),nextBtn&&(nextBtn.disabled=n>=4),typeof _stuEnterPhase=="function"&&_stuEnterPhase(n)}window.showStudioPhase=showStudioPhase,document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.addEventListener("click",function(){showStudioPhase(parseInt(tab.dataset.stuPhase,10))})}),(_uc=document.getElementById("stu-phase-prev"))==null||_uc.addEventListener("click",function(){_stuActive>1&&showStudioPhase(_stuActive-1)}),(_vc=document.getElementById("stu-phase-next"))==null||_vc.addEventListener("click",function(){_stuActive<4&&showStudioPhase(_stuActive+1)});function studioOnShow(){_stuInstallNavGuardOnce(),_stuIsActive=!0,showStudioPhase(_stuActive)}window.studioOnShow=studioOnShow; diff --git a/static/index.html b/static/index.html index 96e9738..f9b58ff 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook.js b/static/js/audiobook.js index ad033f8..d39b380 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -24,6 +24,38 @@ const AUDIOBOOK_RECAST_CONTEXT_CHARS = 4200; const AUDIOBOOK_RECAST_TARGETS_PER_CALL = 6; const AUDIOBOOK_DRAFT_AUTOSAVE_MS = 5 * 60 * 1000; const _audiobook = { running: false, cancel: false }; + +// Studio relocates this control out of the audiobook panel. That means a +// normal panel rebuild can leave an old copy visible even after the async cast +// has finished or failed. Always clear every live copy when the worker state +// reaches a terminal state. +function _abHideCastingControl() { + document.querySelectorAll('#ab-cv-cancel').forEach(btn => { + btn.hidden = true; + btn.disabled = false; + btn.innerHTML = ' Cancel'; + }); +} + +// The casting footer is physically moved into Studio and is also rebuilt by +// the live view. A listener attached only to the original panel can therefore +// disappear with a rebuilt footer. Keep one delegated listener on document so +// Stop Casting always reaches the current button. +if (!window._abDelegatedCancelHandler) { + window._abDelegatedCancelHandler = true; + document.addEventListener('click', event => { + const btn = event.target.closest?.('#ab-cv-cancel'); + if (!btn || btn.hidden) return; + if (_audiobook.running) { + _audiobook.cancel = true; + btn.disabled = true; + btn.innerHTML = ' Stopping...'; + if (typeof _audiobook.abort === 'function') _audiobook.abort(); + } else { + _abHideCastingControl(); + } + }); +} window._audiobook = _audiobook; let _abDraftAutosaveTimer = null; let _abLastServerDraftErrorAt = 0; @@ -1087,7 +1119,15 @@ function _abIdentityTokens(name) { // why a fresh cast produced 73 speakers where the human had 62. A line credited // to one of these is better left Unknown: visibly unresolved, and still // recoverable by the repair passes, instead of silently wrong. +// Grammatical words the caster mistakes for names because they open the sentence +// that introduces a quote: "Hinter ihm raunte Oberst von Blautann:" -> Hinter, +// "Keiner sagte mehr etwas." -> Keiner, a dangling "Von" from "von Blautann". +// Confirmed by a reader hand-correcting a whole book: every one of these was +// deleted as a bogus character. const _AB_NOT_A_CHARACTER = new Set([ + 'hinter','vor','neben','über','unter','zwischen','gegenüber','keiner','keine','niemand','jemand','alle','beide', + 'einige','viele','andere','anderen','jeder','jede','man','von','zu','bei','nach','aus','mit','ihm','ihr','ihnen', + 'dort','hier','draußen','drinnen','oben','unten','plötzlich','endlich','schließlich', 'inneren','innere','herz','herzen','hand','hände','haenden','kopf','stimme','stimmen','wind','feuer','flamme','flammen', 'dunkelheit','stille','schatten','licht','nacht','angst','schmerz','gedanke','gedanken','traum','träume','geliebten', 'geliebter','geliebte','blondschopf','antwort','echo','ferne','nähe','naehe','tiefe','wasser','regen','donner','erde', @@ -1583,8 +1623,8 @@ function _abOpenRecastSelectPopup(existingChars) { }); }; - ov.innerHTML = '
' - + '
Cast selected character roles
' + ov.innerHTML = '
' + + '
Select characters to cast or recast roles
' + '
Only re-reads the passages that mention the characters you pick below, including known aliases, plus a little surrounding context. Everyone else\'s sheet stays as-is.
' + '
' + '' @@ -1626,7 +1666,11 @@ function _abOpenRecastSelectPopup(existingChars) { render(); }); ov.querySelector('#ab-recast-confirm').addEventListener('click', async () => { - const names = [...selected]; + // The DOM checkboxes are the source of truth at confirmation time. The + // Set is only a render/search convenience and could become stale after a + // filtered list was rebuilt, which previously allowed a one-character + // selection to fall through as a full-roster recast. + const names = [...ov.querySelectorAll('.ab-recast-cb:checked')].map(cb => cb.value).filter(Boolean); if (!names.length) { toast('Select at least one character', 'error'); return; } ov.remove(); if (typeof window.audiobookRecastSelectedCharacters === 'function') await window.audiobookRecastSelectedCharacters(names); @@ -4003,6 +4047,7 @@ STRIKTE FORMAT- UND TEXTREGELN: // Park the panel in a "done" state with a Review button instead of auto-popping // the preview — so it waits for you if you wandered off to do something else. complete(summary, onOpen, onRecast, onRecastUnknown) { + _abHideCastingControl(); if (count) count.hidden = true; const castBar = panel.querySelector('.ab-castpanel-bar'); if (castBar) castBar.hidden = true; @@ -4406,7 +4451,7 @@ ABSOLUTE REGELN: castMenu?.addEventListener('click', () => _abToggleFootMenu(castMenu, [ { icon: 'mdi-account-multiple-plus-outline', label: 'Cast all character roles', title: 'Generate / refresh the character sheets for every cast character', onClick: runCastAll }, { icon: 'mdi-account-arrow-right-outline', label: 'Continue uncasted characters', title: 'Only generate profiles for characters with no detail yet — skips anyone already fully cast', onClick: runCastContinueUncasted }, - { icon: 'mdi-account-check-outline', label: 'Cast selected character roles', title: existing.length ? 'Generate / refresh the character sheets for selected cast characters' : 'No character sheets found yet for this book', disabled: !existing.length, onClick: runCastSelected }, + { icon: 'mdi-account-check-outline', label: 'Select characters to cast or recast roles', title: existing.length ? 'Choose character roles to generate or refresh' : 'No character sheets found yet for this book', disabled: !existing.length, onClick: runCastSelected }, { divider: true }, { icon: 'mdi-refresh', label: 'New recast (discard & rebuild all)', title: 'Discard every generated character sheet and rebuild every profile from scratch', onClick: runCastFresh, danger: true }, ])); @@ -4428,6 +4473,7 @@ ABSOLUTE REGELN: if (typeof window.setNavCastingBadge === 'function') window.setNavCastingBadge(false); }, done(options = {}) { + _abHideCastingControl(); const stopped = options.stopped || _audiobook.cancel; if (!stopped) { closePanel(); return; } @@ -4765,6 +4811,7 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel, options = {}) } finally { _audiobook.running = false; _audiobook.abort = null; + _abHideCastingControl(); } // Everything below only ever ran when nothing above threw — but nothing // here was itself guarded, so any failure in this post-processing (dedup, @@ -5352,6 +5399,7 @@ async function audiobookCast(overrideUrl, overrideModel, resume) { _abStopDraftAutosave(); _audiobook.running = false; _audiobook.abort = null; + _abHideCastingControl(); } if (!allSegments.length) { diff --git a/static/js/character-sheets.js b/static/js/character-sheets.js index c23cee1..be451a3 100644 --- a/static/js/character-sheets.js +++ b/static/js/character-sheets.js @@ -43,7 +43,7 @@ For each character output these fields: - mannerisms: habitual gestures, tics, body language, habits and quirks - communication_style: how they communicate socially — blunt, formal, warm, guarded, sarcastic, etc. - voice_pattern: speech style — accent, pacing, vocabulary, register and verbal tics (for voice casting) -- voice_design_prompt: concise English Qwen voice-design prompt (15-45 words). Include age impression, gender/androgyny if inferable, pitch, timbre, pace, accent/register, emotional baseline and suitability for audiobook dialogue. Do NOT mention plot spoilers. +- voice_design_prompt: concise English voice-design prompt, maximum 480 characters. Use only objective, audible traits; no lore-only labels or repeated synonyms. Include gender as a hard audible constraint, age impression, pitch, timbre, pace, accent/register, emotion and audiobook use. Translate species/role into audible cues (orc: chest-led/gravel-edged; vampire: cool/velvety/controlled; elf: airy/precise/adult; demon: dark/subtly otherworldly; warrior: disciplined command cadence; blacksmith: work-worn/practical; mage: measured/precise). Give one distinctive vocal fingerprint. Do NOT mention plot spoilers. - image_prompt: detailed English image-generation prompt for this character. Include face, age impression, build, hair/eyes/skin if known, clothing, posture, props, mood, genre/style, and visible symbols. Mark inferred traits with '*'. - inventory: 1-3 defining items/props/clothing (array of short strings) - secret: dark secret or fatal flaw @@ -76,6 +76,34 @@ function csLlmModel() { return $('reh-llm-model')?.value || ''; } function csLang() { return $('reh-design-lang')?.value || ''; } function csReaderPageHost() { return $('reader-charsheets-panel'); } +// Establish shared world context before the first profile pass. This keeps +// genre/setting/era/language consistent across character sheets, images, and +// voice prompts. The server preserves any fields the user already entered. +async function csEnsureBookContext(book, text, roster, opts = {}) { + if (!book || !text || typeof _getBookProfile !== 'function') return {}; + let existing = {}; + try { existing = await _getBookProfile(book); } catch (_) {} + const missing = ['genre', 'setting', 'era', 'language'].filter(k => !String(existing?.[k] || '').trim()); + if (!missing.length && !opts.force) return existing; + try { + const r = await fetch('/api/book-profile/generate', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + book, text, known_characters: Array.isArray(roster) ? roster : [], + llm_url: csLlmUrl(), model: csLlmModel(), language: csLang(), + }), + }); + if (!r.ok) throw new Error((await r.text()).slice(0, 300) || ('HTTP ' + r.status)); + const data = await r.json(); + if (typeof _bookProfileCache !== 'undefined') _bookProfileCache.set(book, data.profile || {}); + return data.profile || existing; + } catch (e) { + console.warn('[book context] automatic generation failed:', e); + if (typeof toast === 'function') toast('Book context could not be generated; profile casting will continue', 'info'); + return existing; + } +} + // Build page-annotated text from the current reader scope ([p.N] at page changes). function csReaderText() { if (typeof readerScopeIndices !== 'function' || !readerState?.sentences?.length) return ''; @@ -289,6 +317,44 @@ function csSheetMatchesRoster(sheet, roster = []) { return false; } +// The dialogue-casting roster is the source of truth for the character's +// display name. Character-sheet extraction may discover a real name, role, or +// epithet and put it in `name`, but that must never replace the name the user +// sees in Characters found / the cast. Keep those discoveries in aliases. +function csRosterCanonicalName(sheet, roster = []) { + const names = csCleanRoster(roster); + if (!names.length) return ''; + const tokens = csNameTokens(sheet); + const exact = names.find(n => tokens.has(n.toLowerCase())); + return exact || ''; +} + +function csCanonicalizeRosterNames(sheets, roster = []) { + const canonical = new Map(); + for (const original of sheets || []) { + if (!original) continue; + const sheet = { ...original }; + const rosterName = csRosterCanonicalName(sheet, roster); + if (rosterName && sheet.name && sheet.name.toLowerCase() !== rosterName.toLowerCase()) { + sheet.aliases = csMergeAliases( + { name: rosterName, aliases: sheet.aliases }, + { name: sheet.name, aliases: '' }, + ); + sheet.name = rosterName; + } + // Tell the library upsert that this name came from the live cast roster. + // It must be allowed to correct an older record whose canonical name was + // previously chosen from an alias (for example an LLM-selected real name + // replacing the sidebar role name Fremder). + if (rosterName) sheet._rosterNameAuthoritative = true; + const key = String(sheet.name || '').trim().toLowerCase(); + if (!key) continue; + if (!canonical.has(key)) canonical.set(key, sheet); + else csMerge(canonical, [sheet]); + } + return [...canonical.values()]; +} + function csIdentityNeedles(value) { return csSplitIdentityTokens(value, { aliases: true }).filter(v => { const n = String(v || '').trim(); @@ -441,7 +507,7 @@ function csMerge(map, sheets) { // this run (settled by Identify Characters or an earlier cast pass) as // canonical, with the other name demoted to an alias — rather than letting // the character-sheet step invent a competing identity. -function csConsolidateAliasDuplicates(map, establishedKeys, establishedCreated) { +function csConsolidateAliasDuplicates(map, establishedKeys, establishedCreated, roster = []) { let changed = true; while (changed) { changed = false; @@ -456,8 +522,18 @@ function csConsolidateAliasDuplicates(map, establishedKeys, establishedCreated) // pass), fall back to whichever record is OLDER — the one Identify // Characters / the original cast settled on first. const sEst = establishedKeys.has(key), oEst = establishedKeys.has(dupKey); + const rosterNames = csCleanRoster(roster); + const sRoster = rosterNames.findIndex(n => n.toLowerCase() === key); + const oRoster = rosterNames.findIndex(n => n.toLowerCase() === dupKey); let keepKey = key, dropKey = dupKey; - if (oEst && !sEst) { keepKey = dupKey; dropKey = key; } + // If both names are present in the current Characters-found roster, + // preserve the roster's own order. That makes the displayed sidebar + // name authoritative (for example Fremder), while the other name + // remains an AKA instead of unexpectedly becoming the profile title. + if (sRoster >= 0 && oRoster >= 0 && sRoster !== oRoster) { + if (oRoster < sRoster) { keepKey = dupKey; dropKey = key; } + } else if (sRoster < 0 && oRoster >= 0) { keepKey = dupKey; dropKey = key; } + else if (oEst && !sEst) { keepKey = dupKey; dropKey = key; } else if (sEst === oEst) { const filled = (sh) => CS_DETAIL_FIELDS.filter(f => _csStr(sh[f]).trim()).length; const sFilled = filled(s), oFilled = filled(other); @@ -757,8 +833,11 @@ async function csGenerate(text, cacheKey, initialRoster, opts = {}) { } } finally { prog.done(); } if (_cs.cancel) { toast('Cancelled', 'error'); return null; } - csConsolidateAliasDuplicates(map, establishedKeys, establishedCreated); - const sheets = [...map.values()]; + csConsolidateAliasDuplicates(map, establishedKeys, establishedCreated, roster); + // The live casting roster (and its Characters found sidebar) owns the + // display name. Any LLM-selected real name/role stays in aliases instead of + // renaming the profile. + const sheets = csCanonicalizeRosterNames([...map.values()], roster); if (cacheKey) _cs.cache[cacheKey] = sheets; return sheets; } catch (e) { @@ -1267,6 +1346,7 @@ function csAlignmentBar(score, arcDirection, arcNote) { function csBuildImagePrompt(s, bookProfile) { if (_csStr(s.image_prompt).trim()) return _csStr(s.image_prompt).trim(); const parts = []; + if (s.gender) parts.push(`exactly ${s.gender}`); if (s.race_species) parts.push(s.race_species); if (s.archetype) parts.push(s.archetype); if (s.physical) parts.push(s.physical); @@ -1289,6 +1369,9 @@ function csBuildImagePrompt(s, bookProfile) { + `real-world default. ` : ''; return `Create a complete character reference sheet for an original character named ${s.name}, ${parts.filter(Boolean).join(', ')}. ` + + `VISUAL IDENTITY LOCK: use the exact gender, age impression, species, face shape, skin tone, hair, eyes, facial hair, ` + + `distinguishing marks, build, clothing, colors, armor, jewelry, weapons, tools, and signature props established here. ` + + `Keep one identical face identity anchor across the portrait, front/side/back turnaround, expression sheet, and later concept art. ` + settingClause + `Base the setting, era, and art style strictly on the character's own described archetype and clothing above — ` + `do not default to a modern or real-world 20th/21st-century look for occupation-sounding titles (e.g. an "Admiral" ` @@ -2241,11 +2324,17 @@ async function csForReader(opts = {}) { if (typeof showReaderView === 'function') setTimeout(() => showReaderView('chars'), 120); const pageHost = csReaderPageHost(); if (!fresh && _cs.cache[key]) { - await csSaveToLibrary(book, _cs.cache[key]); - if (pageHost) await csShow(_cs.cache[key], book, text, book, pageHost); + const cached = csCanonicalizeRosterNames(_cs.cache[key], csKnownReaderRoster()); + _cs.cache[key] = cached; + await csSaveToLibrary(book, cached); + if (pageHost) await csShow(cached, book, text, book, pageHost); return; } const knownRoster = csKnownReaderRoster(); + // Generate context after Identify Characters has produced the roster and + // before any character-sheet passage is sent. This is intentionally best + // effort: a model outage must not prevent the actual cast-profile run. + await csEnsureBookContext(book, text, knownRoster, { force: fresh }); let seedSheets = []; if (!fresh) try { if (typeof clGetAllByTagOrBook === 'function') { @@ -2257,9 +2346,25 @@ async function csForReader(opts = {}) { // the CURRENT cast (knownRoster, from the live audiobook roster) no // longer has them. Keep only saved records that still match a name in // the current cast, so this list stays in sync with what step 2 shows. - const rosterSet = new Set(knownRoster.map(n => n.toLowerCase())); - const stillCast = rec => !rosterSet.size || rosterSet.has(String(rec?.name || '').trim().toLowerCase()); - seedSheets = (existing || []).filter(stillCast).map(rec => csSeedSheet(rec)).filter(s => String(s.name || '').trim()); + const stillCast = rec => !knownRoster.length || csSheetMatchesRoster({ + name: rec?.name || '', + aliases: rec?.sheet?.aliases || '', + first_name: rec?.sheet?.first_name || '', + last_name: rec?.sheet?.last_name || '', + full_name: rec?.sheet?.full_name || '', + }, knownRoster); + seedSheets = (existing || []).filter(stillCast).map(rec => { + const sheet = csSeedSheet(rec); + const rosterName = csRosterCanonicalName(sheet, knownRoster); + if (rosterName && sheet.name.toLowerCase() !== rosterName.toLowerCase()) { + sheet.aliases = csMergeAliases( + { name: rosterName, aliases: sheet.aliases }, + { name: sheet.name, aliases: '' }, + ); + sheet.name = rosterName; + } + return sheet; + }).filter(s => String(s.name || '').trim()); } } catch (_) { seedSheets = []; } // Computed up front (not just after the whole run finishes) so "Lines" @@ -2309,6 +2414,7 @@ async function csForReaderSelective(selectedNames) { // that race regardless of which async step caused it. if (typeof showReaderView === 'function') setTimeout(() => showReaderView('chars'), 120); const pageHost = csReaderPageHost(); + const knownRoster = csKnownReaderRoster(); let records = []; try { records = (typeof clGetAllByTagOrBook === 'function') ? await clGetAllByTagOrBook(book) : []; } catch (_) { records = []; } const picked = []; @@ -2329,19 +2435,37 @@ async function csForReaderSelective(selectedNames) { } return false; }; - (records || []).forEach(rec => { - if (!rec || !rec.name || !matchesWanted(rec)) return; - const key = String(rec.name || '').trim().toLowerCase(); - if (seen.has(key)) return; - seen.add(key); - picked.push(rec); - }); + // Exact canonical names always win. Alias matching is only a fallback for + // old records that have no exact record; otherwise selecting Lysandra could + // pick an Alrik profile whose generated aliases happened to contain the + // word "Lysandra". (selectedNames || []).forEach(name => { - const key = String(name || '').trim().toLowerCase(); + const canonical = String(name || '').trim(); + const key = canonical.toLowerCase(); if (!key || seen.has(key)) return; - const fallback = { name: String(name || '').trim(), sheet: csBlankSheet(String(name || '').trim()) }; + const exact = (records || []).find(rec => String(rec?.name || '').trim().toLowerCase() === key); + const aliasMatch = exact || (records || []).find(rec => { + if (!rec || !rec.name) return false; + const tokens = csNameTokens({ + name: rec.name, + aliases: rec.sheet?.aliases || '', + first_name: rec.sheet?.first_name || '', + last_name: rec.sheet?.last_name || '', + full_name: rec.sheet?.full_name || '', + }); + return tokens.some(t => { + const lower = String(t || '').toLowerCase(); + return lower === key || lower.includes(key) || key.includes(lower); + }); + }); + let chosen = aliasMatch || { name: canonical, sheet: csBlankSheet(canonical) }; + // Keep the selected sidebar name as the generation identity even when we + // borrowed profile evidence from a legacy alias-linked record. + if (chosen.name !== canonical) { + chosen = Object.assign({}, chosen, { name: canonical, sheet: Object.assign({}, chosen.sheet || {}, { name: canonical }) }); + } seen.add(key); - picked.push(fallback); + picked.push(chosen); }); picked.sort((a, b) => (b?.sheet?.line_count || 0) - (a?.sheet?.line_count || 0) || String(a?.name || '').localeCompare(String(b?.name || ''))); if (!picked.length) { toast('No matching cast characters found', 'error'); return; } @@ -2349,13 +2473,16 @@ async function csForReaderSelective(selectedNames) { for (const rec of picked) { const targetName = String(rec.name || '').trim(); if (!targetName) continue; + const rosterName = targetName; const needles = csRecordNeedles(rec); const scanText = csEvidenceWindowText(csReaderParagraphBlocks(), needles, 2, 2) || text; const seedSheet = csSeedSheet(rec); - const sheets = await csGenerate(scanText, null, [targetName], { pageHost, seedSheets: [seedSheet] }); + seedSheet.name = targetName; + const sheets = await csGenerate(scanText, null, [rosterName], { pageHost, seedSheets: [seedSheet] }); if (!sheets) return; const filtered = sheets.filter(s => wanted.has(String(s.name || '').trim().toLowerCase())); if (!filtered.length) continue; + filtered.forEach(s => { s.name = targetName; s._rosterNameAuthoritative = true; }); if (rec?.sheet?.line_count != null) filtered.forEach(s => { s.line_count = rec.sheet.line_count; }); csMerge(finalMap, filtered); } diff --git a/static/js/characters-library.js b/static/js/characters-library.js index 42d04c8..716ed91 100644 --- a/static/js/characters-library.js +++ b/static/js/characters-library.js @@ -206,15 +206,25 @@ function clMergeTags(...parts) { async function clUpsert(book, sheet, knownId) { const name = (sheet.name || '').trim(); if (!name) return null; + const rosterNameAuthoritative = sheet._rosterNameAuthoritative === true; const bk = (book || '').trim() || 'Unsorted'; - const aliasPrev = knownId + // A live cast roster is an explicit identity, not a guess. Never let an + // alias from the generated sheet redirect `Krieger` into an older + // `Zerwas` record (or `Fremder` into `Lancorian`). The sidebar name and + // book-scoped key are authoritative; aliases remain data on that record. + const authoritativeId = rosterNameAuthoritative ? clKey(bk, name) : null; + const aliasPrev = authoritativeId + ? await clGet(authoritativeId).catch(() => null) + : knownId ? await clGet(knownId).catch(() => null) : (await clGetAll().catch(() => [])).find(r => clSameIdentity(r, bk, sheet)); - const id = aliasPrev?.id || knownId || clKey(bk, name); + const id = authoritativeId || aliasPrev?.id || knownId || clKey(bk, name); const now = new Date(); const prev = aliasPrev || await clGet(id).catch(() => null); - const merged = prev ? clMergeSheet(prev.sheet || {}, sheet) : { ..._clSanitize(sheet), name }; - const canonicalName = prev?.name || name; + const sanitized = _clSanitize(sheet); + delete sanitized._rosterNameAuthoritative; + const merged = prev ? clMergeSheet(prev.sheet || {}, sheet) : { ...sanitized, name }; + const canonicalName = rosterNameAuthoritative ? name : (prev?.name || name); merged.name = canonicalName; const tags = clMergeTags(prev?.tags, sheet.tags, bk); const color = clNormalizeColor(prev?.color || prev?.sheet?.color || sheet.color, canonicalName); diff --git a/static/js/engines.js b/static/js/engines.js index 5150a30..53892f8 100644 --- a/static/js/engines.js +++ b/static/js/engines.js @@ -150,7 +150,7 @@ function initElevenLabsBrowser() {
- ${prev ? `` : ''} + ${prev ? `` : ''}
`; } @@ -170,15 +170,54 @@ function initElevenLabsBrowser() { _elAudio.addEventListener('ended', () => { btn.textContent = '▶'; _elAudio = null; _elAudioBtn = null; }); } - function elImport(btn) { + async function elImport(btn) { const url = btn.dataset.preview; if (!url) return; - if (typeof navTo === 'function') navTo('s-clone'); - setTimeout(() => { - const inp = $('lib-add-url'), btn2 = $('lib-add-url-btn'); - if (inp) { inp.value = url; inp.dispatchEvent(new Event('input')); } - if (btn2) btn2.click(); - }, 120); + const original = btn.innerHTML; + btn.disabled = true; + btn.innerHTML = ' Importing…'; + + const name = btn.dataset.name || 'ElevenLabs voice'; + const lang = (typeof sourceLanguageCode === 'function' + ? sourceLanguageCode(btn.dataset.language) + : String(btn.dataset.language || 'EN').slice(0, 2).toUpperCase()); + const gender = typeof sourceGenderCode === 'function' + ? sourceGenderCode(btn.dataset.gender, name) + : 'N'; + const base = name.normalize('NFKD').replace(/[\u0300-\u036f]/g, '') + .replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 40) || 'Voice'; + const voiceId = `EL_${lang}_${gender}_${base}`; + + try { + const r = await fetch('/api/quick-import-voice', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ audio_url: url, voice_id: voiceId, transcript: '' }), + }); + if (!r.ok) { + const e = await r.json().catch(() => ({})); + throw new Error(e.detail || r.statusText); + } + const d = await r.json(); + if (typeof saveMeta === 'function') { + await saveMeta(d.voice_id, { + name, tag: 'elevenlabs', group: 'elevenlabs', origin: 'cloned', + gender, language: lang, + note: (btn.dataset.description || '').slice(0, 180), + source: `ElevenLabs shared voice${btn.dataset.voiceId ? ` · ${btn.dataset.voiceId}` : ''}`, + }).catch(() => {}); + } + btn.innerHTML = ' In library'; + btn.classList.add('el-imported'); + if (typeof toast === 'function') toast(`Imported "${name}" to Voice Library`, 'success'); + if (typeof loadVoiceLibrary === 'function') { + _libraryLoadPromise = null; + await loadVoiceLibrary(); + } + } catch (e) { + btn.disabled = false; + btn.innerHTML = original; + if (typeof toast === 'function') toast('Import failed: ' + e.message, 'error'); + } } elLoadKey(); diff --git a/static/js/library-characters.js b/static/js/library-characters.js index d64649a..2352d83 100644 --- a/static/js/library-characters.js +++ b/static/js/library-characters.js @@ -97,6 +97,7 @@ async function libraryRenderCharacters() { ['lines', 'Anzahl Zeilen'], ['gender', 'Geschlecht'], ['voice', 'Stimme zugewiesen'], + ['rating', 'Stimmenbewertung'], ]; const sortMode = SORT_OPTIONS.some(function (o) { return o[0] === localStorage.getItem('ttsvc_libchars_sort'); }) ? localStorage.getItem('ttsvc_libchars_sort') : 'tier'; @@ -148,6 +149,11 @@ async function libraryRenderCharacters() { return ga.localeCompare(gb) || (a.name || '').localeCompare(b.name || ''); }, voice: function (a, b) { return (b.voice ? 1 : 0) - (a.voice ? 1 : 0) || (a.name || '').localeCompare(b.name || ''); }, + rating: function (a, b) { + const idOf = function (r) { return r.voice ? (typeof r.voice === 'object' ? r.voice.id : r.voice) : ''; }; + const ratingOf = function (r) { return Number((window._voices || []).find(function (v) { return v.id === idOf(r); })?.rating || 0); }; + return ratingOf(b) - ratingOf(a) || (a.name || '').localeCompare(b.name || ''); + }, age: function (a, b) { return (_charAgeSortVal(a.sheet) - _charAgeSortVal(b.sheet)) || (a.name || '').localeCompare(b.name || ''); }, language: function (a, b) { return _charLangLabel(a).localeCompare(_charLangLabel(b)) || (a.name || '').localeCompare(b.name || ''); }, align: function (a, b) { return (b.sheet?.moral_alignment_score ?? -1) - (a.sheet?.moral_alignment_score ?? -1) || (a.name || '').localeCompare(b.name || ''); }, @@ -421,6 +427,41 @@ async function libraryRenderCharacters() { // Wire voice selectors and auto-assign buttons _wireCharCards(prod, byId, chars); + // Keep the Voice Library's review metadata available where casting + // decisions are made. Ratings and notes belong to the assigned voice + // (not the character), so edits write through the same saveMeta path as + // the Voice Library and immediately stay consistent in both views. + prod.querySelectorAll('.lib-chars-tbl-rating-star').forEach(function (star) { + star.addEventListener('click', async function (e) { + e.stopPropagation(); + const voiceId = star.dataset.voiceId; + const voice = (window._voices || []).find(function (v) { return v.id === voiceId; }); + if (!voice || typeof saveMeta !== 'function') return; + const value = Number(star.dataset.rating || 0); + voice.rating = value === Number(voice.rating || 0) ? 0 : value; + prod.querySelectorAll('.lib-chars-tbl-rating-star').forEach(function (btn) { + if (btn.dataset.voiceId === voiceId) btn.classList.toggle('is-on', Number(btn.dataset.rating) <= voice.rating); + }); + try { await saveMeta(voiceId, { rating: voice.rating }); } + catch (err) { toast('Could not save voice rating: ' + (err.message || err), 'error'); } + }); + }); + prod.querySelectorAll('.lib-chars-tbl-voice-note').forEach(function (input) { + let timer = null; + input.addEventListener('click', function (e) { e.stopPropagation(); }); + input.addEventListener('input', function () { + clearTimeout(timer); + timer = setTimeout(async function () { + const voiceId = input.dataset.voiceId; + const voice = (window._voices || []).find(function (v) { return v.id === voiceId; }); + if (!voice || typeof saveMeta !== 'function') return; + voice.note = input.value; + try { await saveMeta(voiceId, { note: voice.note }); } + catch (err) { toast('Could not save voice note: ' + (err.message || err), 'error'); } + }, 600); + }); + }); + productions.appendChild(prod); }); container.appendChild(productions); @@ -821,7 +862,16 @@ function _charsTableHtml(chars, sortMode, sortDir) { const sh = rec.sheet || {}; const hue = _charHue(rec.name); const voiceId = rec.voice ? (typeof rec.voice === 'object' ? (rec.voice.id || '') : String(rec.voice)) : ''; + const voiceMeta = voiceId ? (window._voices || []).find(function (v) { return v.id === voiceId; }) : null; const voiceMissing = !!voiceId && !_voiceExists(voiceId); + const voiceRating = Math.max(0, Math.min(5, Number(voiceMeta?.rating || 0))); + const voiceNote = _libStr(voiceMeta?.note); + const voicePrompt = _libStr(voiceMeta?.voice_design_prompt || sh.voice_design_prompt); + const stars = voiceId + ? '
' + [1, 2, 3, 4, 5].map(function (n) { + return ''; + }).join('') + '
' + : '—'; const voiceLang = _charLangLabel(rec); const tier = String(sh.tier || '').toLowerCase(); const tierBadge = tier === 'main' ? 'Haupt' @@ -865,6 +915,11 @@ function _charsTableHtml(chars, sortMode, sortDir) { + (voiceId ? '' : '') + '' + '' + + '' + stars + '' + + '' + (voicePrompt ? escHtml(voicePrompt) : '—') + '' + + '' + + (voiceId ? '' : '—') + + '' + '
' + tagList.map(function (t) { return '' + escHtml(t) + ''; }).join('') + '
' + '' + ''; @@ -877,11 +932,12 @@ function _charsTableHtml(chars, sortMode, sortDir) { // the same row. Fixed explicit widths sidestep the whole class of bug. + '' + '' - + '' + + '' + '' + '' + '' + th('alpha', 'Name') + th('gender', 'Geschlecht') + th('age', 'Alter', 'Estimated age') - + th('lines', 'Zeilen', 'Anzahl Zeilen') + th('language', 'Sprache') + th('align', 'Gut/Böse', 'Moralische Gesinnung') + th('voice', 'Stimme') + + th('lines', 'Zeilen', 'Anzahl Zeilen') + th('language', 'Sprache') + th('align', 'Gut/Böse', 'Moralische Gesinnung') + th('voice', 'Stimme') + th('rating', 'Rating', 'Rate the assigned voice') + + 'Voice Design PromptNotes' + 'TagsBook / Script' + '' + rows + ''; } @@ -1009,6 +1065,7 @@ function _charLineCount(c) { async function _charDetailPage(rec, allChars, opts) { opts = opts || {}; + window._libDetailOpts = opts; // Defaults to the Library grid's own list container/back-navigation, but // any caller showing character cards elsewhere (e.g. the fresh-recast // results grid on the Read Aloud page) can pass its own container + onBack @@ -1042,13 +1099,8 @@ async function _charDetailPage(rec, allChars, opts) { const avatarHtml = '
' + (rec.image - ? '
' + escHtml(rec.name) + '
' - : '
' + escHtml((rec.name || '?')[0].toUpperCase()) + '
') - + '
' - + '' - + '' - + '' - + '
' + ? '
' + escHtml(rec.name) + '
' + : '
' + escHtml((rec.name || '?')[0].toUpperCase()) + '
') + '
'; // Concept art (the auto-generated NPC design-sheet image, distinct from @@ -1177,7 +1229,7 @@ async function _charDetailPage(rec, allChars, opts) { pg.style.gridColumn = '1 / -1'; pg.innerHTML = '
' - + '' + + '' + '
' + avatarHtml + '
' @@ -1196,11 +1248,12 @@ async function _charDetailPage(rec, allChars, opts) { + conceptArtHtml + '
' + '' + + (voiceId ? '' : '') + '' + (voiceId ? (!_voiceExists(voiceId) ? ' ' + escHtml(voiceId) + '' : escHtml(voiceId)) : 'Noch keine Stimme zugewiesen') + '' + + '' + '' + '' + '' - + '' + '' + '
' + alignHtml @@ -1258,65 +1311,19 @@ async function _charDetailPage(rec, allChars, opts) { container.appendChild(pg); - pg.querySelector('.lib-cpg-back').addEventListener('click', function () { goBack(); }); - - const _lcdUploadAvatar = function () { - const inp = document.createElement('input'); - inp.type = 'file'; inp.accept = 'image/*'; - inp.onchange = async function () { - const file = inp.files[0]; if (!file) return; - const fr = new FileReader(); - fr.onload = async function (ev) { - if (typeof clSetImage === 'function') await clSetImage(rec.id, ev.target.result); - toast('Profilbild gespeichert', 'success'); - rec.image = ev.target.result; - _syncVoicePictureFromChar(rec); - const av = pg.querySelector('.lcd-avatar-upload'); - if (av) av.innerHTML = '' + escHtml(rec.name) + ''; - }; - fr.readAsDataURL(file); - }; - inp.click(); - }; - pg.querySelector('.lcd-avatar-upload').addEventListener('click', _lcdUploadAvatar); - pg.querySelector('.lcd-avatar-upload-btn')?.addEventListener('click', function (e) { e.stopPropagation(); _lcdUploadAvatar(); }); - - // Opens an image search in a new tab for the user to browse and save an - // image themselves (dragging it onto the avatar, or downloading and using - // the Upload button) — no scraping/auto-fetch, since that'd need to pick a - // result and copyright/likeness on a random web image is the user's call. - pg.querySelector('.lcd-avatar-online-btn')?.addEventListener('click', function (e) { + pg.querySelector('.lib-cpg-back').addEventListener('click', function (e) { + e.preventDefault(); e.stopPropagation(); - const q = [rec.name, rec.book, sh.archetype, 'character art'].filter(Boolean).join(' '); - window.open('https://www.google.com/search?tbm=isch&q=' + encodeURIComponent(q), '_blank', 'noopener'); + Promise.resolve(goBack()).catch(function (err) { + console.error('[character detail] back navigation failed', err); + toast('Character list could not be opened', 'error'); + }); }); - pg.querySelector('.lcd-avatar-gen-btn')?.addEventListener('click', async function (e) { - e.stopPropagation(); - const btn = this; - const bookProfile = typeof _getBookProfile === 'function' ? await _getBookProfile(rec.book) : {}; - const prompt = _libStr(sh.image_prompt).trim() || (typeof csBuildImagePrompt === 'function' ? csBuildImagePrompt(sh, bookProfile) : ''); - if (!prompt) { toast('No image prompt to work from — generate the Character Image Prompt below first', 'error'); return; } - const orig = btn.innerHTML; - btn.disabled = true; - btn.innerHTML = ''; - try { - const r = await fetch('/api/character-generate-image', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ prompt: prompt }), - }); - if (!r.ok) throw new Error((await r.json().catch(function () { return {}; })).detail || r.statusText); - const d = await r.json(); - if (typeof clSetImage === 'function') await clSetImage(rec.id, d.image); - rec.image = d.image; - _syncVoicePictureFromChar(rec); - toast('Profile picture generated', 'success'); - _charDetailPage(rec, allChars, opts); - } catch (err) { - toast('Image generation failed: ' + (err.message || err), 'error'); - btn.disabled = false; - btn.innerHTML = orig; - } + // Both the initial letter and an existing portrait open the same clear + // image workflow; clicking no longer launches an unexplained file picker. + pg.querySelector('.lcd-avatar-upload')?.addEventListener('click', function () { + _openAvatarLightbox(rec, function () { _charDetailPage(rec, allChars, opts); }); }); pg.querySelector('.lcd-conceptart-gen')?.addEventListener('click', async function (e) { @@ -1340,22 +1347,10 @@ async function _charDetailPage(rec, allChars, opts) { } }); - pg.querySelector('.lcd-conceptart-img')?.addEventListener('click', function () { - document.getElementById('conceptart-lightbox')?.remove(); - const ov = document.createElement('div'); - ov.id = 'conceptart-lightbox'; - ov.className = 'audiobook-overlay'; - ov.innerHTML = '
' - + '
' + escHtml(rec.name) + ' — Konzeptbild' - + '' - + '' - + '
' - + 'Concept art — ' + escHtml(rec.name) + '' - + '
'; - document.body.appendChild(ov); - const close = function () { ov.remove(); }; - ov.querySelector('#calb-close').addEventListener('click', close); - ov.addEventListener('click', function (e) { if (e.target === ov) close(); }); + pg.querySelector('.lcd-conceptart-img, .lcd-conceptart-empty')?.addEventListener('click', function () { + _openConceptArtLightbox(rec, function () { + _charDetailPage(rec, allChars, opts); + }); }); pg.querySelectorAll('.lib-cpg-sidebar-item').forEach(function (item) { @@ -1388,6 +1383,11 @@ async function _charDetailPage(rec, allChars, opts) { pg.querySelector('.lcd-online-voice')?.addEventListener('click', function () { _charSearchOnline(rec); }); pg.querySelector('.lcd-gen-voice')?.addEventListener('click', function () { _charDesignVoiceInline(rec); }); pg.querySelector('.lcd-clone-voice')?.addEventListener('click', function () { _charCloneVoice(rec); }); + pg.querySelector('.lcd-preview-voice')?.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + _libPreviewCharVoice(rec, e.currentTarget); + }); // Generation Prompts section: copy buttons + one-call generation of all four // external-tool prompts (Voice Design / Image / SillyTavern / Concept Art) @@ -1565,10 +1565,10 @@ function _charDetailModal(rec, allChars) { + '
' + '' + '' + (voiceId ? (!_voiceExists(voiceId) ? ' ' + escHtml(voiceId) + '' : escHtml(voiceId)) : 'Noch keine Stimme zugewiesen') + '' + + '' + '' + '' + '' - + '' + '
' + alignHtml + '
' @@ -1693,12 +1693,16 @@ function _openAvatarLightbox(rec, onSaved) { + '
' + (rec.image ? '' + escHtml(rec.name) + '' - : '
') + : '
' + escHtml((rec.name || '?')[0].toUpperCase()) + '
') + '
' + '
' + + '
' + + '' + + '' + + '' + + '
' + '
' - + '' - + '' + + '' + '
' + '
' + '' @@ -1719,7 +1723,6 @@ function _openAvatarLightbox(rec, onSaved) { + '' + '' + '' - + '' + '
' + '
' + '
' @@ -1729,6 +1732,11 @@ function _openAvatarLightbox(rec, onSaved) { document.body.appendChild(ov); ov.addEventListener('click', function (e) { if (e.target === ov) ov.remove(); }); ov.querySelector('#alb-close').addEventListener('click', function () { ov.remove(); }); + ov.querySelector('#alb-upload-btn').addEventListener('click', function () { ov.querySelector('#alb-file-input').click(); }); + ov.querySelector('#alb-online-focus-btn').addEventListener('click', function () { + ov.querySelector('#alb-url-input').focus(); + ov.querySelector('#alb-url-input').scrollIntoView({ behavior: 'smooth', block: 'center' }); + }); const setPreview = function (src) { ov.querySelector('.alb-preview').innerHTML = '' + escHtml(rec.name) + ''; @@ -2165,6 +2173,56 @@ function _hashPick(str, pool) { for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0; return pool[h % pool.length]; } + +// A species or profession is only useful to a voice-design model when it is +// translated into something a listener can actually hear. Sending it labels +// such as "orc" or "blacksmith" alone led to the model producing the same +// generic human voice for most of the cast. These are intentionally vocal +// directions, not lore: they preserve intelligibility for an audiobook while +// giving each kind of character a clearly different sound world. +function _voiceAudibleSpeciesCue(value) { + const v = String(value || '').toLowerCase(); + if (/\b(ork|orc|orcs)\b/.test(v)) return 'Orc: chest-led, gravel-edged resonance; clipped consonants, forceful breath, fully intelligible.'; + if (/\b(vampir|vampire)\b/.test(v)) return 'Vampire: cool velvety timbre, precise diction, restrained predatory calm.'; + if (/\b(elf|elv|elfe)\b/.test(v)) return 'Elf: clear airy resonance, precise musical diction, adult and grounded.'; + if (/\b(d[aä]mon|demon|teufel|devil)\b/.test(v)) return 'Demon: dark resonant timbre with a subtle otherworldly undertone; fully intelligible.'; + if (/\b(zwerg|dwarf)\b/.test(v)) return 'Dwarf: compact earthy resonance, sturdy cadence, deliberate articulation.'; + if (/\b(fee|fae|geist|spirit|untot|undead)\b/.test(v)) return 'Otherworldly: unusual natural resonance, measured breath, emotionally believable.'; + return ''; +} + +function _voiceAudibleRoleCue(sheet) { + const v = [sheet.profession, sheet.title, sheet.archetype, sheet.social_class, sheet.capabilities].filter(Boolean).join(' ').toLowerCase(); + if (/krieger|soldat|ritter|wache|kämpfer|warrior|soldier|knight|guard|general|anführer|commander/.test(v)) return 'Warrior/commander: grounded projection, disciplined breath, concise command cadence.'; + if (/schmied|blacksmith|handwerk|artisan|craft/.test(v)) return 'Craftsperson: practical work-worn texture, measured pace, plain-spoken certainty.'; + if (/magier|zauber|hexe|hexer|wizard|mage|sorcer|druid/.test(v)) return 'Magic-user: measured phrasing, precise diction, contained intensity.'; + if (/priester|inquis|mönch|monk|priest|cleric|prophet/.test(v)) return 'Religious authority: formal diction, calm conviction, weighted pauses.'; + if (/könig|königin|baron|graf|adel|noble|lord|lady|herrscher|herrscherin/.test(v)) return 'High-status authority: polished diction, composed pace, effortless command.'; + if (/dieb|spion|assassin|schurke|rogue|thief/.test(v)) return 'Covert operator: quiet confidence, economical phrasing, alert timing.'; + if (/gelehrt|scholar|lehrer|teacher|arzt|heiler|healer/.test(v)) return 'Learned professional: articulate, patient pace, observant and assured.'; + return ''; +} + +function _voiceIdentityPrompt(rec, sourcePrompt, langName) { + const sh = rec.sheet || {}; + const rawGender = String(sh.gender || _genderFromGenericName(rec.name) || '').toLowerCase(); + const gender = rawGender.startsWith('f') ? 'female' : rawGender.startsWith('m') ? 'male' : rawGender.startsWith('non') ? 'androgynous/nonbinary' : ''; + const hardGender = gender + ? 'Adult ' + gender + ' voice, clearly ' + (gender === 'female' ? 'feminine and never masculine-coded' : gender === 'male' ? 'masculine and never feminine-coded' : 'androgynous') + '.' + : ''; + const age = String(sh.age_estimate || '').trim(); + const ageCue = age ? 'Age: ' + age.slice(0, 36) + '.' : ''; + const lang = String(langName || '').trim(); + const languageCue = lang ? (lang.toLowerCase() === 'english' ? 'Neutral international English.' : 'Native ' + lang + ' delivery, never American-accented.') : ''; + const species = _voiceAudibleSpeciesCue([sh.race_species, sh.archetype, sh.profession, sh.aliases].filter(Boolean).join(' ')); + const role = _voiceAudibleRoleCue(sh); + const distinct = 'Distinct natural audiobook voice; no imitation.'; + const prefix = [hardGender, ageCue, languageCue, species, role, distinct].filter(Boolean).join(' '); + const base = String(sourcePrompt || '').replace(/\s+/g, ' ').trim(); + const room = Math.max(0, 480 - prefix.length - (prefix && base ? 1 : 0)); + return (prefix + (base ? ' ' + base.slice(0, room) : '')).slice(0, 480); +} + function _buildVoicePrompt(rec, profile, langName) { const sh = rec.sheet || {}; const g = String(sh.gender || '').toLowerCase(); @@ -2365,7 +2423,7 @@ async function _charDesignVoice(rec) { _selectLoose(document.getElementById('design-gender'), sh.gender); _selectLoose(document.getElementById('design-language'), lang); const instruct = document.getElementById('design-instruct'); - if (instruct) instruct.value = savedPrompt || _buildVoicePrompt(rec, null, lang); + if (instruct) instruct.value = _voiceIdentityPrompt(rec, savedPrompt || _buildVoicePrompt(rec, null, lang), lang); const nm = document.getElementById('design-preset-name'); if (nm) nm.value = rec.name; }, 140); @@ -2384,7 +2442,7 @@ function _charDesignVoiceInline(rec) { const sh = rec.sheet || {}; const lang = _charLang(rec); const savedPrompt = _libStr(sh.voice_design_prompt).trim(); - const instruct = savedPrompt || _buildVoicePrompt(rec, null, lang); + const instruct = _voiceIdentityPrompt(rec, savedPrompt || _buildVoicePrompt(rec, null, lang), lang); const ov = document.createElement('div'); ov.className = 'audiobook-overlay'; ov.innerHTML = '
' @@ -2410,9 +2468,22 @@ function _charDesignVoiceInline(rec) { try { await _charAutoDesignVoice(rec, true, text); _schedulePendingTtsRestart(); + if (!rec.sheet) rec.sheet = {}; + rec.sheet.voice_design_prompt = text; close(); toast('New voice designed for ' + rec.name, 'success'); - if (typeof libraryRenderCharacters === 'function') libraryRenderCharacters(); + // Preserve the open profile. Re-rendering the entire library here can + // replace the detail page while the async save is finishing, leaving a + // blank-looking profile until the user navigates away and back. + const detailPage = document.querySelector('.lib-char-page'); + const detailOpts = window._libDetailOpts; + if (detailPage && detailOpts && typeof _charDetailPage === 'function') { + const all = typeof clGetAll === 'function' ? await clGetAll().catch(function () { return []; }) : []; + const updated = all.find(function (r) { return r.id === rec.id; }) || rec; + _charDetailPage(updated, all.filter(function (r) { return r.book === updated.book; }), detailOpts); + } else if (typeof libraryRenderCharacters === 'function') { + await libraryRenderCharacters(); + } if (typeof _libRefreshDetailModal === 'function') _libRefreshDetailModal(rec); } catch (err) { toast('Voice design failed: ' + (err.message || err), 'error'); @@ -2599,8 +2670,9 @@ async function _charAutoDesignVoice(rec, force, instructOverride) { } const sh = rec.sheet || {}; const langName = (await _resolveBookLang(rec)) || 'English'; - const instruct = instructOverride || _buildVoicePrompt(rec, await _getBookProfile(rec.book), langName); + const instruct = _voiceIdentityPrompt(rec, instructOverride || _libStr(sh.voice_design_prompt).trim() || _buildVoicePrompt(rec, await _getBookProfile(rec.book), langName), langName); if (!instruct.trim()) throw new Error('No character description to design a voice from yet'); + if (instructOverride) sh.voice_design_prompt = instruct; const langCode = (typeof DESIGN_LANG_CODE !== 'undefined' && DESIGN_LANG_CODE[langName]) || 'EN'; // A generic/minor entry the casting pass extracted from a plain noun in // the text ("die Frau", "der Mann", "ein Bote") rather than a real named @@ -2815,9 +2887,105 @@ async function _charAutoGenerateConceptArt(rec, provider) { return d.image; } +// Concept art uses the same editable image workflow as profile portraits, +// but stores its result in sheet.concept_art_image so the portrait is never +// replaced accidentally. +function _openConceptArtLightbox(rec, onSaved) { + document.getElementById('conceptart-lightbox')?.remove(); + const sh = rec.sheet || {}; + const prompt = _libStr(sh.concept_art_prompt).trim(); + const ov = document.createElement('div'); + ov.id = 'conceptart-lightbox'; + ov.className = 'audiobook-overlay'; + ov.innerHTML = '
' + + '
' + escHtml(rec.name) + ' — Konzeptbild' + + '' + + '' + + '
' + + '
' + + (sh.concept_art_image + ? 'Concept art — ' + escHtml(rec.name) + '' + : '
No concept art yet
') + + '
' + + '
' + + '' + + '' + + '' + + '
' + + '' + + '
' + + '' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '
'; + document.body.appendChild(ov); + const close = function () { ov.remove(); }; + const status = function (msg, cls) { + const el = ov.querySelector('#calb-status'); + el.textContent = msg || ''; + el.className = 'llm-active-status' + (cls ? ' ' + cls : ''); + }; + const setPreview = function (src) { + ov.querySelector('.conceptart-editor-preview').innerHTML = 'Concept art — ' + escHtml(rec.name) + ''; + }; + const commit = async function (dataUri) { + const current = typeof clGet === 'function' ? await clGet(rec.id).catch(function () { return null; }) : null; + const target = current || rec; + target.sheet = Object.assign({}, target.sheet || {}, { concept_art_image: dataUri }); + target.updated = new Date(); + if (typeof clPut === 'function') await clPut(target); + rec.sheet = target.sheet; + setPreview(dataUri); + toast('Concept art saved', 'success'); + if (typeof onSaved === 'function') onSaved(); + }; + ov.addEventListener('click', function (e) { if (e.target === ov) close(); }); + ov.querySelector('#calb-close').addEventListener('click', close); + ov.querySelector('#calb-upload').addEventListener('click', function () { ov.querySelector('#calb-file').click(); }); + ov.querySelector('#calb-online').addEventListener('click', function () { + ov.querySelector('#calb-url').focus(); + ov.querySelector('#calb-url').scrollIntoView({ behavior: 'smooth', block: 'center' }); + }); + ov.querySelector('#calb-file').addEventListener('change', function () { + const file = this.files[0]; if (!file) return; + const fr = new FileReader(); + fr.onload = function (e) { commit(e.target.result).catch(function (err) { toast('Upload failed: ' + err.message, 'error'); }); }; + fr.readAsDataURL(file); + }); + ov.querySelector('#calb-url-btn').addEventListener('click', async function () { + const url = ov.querySelector('#calb-url').value.trim(); + if (!url) { toast('Enter an image URL', 'error'); return; } + const btn = this; btn.disabled = true; status('Downloading…'); + try { + const r = await fetch('/api/character-image-from-url', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url }) }); + const d = await r.json(); if (!r.ok) throw new Error(d.detail || r.statusText); + await commit(d.image); status('✓ Downloaded', 'ok'); + } catch (e) { status('Failed', 'err'); toast('Download failed: ' + e.message, 'error'); } + finally { btn.disabled = false; } + }); + ov.querySelector('#calb-regenerate').addEventListener('click', async function () { + const nextPrompt = ov.querySelector('#calb-prompt').value.trim(); + if (!nextPrompt) { toast('Generate or enter a concept-art prompt first', 'error'); return; } + const btn = this; const orig = btn.innerHTML; btn.disabled = true; + btn.innerHTML = ' Generating…'; status('Generating…'); + try { + sh.concept_art_prompt = nextPrompt; + const provider = ov.querySelector('#calb-provider').value; + const img = await _charAutoGenerateConceptArt(rec, provider); + await commit(img); status('✓ Generated', 'ok'); + } catch (e) { status('Failed', 'err'); toast('Generation failed: ' + e.message, 'error'); } + finally { btn.disabled = false; btn.innerHTML = orig; } + }); +} + window._charSearchOnline = _charSearchOnline; window._charDesignVoice = _charDesignVoice; window._charAutoDesignVoice = _charAutoDesignVoice; window._charAutoGenerateImage = _charAutoGenerateImage; window._charAutoGenerateConceptArt = _charAutoGenerateConceptArt; +window._openConceptArtLightbox = _openConceptArtLightbox; window.libraryRenderCharacters = libraryRenderCharacters; diff --git a/static/js/studio.js b/static/js/studio.js index 03c561b..d365ce3 100644 --- a/static/js/studio.js +++ b/static/js/studio.js @@ -336,21 +336,62 @@ document.querySelectorAll('#stu-cast-inner-tabs .stu-inner-tab').forEach(functio // Rehearser" still showing) sitting back at the bottom. Watch for it instead // of relocating once. let _stuCastFootObserver = null; +let _stuCastFootHealthTimer = null; + +// The Studio controls must never depend on the borrowed audiobook footer. +// audiobookCastView rebuilds that footer during several async transitions, +// which previously left this toolbar empty because the plain Studio buttons +// had already been hidden. The only time they may be hidden is while a cast +// job is actually running; in that state the borrowed footer exposes Cancel. +function _stuCastIsRunning() { + return typeof _audiobook !== 'undefined' && !!_audiobook.running; +} + +function _stuSyncCastFallbackControls(tabs, innerTabs, hasActionFooter) { + const running = _stuCastIsRunning(); + tabs.hidden = false; + tabs.style.display = 'flex'; + innerTabs.forEach(function (tab) { + // The real three-menu action bar replaces these two simple navigation + // buttons only after its completed-cast actions exist. Until then they + // remain the reliable fallback, rather than leaving an empty toolbar. + const hideFallback = running || hasActionFooter; + tab.hidden = hideFallback; + tab.style.display = hideFallback ? 'none' : ''; + }); +} + function _stuRelocateCastFoot() { _stuTryRelocateCastFoot(); if (_stuCastFootObserver) return; - const slot = document.getElementById('stu-cast-slot'); - if (!slot) return; + // The audiobook panel is rebuilt asynchronously and can briefly be moved + // through its native reader container before Studio's borrow completes. + // Observe the document, not only the slot, so a freshly-created footer is + // never stranded outside Studio's visible action strip. _stuCastFootObserver = new MutationObserver(function () { _stuTryRelocateCastFoot(); }); - _stuCastFootObserver.observe(slot, { childList: true, subtree: true }); + _stuCastFootObserver.observe(document.body, { childList: true, subtree: true }); + [0, 100, 300, 800, 1500].forEach(ms => setTimeout(_stuTryRelocateCastFoot, ms)); + // Async restore/recast paths can replace the footer without producing a + // useful childList mutation in the Studio slot. Keep a lightweight health + // check while Studio is active so the toolbar can never remain blank. + _stuCastFootHealthTimer = setInterval(function () { + if (_stuIsActive && _stuActive === 2) _stuTryRelocateCastFoot(); + }, 500); } function _stuTryRelocateCastFoot() { const slot = document.getElementById('stu-cast-slot'); const tabs = document.getElementById('stu-cast-inner-tabs'); if (!tabs) return; - const freshFoot = slot ? slot.querySelector('#ab-cv-foot') : null; - const alreadyRelocated = tabs.querySelector('#ab-cv-foot'); + const freshFoot = (slot && slot.querySelector('#ab-cv-foot')) || document.querySelector('#ab-cv-foot'); + const innerTabs = Array.from(tabs.querySelectorAll(':scope > .stu-inner-tab')); + // Do not take the footer while audiobookCastView is still building it. + // Its initial Cancel/placeholder buttons are replaced asynchronously by + // the completed-cast Identify / Cast Characters / Cast menus. Relocating + // that early steals the node from view.complete(), so its submenu buttons + // can never be installed and Studio ends up with only the fallback tabs. + const footerHasActions = freshFoot && freshFoot.querySelector('#ab-cv-menu-identify, #ab-cv-menu-cast, #ab-cv-menu-viewcast'); + _stuSyncCastFallbackControls(tabs, innerTabs, !!footerHasActions); // Transient states (e.g. "Checking saved cast before starting a new // one…") rebuild the panel WITHOUT a footer at all — confirmed live: the // plain tabs stayed hidden from an earlier relocation with nothing having @@ -358,17 +399,28 @@ function _stuTryRelocateCastFoot() { // showing the plain tabs any time there's genuinely no footer to relocate // (neither a fresh one nor an already-relocated one), rather than only // ever hiding them and never re-checking. - if (!freshFoot && !alreadyRelocated) { - document.querySelectorAll('#stu-cast-inner-tabs > .stu-inner-tab').forEach(function (t) { t.hidden = false; }); + // The footer is intentionally empty during some async rebuild phases. + // Never hide the usable Studio buttons while that happens. + if (!freshFoot || !footerHasActions) { + return; + } + // A previous relocation can leave the footer in the toolbar but hidden + // after audiobookCastView rebuilds its own panel. Always restore its + // visibility when it is already in Studio's toolbar. + if (freshFoot.parentElement === tabs) { + freshFoot.hidden = false; + freshFoot.style.display = 'flex'; return; } - if (!freshFoot || freshFoot.parentElement === tabs) return; tabs.querySelectorAll('#ab-cv-foot').forEach(function (stale) { stale.remove(); }); + tabs.hidden = false; + tabs.style.display = 'flex'; freshFoot.style.borderTop = 'none'; freshFoot.style.padding = '0'; freshFoot.style.justifyContent = 'flex-start'; + freshFoot.hidden = false; + freshFoot.style.display = 'flex'; tabs.appendChild(freshFoot); - document.querySelectorAll('#stu-cast-inner-tabs > .stu-inner-tab').forEach(function (t) { t.hidden = true; }); const openReh = freshFoot.querySelector('#ab-cv-open-reh'); if (openReh) openReh.hidden = true; } @@ -380,6 +432,7 @@ function _stuTryRelocateCastFoot() { // panel itself goes home, so the footer rides back inside it intact. function _stuRestoreCastFoot() { if (_stuCastFootObserver) { _stuCastFootObserver.disconnect(); _stuCastFootObserver = null; } + if (_stuCastFootHealthTimer) { clearInterval(_stuCastFootHealthTimer); _stuCastFootHealthTimer = null; } const tabs = document.getElementById('stu-cast-inner-tabs'); const panel = document.getElementById('reader-audiobook-panel'); const foot = tabs ? tabs.querySelector('#ab-cv-foot') : null; diff --git a/static/style.css b/static/style.css index 4e09995..f0315c4 100644 --- a/static/style.css +++ b/static/style.css @@ -609,7 +609,7 @@ audio { width: 100%; } /* Table view — dense alternative to the card grid for scanning a large cast */ .lib-chars-tbl-wrap { overflow-x:auto; border:1px solid var(--border); border-radius:12px; background:var(--surface); position:relative; } -.lib-chars-tbl { width:100%; min-width:1662px; table-layout:fixed; border-collapse:collapse; font-size:12.9px; white-space:nowrap; } +.lib-chars-tbl { width:100%; min-width:2410px; table-layout:fixed; border-collapse:collapse; font-size:12.9px; white-space:nowrap; } .lib-chars-tbl td { overflow: hidden; text-overflow: ellipsis; } .lib-chars-tbl thead th { text-align:left; padding:8px 8px; font-size:10.5px; font-weight:800; text-transform:uppercase; @@ -686,6 +686,18 @@ tr.lib-char-card.lib-chars-tbl-row:hover { transform: none; box-shadow: none; ba matching its own header entirely). Flex only the inner wrapper instead. */ .lib-chars-tbl-tags { white-space:normal; max-width:260px; } .lib-chars-tbl-tags-wrap { display:flex; flex-wrap:wrap; gap:3px; } +.lib-chars-tbl-rating { display:inline-flex; gap:1px; } +.lib-chars-tbl-rating-star { + border:0; background:transparent; padding:1px; color:#d1d5db; cursor:pointer; line-height:1; +} +.lib-chars-tbl-rating-star:hover, .lib-chars-tbl-rating-star.is-on { color:#f59e0b; } +.lib-chars-tbl-prompt { color:var(--subtext); font-size:11.5px; font-style:italic; } +.lib-chars-tbl-note-cell { padding:5px 8px !important; } +.lib-chars-tbl-voice-note { + width:100%; min-width:0; border:1px solid transparent; background:transparent; color:var(--text); + padding:4px 5px; border-radius:5px; font:inherit; +} +.lib-chars-tbl-voice-note:hover, .lib-chars-tbl-voice-note:focus { border-color:var(--border); background:var(--panel); outline:none; } .lib-chars-tbl-dash { opacity:.4; } .lib-chars-tbl-check { display:inline-flex; } .lib-chars-tbl-check.is-yes { color:#4caf50; } @@ -970,7 +982,7 @@ tr.lib-char-card.lib-chars-tbl-row:hover { transform: none; box-shadow: none; ba .lib-char-card-banner { aspect-ratio: auto; height: auto; min-height: 120px; } .lib-char-avatar { width:82px; height:82px; font-size:30px; } .lib-char-name { font-size:18px; } - .lib-chars-tbl { min-width: 1340px; } + .lib-chars-tbl { min-width: 2410px; } } /* ── Character detail modal (D&D / LARP card) ───────────────────────────────── */ @@ -1066,8 +1078,17 @@ tr.lib-char-card.lib-chars-tbl-row:hover { transform: none; box-shadow: none; ba } .lcd-conceptart-empty .mdi { font-size: 30px; } .lcd-conceptart-empty.is-noprompt { opacity: .6; } -.conceptart-lightbox-box { width: min(900px, 96vw); max-height: 94vh; overflow-y: auto; } +.lcd-conceptart-empty { cursor:pointer; } +.lcd-conceptart-empty:hover { border-color:var(--accent); } +.conceptart-lightbox-box { width: min(980px, 96vw); max-height: 94vh; overflow-y: auto; } .conceptart-lightbox-img { width: 100%; height: auto; border-radius: 10px; display: block; } +.conceptart-editor-preview { display:flex; justify-content:center; min-height:120px; margin-top:18px; } +.conceptart-editor-preview .conceptart-lightbox-img { max-width:min(900px, 90vw); max-height:62vh; object-fit:contain; width:auto; } +.conceptart-editor-empty { width:min(700px, 88vw); min-height:240px; border:2px dashed var(--border); border-radius:10px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; color:var(--subtext); } +.conceptart-editor-empty .mdi { font-size:38px; } +.conceptart-quick-actions { display:flex; justify-content:center; flex-wrap:wrap; gap:10px; margin:18px 0; } +.conceptart-editor-tools { display:flex; flex-direction:column; gap:9px; padding:0 10px; } +.conceptart-editor-tools textarea { width:100%; resize:vertical; padding:10px; font:inherit; min-height:120px; } .lcd-header-btns { display:flex; gap:8px; flex-shrink:0; align-self:flex-start; } .lcd-header-btns button { background:rgba(255,255,255,.18); border:1px solid rgba(255,255,255,.32); color:#fff; @@ -1200,7 +1221,7 @@ tr.lib-char-card.lib-chars-tbl-row:hover { transform: none; box-shadow: none; ba background:var(--surface); display:flex; align-items:center; gap:12px; flex-wrap:wrap; } .lcd-voice-top .lcd-section-label { margin-bottom:0; flex-shrink:0; } -.lcd-voice-top .lcd-voice-label { font-size:14px; flex:1; min-width:120px; } +.lcd-voice-top .lcd-voice-label { font-size:14px; flex:0 1 auto; min-width:0; } .lcd-voice-top button { font-size:13px; padding:6px 16px; border-radius:7px; cursor:pointer; border:1px solid var(--border); background:var(--panel); color:var(--text); @@ -1210,6 +1231,17 @@ tr.lib-char-card.lib-chars-tbl-row:hover { transform: none; box-shadow: none; ba .lcd-voice-top .lcd-auto-voice:hover { border-color:var(--green,#22c55e); color:var(--green,#22c55e); } .lcd-voice-top .lcd-online-voice:hover { border-color:#0ea5e9; color:#0ea5e9; } .lcd-voice-top .lcd-gen-voice:hover { border-color:#a855f7; color:#a855f7; } +.lcd-voice-top .lcd-preview-voice { display:inline-flex; align-items:center; justify-content:center; width:30px; height:30px; padding:0; border-radius:50%; } +.lcd-voice-top .lcd-preview-voice { background:#2563eb; border-color:#2563eb; color:#fff; } +.lcd-voice-top .lcd-preview-voice:hover { background:#1d4ed8; border-color:#1d4ed8; color:#fff; } +.lcd-voice-top .lcd-preview-voice.playing { background:#1d4ed8; border-color:#1d4ed8; color:#fff; } +.lcd-voice-top .lcd-preview-voice.loading { opacity:.65; cursor:wait; } +.lcd-voice-top .lcd-design-voice-icon { + display:inline-flex; align-items:center; justify-content:center; + width:30px; height:30px; padding:0; border-radius:50%; + color:#7c3aed; +} +.lcd-voice-top .lcd-design-voice-icon:hover { background:#f3e8ff; border-color:#a855f7; color:#7c3aed; } /* Sources section */ .lcd-sources { padding:20px 28px; background:var(--panel); border-top:1px solid var(--border); @@ -6450,6 +6482,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami /* Audiobook casting overlay */ .audiobook-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; z-index: 1000; } .audiobook-box { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 22px 24px; width: min(420px, 90vw); box-shadow: 0 12px 40px rgba(0,0,0,.25); } +.audiobook-box.conceptart-lightbox-box { width: min(1120px, calc(100vw - 40px)); max-width: calc(100vw - 40px); } .audiobook-title { font-weight: 800; font-size: 15px; display: flex; align-items: center; gap: 8px; } .audiobook-title .mdi { color: var(--accent); font-size: 20px; } .audiobook-msg { color: var(--subtext); font-size: 13px; margin: 10px 0 12px; } @@ -6457,17 +6490,20 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .audiobook-box label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; font-weight: 600; color: var(--subtext); } .audiobook-box label input[type="text"] { font: inherit; font-weight: 400; color: var(--text); padding: 7px 9px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); } .audiobook-actions { margin-top: 14px; text-align: right; display: flex; gap: 8px; justify-content: flex-end; } +.conceptart-editor-box .conceptart-editor-preview { width: 100%; } +.conceptart-editor-box .conceptart-editor-preview .conceptart-lightbox-img { max-width: 100%; width: auto; } /* Avatar lightbox — click any character avatar to open */ .avatar-lightbox-box { width: min(1180px, 96vw); max-height: 92vh; overflow-y: auto; } -.alb-body { display: flex; gap: 32px; margin-top: 20px; flex-wrap: wrap; } -.alb-preview { flex: 0 0 440px; } -.alb-preview img { width: 440px; height: 440px; object-fit: cover; border-radius: 14px; border: 1px solid var(--border); } +.alb-body { display: flex; flex-direction: column; gap: 20px; margin-top: 20px; align-items: center; } +.alb-preview { width: min(560px, 86vw); } +.alb-preview img { width: min(560px, 86vw); height: min(560px, 86vw); min-width: 500px; min-height: 500px; object-fit: cover; border-radius: 14px; border: 1px solid var(--border); } .alb-preview-empty { - width: 440px; height: 440px; border-radius: 14px; border: 1px dashed var(--border); + width: min(560px, 86vw); height: min(560px, 86vw); min-width: 500px; min-height: 500px; border-radius: 14px; border: 1px dashed var(--border); display: flex; align-items: center; justify-content: center; font-size: 120px; color: var(--subtext); opacity: .5; } -.alb-actions { flex: 1; min-width: 340px; display: flex; flex-direction: column; gap: 22px; } +.alb-actions { width: min(760px, 92vw); display: flex; flex-direction: column; gap: 22px; } +.alb-quick-actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; } .alb-section-label { font-size: 14px; font-weight: 700; color: var(--subtext); display: flex; align-items: center; gap: 6px; margin-bottom: 8px; } .alb-section input[type="file"] { font-size: 13.5px; width: 100%; } .alb-url-row { display: flex; gap: 8px; } @@ -6477,11 +6513,15 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .alb-gen-row select { flex: 1; min-width: 0; font-size: 13.5px; padding: 8px 10px; } .alb-gen-row .btn-primary { font-size: 13.5px; padding: 9px 16px; } @media (max-width: 820px) { - .alb-preview, .alb-preview img, .alb-preview-empty { flex-basis: auto; width: min(440px, 80vw); height: min(440px, 80vw); } + .alb-preview, .alb-preview img, .alb-preview-empty { min-width: 0; width: min(500px, 86vw); height: min(500px, 86vw); } .alb-actions { min-width: 0; width: 100%; } } #alb-status { margin-top: 10px; } .ab-recast-select-list { max-height: 320px; overflow-y: auto; margin: 10px 0; text-align: left; border: 1px solid var(--border); border-radius: 6px; padding: 4px; } +.audiobook-box.ab-recast-dialog { width: min(760px, calc(100vw - 40px)); max-width: calc(100vw - 40px); max-height: 90vh; } +.ab-recast-select-list label.ab-char-item { display:flex; flex-direction:row; align-items:center; width:100%; box-sizing:border-box; } +.ab-recast-select-list .ab-char-name { min-width:0; flex:1; text-align:left; } +.ab-recast-select-list .ab-char-count { margin-left:auto; flex:0 0 auto; } .ab-recast-select-list .ab-char-item + .ab-char-item { margin-top: 2px; } .ab-recast-select-list .ab-recast-cb { flex-shrink: 0; width: 16px; height: 16px; cursor: pointer; } .ab-recast-select-list .ab-cv-empty { padding: 16px; text-align: center; color: var(--subtext); font-size: 13px; }