diff --git a/CHANGELOG.md b/CHANGELOG.md index b95facf..dc853d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi and avoids unnecessary duplicate assignments when distinct catalogue voices are available. - **4 GiB Container Validation.** Verified the current image serves the application under a hard 4 GiB cgroup limit with no OOM kill; the probe recorded approximately 50.6 MiB idle memory and 0.09% CPU. +- **Profile Completeness Coverage.** Added browser regression coverage for empty, partial, and fully populated + character-sheet counts used by the library table and profile sorting. - **STT Cancellation Safety.** Queued transcription cancels before consuming the single STT slot, and timed-out requests keep the device slot held until the underlying worker actually exits. - **Credential Boundary Review.** Provider credentials are now documented as server-only; only the explicitly diff --git a/TODO.md b/TODO.md index 0fdf24c..ab9b435 100644 --- a/TODO.md +++ b/TODO.md @@ -167,7 +167,7 @@ the first block before investing in architecture or polish. - [ ] Character parsing and phantom-speaker prevention. - [x] Voice-matching constraints reject invented voice IDs and avoid unnecessary duplicate assignments. -- [ ] Profile-completeness counting. +- [x] Profile-completeness counting for empty, partial, and fully populated sheets. - [x] Rating persistence. - [x] STT endpoint normalization and backend-response shape handling. - [x] ComfyUI workflow submission, prompt injection, polling, and image retrieval. diff --git a/VERSION b/VERSION index 27ddcc1..ca8ec41 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.23.4 +1.23.5 diff --git a/static/dist/main.min.js b/static/dist/main.min.js index d12559f..152ff9b 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -1838,4 +1838,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},()=>'
No books yet.
Open Read Aloud, import a PDF or text, and save it to your library.
No theater plays yet.
Open Script Rehearsal \u2192 Import / Export to add a script, or cast a book as an audiobook.
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.
| '+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("profile","Profil (gef\xFCllt)","Sort by filled profile fields; the second number is always 32")+th("voice","Stimme")+th("rating","Rating","Rate the assigned voice")+' | Voice Design Prompt | Notes | Tags | Book / Script |
|---|
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.
"'+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?
Edit the description, then generate a new voice from it. This replaces '+(rec.voice?"the current voice":"this character\u2019s voice")+'.
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.
No books yet.
Open Read Aloud, import a PDF or text, and save it to your library.
No theater plays yet.
Open Script Rehearsal \u2192 Import / Export to add a script, or cast a book as an audiobook.
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.
| '+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("profile","Profil (gef\xFCllt)","Sort by filled profile fields; the second number is always 32")+th("voice","Stimme")+th("rating","Rating","Rate the assigned voice")+' | Voice Design Prompt | Notes | Tags | Book / Script |
|---|
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.
"'+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?
Edit the description, then generate a new voice from it. This replaces '+(rec.voice?"the current voice":"this character\u2019s voice")+'.
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.