chore: release 1.21.1 - Testing and scratch scripts
This commit is contained in:
parent
fee1b5db74
commit
f04c8d5e2b
91
2026-07-16 inventory.md
Normal file
91
2026-07-16 inventory.md
Normal file
@ -0,0 +1,91 @@
|
||||
Script Rehearsal — Feature Inventory
|
||||
1. Library phase (phase 1)
|
||||
Files: s-rehearser.html #reh-phase-1, JS renderLibraryList(), rehDbGetAll/rehDbAdd/rehDbPut, loadRecord() (exposed globally as window.rehLoadRecord), bookCover(), rehUniqueLibraryRecords(), rehTitleKey().
|
||||
Shows saved rehearsals as a "bookshelf" (#reh-library-list.reh-bookshelf, list/shelf toggle #reh-lib-view-toggle), each with a generated cover (bookCover), dedup'd by title (rehUniqueLibraryRecords).
|
||||
Script entry points: paste text into #reh-script-text + #reh-script-title → #reh-parse-btn → parseScript(); file upload (#reh-file-input txt/md, #reh-pdf-input pdf via importPDFScript, #reh-fdx-input fdx/osf via importFDX); drag-and-drop onto #reh-dropzone; .reh session import (#reh-lib-import-file, rehImportAnyFile); IMSDb/Scripts.com browser modal (#reh-imsdb-modal) fetching scripts from the web; and opening a saved record from the Library section or Audiobook (loadRecord/window.rehLoadRecord, called by library.js and audiobook.js).
|
||||
rehState shape (defined ~line 64):
|
||||
|
||||
lines:[], cast:{}, lineIndex:0, clips:[], voices:[], backend:'',
|
||||
playing:false, repeat:false, savedId:null, synthCache:Map, staleLines:Set,
|
||||
synthCancelled:false, synthRunning:false, skipDescriptions:true, narratorVoice:'',
|
||||
practiceStart:null, practiceEnd:null, bulkMode:false, bulkSel:Set, bulkAnchor:null,
|
||||
showHidden:false, plus mic-recording fields (recStream, recAudioCtx, mediaRec, recChunks, ...)
|
||||
2. Cast phase (phase 2)
|
||||
Per-character cards (renderCastList() → #reh-cast-list, card/list view applyCastView): library-linked characters render via _charCardHtml() reused from the shared character library, wrapped in .reh-cast-card-libcard; unmatched speakers (including a synthetic Narrator entry, key REH_NARRATOR_KEY) get a fallback header/card.
|
||||
"I play this" mic toggle: .reh-me-check checkbox per card, sets rehState.cast[sp].voice = 'me' (handled via _castApplyToLines/change listener ~line 1518); when voice==='me' the character is recorded live instead of TTS-synthesized.
|
||||
Narrator handling: _ensureNarrator() keeps rehState.cast[REH_NARRATOR_KEY] bidirectionally synced with rehState.narratorVoice (a dedicated cast row, not a separate UI section anymore).
|
||||
Voice-matching tools: #reh-matchlib-btn ("Match local" — LLM picks best existing library voice, no TTS engine needed), #reh-matchonline-btn ("Match online" — searches fish.audio, imports voices), #reh-autodesign-btn ("Design all" — generates new voices via Voice Design engine); all driven by rehResearchCast()/rehWriteCharacterNote()/_pickVoiceAvatar() and progress bar #reh-autodesign-progress.
|
||||
Bulk/filter tools: search/filter/sort toolbar (#reh-cast-search, gender/lang filters, #reh-cast-sort, #reh-cast-sort-dir), _castSortFilter(); per-card actions include voice audition (_rehAudition, _rehPreviewLocal, _rehPreviewCastLine), assign local voice (_rehAssignLocal), online search results (_rehSearchOnline, _rehUseSearchResult, _rehImportFishCandidate), "develop" a character bio (_castDevelop), and delete (_castDeleteCharacter).
|
||||
Buttons: Back (#reh-back-1-btn), Save to library (#reh-cast-save-btn), Open rehearsal (#reh-start-btn → showPhase(3)).
|
||||
3. Stage phase (phase 3)
|
||||
Rendering: buildScriptPage() builds #reh-script-lines from rehState.lines; each line has type ∈ act | scene | action | pagebreak | transition | direction | dialog (confirmed switch in buildScriptPage), each rendered with its own block class (.reh-act, .reh-scene, .reh-action-block, .reh-block-pagebreak, .reh-transition, .reh-direction, .reh-block for dialog).
|
||||
Per-dialog-line controls: play/pause avatar button (_rehCharAvatarHtml, .reh-line-play-avatar), speaker name/color, TTS-vs-Me badge, emotion picker (openEmoPicker/selectEmotion/closeEmoPicker), inline text edit pencil (startInlineEdit, .reh-edit-btn), per-line note (.reh-note-btn/.reh-note-ta), synth-status dot and re-synth button (_markSynthDot, _showReSynthBtn).
|
||||
Practice range markers: gutter button sets practiceStart/practiceEnd (updatePracticeRange, clearPracticeRange, setPracticeRangeFromSelection), shown in transport bar (#reh-practice-info).
|
||||
Bulk edit mode: setBulkMode(), .reh-bulk-check per line, toolbar #reh-bulk-bar (select all/clear/set range/ignore/unignore/hide/delete/show-hidden/done) backed by _toggleBulkSel, _bulkApply, _bulkDelete, _reindexLineState.
|
||||
Page-mode / A4 rendering: applyPageMode(), cyclePageMode(), paginateScript(), #reh-page-mode-btn (scroll / A4 pages / PDF-style pages), font size controls (rehApplyStageFont, #reh-font-inc/dec).
|
||||
Character sidebar strip: renderCastStrip() → #reh-cast-strip, collapsible (rehToggleCast, #reh-cast-toggle), with its own search/sort (#reh-cast-side-search, #reh-cast-side-sort).
|
||||
Top toolbar (director's console, #reh-transport-bar): transport (prev/play/stop/next/loop), progress label; action buttons — Synth all (#reh-tb-synth-all → synthAll()), Audiobook (#reh-tb-audiobook → audiobookExport() in audiobook.js), Character sheets (#reh-charsheets-btn → csForRehearser() in character-sheets.js), Re-synth stale (#reh-tb-resynth-stale), Skip-desc toggle, Bulk-edit toggle (#reh-bulk-toggle), Edit script/title (#reh-edit-script-btn → openScriptEditorModal), Save (#reh-tb-save), Export .reh/.fountain buttons, font size, page-mode toggle, Train mode (#reh-train-open-btn), Exit.
|
||||
Also on stage: TTS now-playing bar (#reh-tts-status-bar), "my turn" mic recording overlay (#reh-rec-overlay, startRehMic/rehRenderMeter), and the Train Mode panel (#reh-train-panel, see §7).
|
||||
4. Summary phase (phase 4)
|
||||
renderSummary() → #reh-summary-list: lists all recorded/synthesized clips from the session.
|
||||
Actions: Save to library, export .reh/.fountain/.fdx/.osf, Resume (back to stage), New script (reset to Library).
|
||||
5. Audio pipeline
|
||||
Per-line synth: synthOneLine(idx) (single line, on-demand) and synthAll() (batch pre-synth all TTS-eligible lines), both call fetchTtsPreviewBlob(voice, text, 'wav', instruct, backend) and cache results in rehState.synthCache (Map keyed by line index); stale entries (tone changed after synth) tracked in rehState.staleLines and surfaced via a re-synth button.
|
||||
Playback: startPlay/pausePlay/stopPlay/playNextLine() drive line-by-line playback; preDecodeBlob/playPreDecoded/rehDecodedBuffers give zero-latency playback via pre-decoded PCM; computeWordTimings drives text highlighting.
|
||||
Narrator confirmed: dialog lines always use their cast voice; non-dialog lines (action/scene/direction/transition/act, "any line with text") are read by rehState.narratorVoice — but only if a narrator voice is actually set. If no narrator voice is set, skipDescriptions (default true) skips them entirely during playback and synth (playNextLine/synthAll both check narratorVoice before narrating). Narrator is modeled as a regular cast entry keyed by sentinel REH_NARRATOR_KEY, kept in sync with rehState.narratorVoice via _ensureNarrator().
|
||||
6. Persistence
|
||||
saveToLibrary() builds a record via rehCurrentRecord() (title, reconstructed script text via linesToScriptText(), cast, per-line emotions/notes/ignored/hidden maps, backend, narratorVoice, lineIndex, clips) and calls rehDbPut/rehDbAdd (update vs insert based on rehState.savedId); also writes voice choices back into the shared character roster via castWriteBack().
|
||||
rehDbGetAll/rehDbGetById/rehDbAdd/rehDbPut/rehDbDelete wrap REST calls to /api/rehearsals (routes/rehearsals_db.py), backed by SQLite table rehearsals in core/database.py (columns: id, title, script, cast, emotions, notes, ignored, hidden_lines, backend, narrator_voice, line_index, created, updated — cast/emotions/notes/ignored/hidden stored as JSON TEXT). _rehSanitize/clipsToJson/clipsFromJson handle audio blob (de)serialization for clips.
|
||||
7. Other distinct features
|
||||
Train mode (trainState object, ~line 4233): a practice mode separate from Stage — buildTrainSeq() builds cue/my-line turn sequence, trainGoTo() navigates turns, plays the other actor's cue (_trainPlayCueLines), records the user's line, transcribes via _trainTranscribe (speech-to-text), and compares expected vs actual text (_trainCompare/_trainLcs/_trainNormWords) with a score shown in #reh-train-result. Entered/exited via enterTrainMode()/exitTrainMode().
|
||||
IMSDb/Scripts.com browser: modal #reh-imsdb-modal lets users browse ~1300 movie scripts or paste a URL, fetched server-side and parsed like any pasted script.
|
||||
Import/Export panel (#reh-impex-panel): supports import from .txt/.md/.pdf/.fountain/.fdx/.osf/.reh and URL; export to .reh (full session incl. audio clips), .fountain, .fdx (Final Draft XML), .txt, .md, .osf (Open Screenplay Format) via exportFountain/exportFDX/exportOSF/exportAsTxt/exportAsMd/exportToFile.
|
||||
Character sheets integration (character-sheets.js's csForRehearser) generates RPG-style actor sheets from the script's cast.
|
||||
Audiobook export integration (audiobook.js's audiobookExport) synthesizes the whole script and packages it as chapterized MP3s; audiobook.js can also open a rehearsal directly via window.rehLoadRecord.
|
||||
Now I have a complete picture. Here is the feature inventory.
|
||||
|
||||
Read Aloud — Feature Inventory
|
||||
1. Source import
|
||||
Paste text: #reader-paste-text textarea + #reader-paste-load button → readerImportPastedText() → readerLoadText(text). Sets readerState.docText, readerState.mode = 'text'.
|
||||
File import (drag/drop or #reader-file-input): readerImportFile(file). .txt/.md → readerLoadText; .pdf → two-stage: readerLoadPdfSkeleton(file) (fast raster via pdf.js, no text layer yet, shows #reader-extract-banner) then explicit readerRunExtraction() → readerExtractPdfText(loaded) (runs getTextContent(), optional heading OCR via #reader-ocr-headings → readerOcrPageHeading/Tesseract.js, builds sentences).
|
||||
Sentence/paragraph building: readerBuildSentences(words), readerMarkParagraphBreaks(pageWords) (sets .para/.paraStart), readerGroupUnits(base, mode) groups into synthesis chunks per chunkMode (sentence/paragraph/page, UI: #reader-chunk-mode).
|
||||
Key readerState fields: mode ('pdf'|'text'), pdfDoc, pages[], sentences[] (synth units), baseSentences[] (raw), docText, fileBlob, chunkMode, _seq (import generation guard), savedId/savedAudioIdx/sourceUploaded (library linkage).
|
||||
Document viewport: #reader-doc, zoom (readerApplyZoom, #reader-zoom-*), lazy raster (readerSetupLazyRaster, IntersectionObserver, readerEvictCanvases), in-doc search (readerSearchApply, #reader-search*).
|
||||
Library persistence (separate from casting): readerSaveLibrary(), readerOpenLibraryDoc(id), READER_API = '/api/reader/docs' — stores meta/source/per-unit audio server-side; resume position via readerSaveResume/readerLoadResume (localStorage reader-resume).
|
||||
2. Casting audiobook (speaker attribution)
|
||||
Entry point: #reader-audiobook-btn → audiobookOpenCastView() (static/js/audiobook.js), which restores an in-memory/localStorage/server draft or starts fresh via audiobookCastView(...) (the live UI, #reader-audiobook-panel).
|
||||
Scope text: audiobookScopeText() reads readerScopeIndices()/readerState.sentences (selection, PDF page-range, or whole doc) and builds page-mark offsets (_audiobook.pageMarks).
|
||||
Pass 1 — initial cast: audiobookCast(overrideUrl, overrideModel, resume). Splits scope text into ~3000-char chunks (splitTextIntoChunks), for each chunk calls attributeChunk() → audiobookAttributeStream() (SSE /api/attribute-dialogue/stream, shows live LLM "thinking") falling back to blocking POST /api/attribute-dialogue; on failure retries by splitting the chunk in half; falls back to audiobookSplitByQuotes() (regex-only) if LLM unusable. Runs deterministic audiobookResolveUnknowns() per chunk (colon-rule / inquit heuristics), builds roster[]. Autosaves via _abSaveDraft every chunk / _abStartDraftAutosave.
|
||||
Cleanup pass (end of audiobookCast, not LLM-based): _audiobookFixOrphanedQuoteMarks() → _audiobookMergeAdjacentSameSpeaker() (also _audiobookDedupNearbyDuplicates exists) — merges consecutive same-speaker segments, fixes stray quote boundaries.
|
||||
Pass 2 — "Verify"/recast unknowns: audiobookRecastUnknown(overrideUrl, overrideModel, options) — re-resolves mechanical cases, then re-sends only Unknown-speaker (and optionally, via options.includeNarrator, all Narrator) lines in small context groups (audiobookRecastGroups/audiobookRecastContext) to the LLM, canonicalizing returned names against saved character-library aliases (clGetAllByTagOrBook, clSplitIdentityTokens).
|
||||
State: _audiobook = { running, cancel, segments, roster, lastText, pageMarks, rehId, completedChunks, completedTotal, bookId } (global, window._audiobook).
|
||||
Persists cast to Rehearsal library via audiobookSaveAsRehearsal({silent:true}) and hands off to Script Rehearser: audiobookShowPreview() → audiobookOpenCurrentInRehearser() / (legacy path) audiobookBuildScript(segments) + audiobookOpenInRehearser(script, title, dialogueEmotions) (fills #reh-script-text, calls navTo('s-rehearser'), clicks #reh-parse-btn).
|
||||
3. Cast Characters (character sheet generation) — character-sheets.js
|
||||
Distinct from dialogue casting: generates rich profiles, not speaker attribution. Entry csForReader(opts) (button "Cast from Reader" in #reader-charsheets-panel's empty state) — builds csReaderText() from readerScopeIndices(), seeds from existing library records (csSeedSheet, clGetAllByTagOrBook), attaches dialogue-line counts from _audiobook.segments (csAttachLineCounts), then calls csGenerate(text, cacheKey, knownRoster, {pageHost, seedSheets, lineCounts}) which streams passage-by-passage extraction (csGenerateStream) and merges results (csMerge, csConsolidateAliasDuplicates).
|
||||
opts.fresh triggers a full recast discarding existing sheets.
|
||||
csForReaderSelective(selectedNames) regenerates only picked characters (targeted evidence windows via csEvidenceWindowText/csParagraphBlocks).
|
||||
Also csForRehearser() — same mechanism sourced from the Rehearser script instead (not in scope here).
|
||||
Persists via csSaveToLibrary(book, sheets) → clUpsertMany (character library, shared with casting/Rehearser).
|
||||
Renders in #reader-charsheets-panel via csShow(sheets, title, sourceText, book, hostEl); each sheet card built by csCardHtml; auto-generates external prompts (SillyTavern card / image-gen turnaround) via csAutoGenerateExternalPrompts as a background pass.
|
||||
4. Cast / roster review UI (inside audiobookCastView, audiobook.js ~1313-3100)
|
||||
Live feed (.ab-cv-feed inside #reader-audiobook-panel) shows attributed narration/dialogue rows as they stream in, with page-break dividers (view.pagemark).
|
||||
Sidebar roster: renderRoster() — list of characters with line counts, colors, search filter, sort (count/alpha), persisted sort in localStorage ttsvc_ab_side_sort.
|
||||
Per-row: click a speaker name to open a reassignment popup; _abOpenAliasPopup(name, anchorEl) lets you add an alias (writes via clUpsert(book, {name, aliases})) or merge two roster entries — _abMergeCharacters(fromName, intoName) relabels every matching segment, then _abPatchScatteredRows/_abRedrawSegmentsChunked repaints only affected rows.
|
||||
_abOpenRecastSelectPopup(existingChars) / audiobookRecastSelectedCharacters(selectedNames) — pick specific characters to re-verify.
|
||||
Export/handoff buttons inside the panel: recast unknowns ("Verify"), open/continue casting, export cast as Markdown (audiobookExportCastMd() → _abCastMarkdown(data)), and "Review & cast" → audiobookShowPreview().
|
||||
5. TTS playback in Read Aloud itself (separate from audiobook generation)
|
||||
Yes — Read Aloud has its own single-voice sentence-by-sentence playback with word highlighting, independent of the multi-speaker audiobook pipeline:
|
||||
|
||||
Transport bar #reader-transport (#reader-play, #reader-stop, #reader-prev/#reader-next, #reader-progress-fill).
|
||||
readerPlay()/readerPause()/readerPlayCurrent() — fetches/caches per-unit audio (readerGetBuffer, fetchTtsPreviewBlob), computes word timings via computeWordTimings (reused from rehearser.js), and highlights the active word (readerHighlightWord) plus auto-scroll (readerEnsureVisible).
|
||||
Uses readerCtx() (Web Audio API), readerComputeGain for RMS loudness normalization (#reader-normalize), readerState.speed (#reader-speed slider) for playback-rate control, readerGenParams() for seed/temperature/native-speed.
|
||||
Batch pre-synthesis ("Synthesise ahead"): readerSynthAll()/readerSynthIndices(targets), scoped via readerScopeIndices() (selection / page range / all), shown in #reader-synthbar / #reader-synth-prog.
|
||||
Per-unit status coloring (pending/synthesising/ready/reading) via readerSetStatus/readerPaintStatus, legend .reader-legend.
|
||||
6. Other distinct features
|
||||
Range selection: click-to-select start/end sentence (readerSetSelecting, readerPickSentence, #reader-select-toggle), used to scope both synthesis and casting.
|
||||
MP3 export: readerExport(mode) — #reader-export-mode (page/sentence), #reader-export-btn; synthesizes any missing scoped units first and blocks export if any fail.
|
||||
Document search: readerSearchApply/readerJumpToSearchHit (#reader-search*), independent of casting.
|
||||
Voice-consistency ("chunk mode") re-grouping: readerRechunk(mode) — clears cached audio when granularity changes.
|
||||
Backend/voice pickers + tuning: #reader-backend-select, #reader-voice-select (VoicePicker), #reader-instruct, seed/temperature/native-speed inputs, readerUpdateBackendHint() (warns about voice drift on non-style-aware backends).
|
||||
Reader library (#reader-library-card/#reader-lib-list, readerRenderLibrary) — separate saved-books browser with cover-image drag/drop upload, delete-with-confirm overlay; distinct from the Character Library and Rehearsal Library.
|
||||
View switching within the section: window.showReaderView(view) toggles reader-main-view / reader-audiobook-panel ('cast') / reader-charsheets-panel ('chars') / reader-library-card ('library'), driving refreshWorkflowCrumbs. Also window.readerJumpToPage(pageNum) for cross-linking from other sections (e.g. jump to a PDF page from a character sheet's evidence citation).
|
||||
@ -4,7 +4,14 @@ All notable changes to **TTS Voice Creator — Clone and Design** are documented
|
||||
Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned with [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## [1.21.1] — 2026-08-23
|
||||
|
||||
### Added
|
||||
- **Image Generation Scripts.** Added scratch scripts for testing image generation providers (Flux, Gemini, Google Imagen, OpenAI, Pollinations).
|
||||
- **Character Tooling.** Added scratch scripts for character detail fetching and batch generation.
|
||||
- **Inventory Markdown.** Added 2026-07-16 inventory documentation.
|
||||
|
||||
## [1.21.0] — 2026-08-23
|
||||
|
||||
### Added
|
||||
|
||||
67
scratch/batch_generate_all.py
Normal file
67
scratch/batch_generate_all.py
Normal file
@ -0,0 +1,67 @@
|
||||
import requests
|
||||
import json
|
||||
import urllib.parse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
BASE_URL = "http://localhost:7890"
|
||||
|
||||
def process_char(char):
|
||||
name = char.get("name")
|
||||
if name in ["Marcian", "Nyrilla", "Arthag", "Lysandra", "Darrag", "Himgi"]:
|
||||
return # Skip ones we already fixed perfectly
|
||||
|
||||
char_id = urllib.parse.quote(char["id"])
|
||||
sheet = char.get("sheet") or {}
|
||||
if isinstance(sheet, str):
|
||||
try:
|
||||
sheet = json.loads(sheet)
|
||||
except:
|
||||
sheet = {}
|
||||
|
||||
phys = sheet.get('physical', '')
|
||||
cloth = sheet.get('clothing', '')
|
||||
|
||||
# Portrait
|
||||
portrait_prompt = f"Task: Generate a game NPC portrait. Subject: {name}. Physical traits: {phys}. Clothing: {cloth}. Context: dialogue portrait for a stylized exploration game set in an original world. Style: painterly game concept art with clear shape language, grounded materials, and slightly exaggerated but believable proportions. Composition: a simple square headshot/bust on a plain neutral muted parchment background, facing forward with a subtle expression. Lighting: neutral studio key with soft rim separation and no cinematic color."
|
||||
|
||||
# Concept
|
||||
concept_prompt = f"Task: Generate a game NPC design sheet. Subject: {name}. Physical traits: {phys}. Clothing: {cloth}. Context: handoff reference for a stylized exploration game set in an original world. Style: painterly game concept art with clear shape language, grounded materials, and slightly exaggerated but believable proportions. Composition: full-body front, side, and back views across the top; below them, one dialogue pose and a single clean callout row containing exactly 3 items on a muted parchment landscape board. Lighting: neutral studio key with soft rim separation and no cinematic color."
|
||||
|
||||
print(f"[{name}] Starting generation...")
|
||||
# Generate Portrait
|
||||
try:
|
||||
resp_img = requests.post(f"{BASE_URL}/api/character-generate-image", json={"prompt": portrait_prompt})
|
||||
if resp_img.ok:
|
||||
char["image"] = resp_img.json().get("image")
|
||||
except Exception as e:
|
||||
print(f"Error portrait {name}: {e}")
|
||||
|
||||
# Generate Concept
|
||||
try:
|
||||
resp_concept = requests.post(f"{BASE_URL}/api/character-generate-image", json={"prompt": concept_prompt})
|
||||
if resp_concept.ok:
|
||||
sheet["concept_art_image"] = resp_concept.json().get("image")
|
||||
except Exception as e:
|
||||
print(f"Error concept {name}: {e}")
|
||||
|
||||
sheet["image_prompt"] = portrait_prompt
|
||||
sheet["concept_art_prompt"] = concept_prompt
|
||||
char["sheet"] = sheet
|
||||
|
||||
resp_put = requests.put(f"{BASE_URL}/api/characters/{char_id}", json=char)
|
||||
print(f"[{name}] Finished. Status: {resp_put.status_code}")
|
||||
|
||||
def main():
|
||||
resp = requests.get(f"{BASE_URL}/api/characters")
|
||||
characters = resp.json().get("characters", [])
|
||||
|
||||
to_process = [c for c in characters if c.get("name") not in ["Marcian", "Nyrilla", "Arthag", "Lysandra", "Darrag", "Himgi"]]
|
||||
|
||||
print(f"Processing {len(to_process)} characters...")
|
||||
|
||||
# Use 10 workers for speed! Pollinations can handle it.
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
executor.map(process_char, to_process)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
56
scratch/fix_avatars.py
Normal file
56
scratch/fix_avatars.py
Normal file
@ -0,0 +1,56 @@
|
||||
import requests
|
||||
import json
|
||||
import urllib.parse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
BASE_URL = "http://localhost:7890"
|
||||
|
||||
def process_avatar(char):
|
||||
name = char.get("name")
|
||||
if name in ["Marcian", "Nyrilla", "Arthag", "Lysandra", "Darrag", "Himgi"]:
|
||||
return # Skip ones we already fixed perfectly
|
||||
|
||||
char_id = urllib.parse.quote(char["id"])
|
||||
sheet = char.get("sheet") or {}
|
||||
if isinstance(sheet, str):
|
||||
try:
|
||||
sheet = json.loads(sheet)
|
||||
except:
|
||||
sheet = {}
|
||||
|
||||
phys = sheet.get('physical', '')
|
||||
cloth = sheet.get('clothing', '')
|
||||
|
||||
# Strict Portrait Prompt to defeat Flux's tendency to draw turnaround sheets
|
||||
portrait_prompt = f"A beautiful tight headshot portrait of {name}. Physical traits: {phys}. Clothing: {cloth}. Style: high quality painterly fantasy illustration, single face facing forward. Plain neutral parchment background. ONLY ONE PERSON, close-up on face and shoulders, NO MULTIPLE VIEWS, NO concept art sheet."
|
||||
|
||||
print(f"[{name}] Starting avatar fix...")
|
||||
# Generate Portrait
|
||||
try:
|
||||
resp_img = requests.post(f"{BASE_URL}/api/character-generate-image", json={"prompt": portrait_prompt})
|
||||
if resp_img.ok:
|
||||
char["image"] = resp_img.json().get("image")
|
||||
sheet["image_prompt"] = portrait_prompt
|
||||
char["sheet"] = sheet
|
||||
|
||||
resp_put = requests.put(f"{BASE_URL}/api/characters/{char_id}", json=char)
|
||||
print(f"[{name}] Finished. Status: {resp_put.status_code}")
|
||||
else:
|
||||
print(f"[{name}] Failed to generate: {resp_img.text}")
|
||||
except Exception as e:
|
||||
print(f"Error portrait {name}: {e}")
|
||||
|
||||
def main():
|
||||
resp = requests.get(f"{BASE_URL}/api/characters")
|
||||
characters = resp.json().get("characters", [])
|
||||
|
||||
to_process = [c for c in characters if c.get("name") not in ["Marcian", "Nyrilla", "Arthag", "Lysandra", "Darrag", "Himgi"]]
|
||||
|
||||
print(f"Processing {len(to_process)} avatars...")
|
||||
|
||||
# Use 1 worker to avoid rate limits
|
||||
with ThreadPoolExecutor(max_workers=1) as executor:
|
||||
executor.map(process_avatar, to_process)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
18
scratch/get_char_details.py
Normal file
18
scratch/get_char_details.py
Normal file
@ -0,0 +1,18 @@
|
||||
import sqlite3
|
||||
import json
|
||||
|
||||
db = sqlite3.connect("config/tts_creator.db")
|
||||
c = db.cursor()
|
||||
|
||||
names = ["Karyla", "Kolon", "Misira", "Uriens", "Karman", "Inquisitor", "Sklave", "Junge Ritterin", "Ork", "Gernot Brohm", "Kriegerin", "Magier", "Bäckerstochter", "Blautann und seine Männer", "Druide", "Krieger", "Kutscher", "Reiter", "Turmwächter", "Verwundeter Soldat", "Wache", "Yonsus", "alter Weber"]
|
||||
|
||||
prompts = []
|
||||
for name in names:
|
||||
c.execute("SELECT sheet FROM characters WHERE name=?", (name,))
|
||||
row = c.fetchone()
|
||||
if row:
|
||||
sheet = json.loads(row[0])
|
||||
phys = sheet.get("physical", "")
|
||||
cloth = sheet.get("clothing", "")
|
||||
prompt = f"A tight square headshot portrait of {name}. Physical traits: {phys}. Clothing: {cloth}. Style: high quality painterly fantasy illustration, single face facing forward. Plain neutral parchment background. ONLY ONE PERSON, close-up on face and shoulders."
|
||||
print(f"{name}|{prompt}")
|
||||
14
scratch/test_flux.py
Normal file
14
scratch/test_flux.py
Normal file
@ -0,0 +1,14 @@
|
||||
import requests
|
||||
import base64
|
||||
|
||||
prompt = "A simple square headshot portrait of Karyla, an adult female Amazon warrior. Plain neutral parchment background. Painterly fantasy illustration style, single face only, facing forward. Do not include multiple views, this is a dialogue portrait."
|
||||
|
||||
url = f"https://image.pollinations.ai/prompt/{requests.utils.quote(prompt)}?width=512&height=512&nologo=true"
|
||||
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
with open("test_flux_portrait.png", "wb") as f:
|
||||
f.write(response.content)
|
||||
print("Success, saved test_flux_portrait.png")
|
||||
else:
|
||||
print("Error:", response.status_code)
|
||||
26
scratch/test_gemini_imagen.py
Normal file
26
scratch/test_gemini_imagen.py
Normal file
@ -0,0 +1,26 @@
|
||||
import requests
|
||||
import json
|
||||
import base64
|
||||
|
||||
API_KEY = "AQ.Ab8RN6JRC8vPh-vLzItUVeS4vCofqzMf9TyF6Ce6BXw-Y18GuA"
|
||||
# Use Interactions API format if needed, but let's try the standard endpoint first.
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-image:generateContent?key={API_KEY}"
|
||||
|
||||
payload = {
|
||||
"contents": [{
|
||||
"parts": [{"text": "A simple square headshot portrait of Karyla. Style: painterly game concept art."}]
|
||||
}]
|
||||
}
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
print(response.status_code)
|
||||
if response.ok:
|
||||
data = response.json()
|
||||
try:
|
||||
# gemini-3.1-flash-image might return images in a different structure
|
||||
print(json.dumps(data)[:200])
|
||||
except Exception as e:
|
||||
print("Failed to parse response:", data)
|
||||
else:
|
||||
print(response.text[:500])
|
||||
20
scratch/test_google_imagen.py
Normal file
20
scratch/test_google_imagen.py
Normal file
@ -0,0 +1,20 @@
|
||||
import requests
|
||||
import json
|
||||
import base64
|
||||
|
||||
API_KEY = "AIzaSyD6aidNxIhr2SKgauZ9zea_FynuCo2DyDo"
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/interactions"
|
||||
|
||||
payload = {
|
||||
"model": "gemini-3.1-flash-image",
|
||||
"input": [{"type": "user_input", "content": [{"type": "text", "text": "A beautiful square headshot portrait of Karyla. Style: painterly game concept art."}]}],
|
||||
"response_modalities": ["image"]
|
||||
}
|
||||
headers = {'x-goog-api-key': API_KEY, 'Content-Type': 'application/json'}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
print(response.status_code)
|
||||
if response.ok:
|
||||
print("Success! Got image")
|
||||
else:
|
||||
print(response.text[:500])
|
||||
24
scratch/test_openai.py
Normal file
24
scratch/test_openai.py
Normal file
@ -0,0 +1,24 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
API_KEY = "sk-proj-jVzNl_bM-oRpR_W1F-W5bLwXQ4e38nS3pQf4eY6k2M6tX-TZA9YmH4lF" # From settings.json wait, I don't know the full key. I'll read it from settings.json
|
||||
with open("config/settings.json") as f:
|
||||
settings = json.load(f)
|
||||
api_key = settings.get("engine_api_keys", {}).get("openai_image")
|
||||
|
||||
url = "https://api.openai.com/v1/images/generations"
|
||||
|
||||
payload = {
|
||||
"model": "dall-e-3",
|
||||
"prompt": "A beautiful square headshot portrait of Karyla. Style: painterly game concept art.",
|
||||
"n": 1,
|
||||
"size": "1024x1024"
|
||||
}
|
||||
headers = {'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
print(response.status_code)
|
||||
if response.ok:
|
||||
print("Success! OpenAI generated image.")
|
||||
else:
|
||||
print(response.text[:500])
|
||||
13
scratch/test_pollinations_portrait.py
Normal file
13
scratch/test_pollinations_portrait.py
Normal file
@ -0,0 +1,13 @@
|
||||
import requests
|
||||
|
||||
prompt = "Task: Generate a game NPC portrait. Subject: an original adult female elven mage with long pointed ears, slightly glowing eyes, a mystical but weary aura, wearing simple practical traveler/prisoner garb with subtle elven motifs. Context: dialogue portrait for a stylized exploration game set in an original world. Style: painterly game concept art with clear shape language, grounded materials, and slightly exaggerated but believable proportions. Composition: a simple square headshot/bust on a plain neutral muted parchment background, facing forward with a subtle expression. Lighting: neutral studio key with soft rim separation and no cinematic color."
|
||||
|
||||
url = f"https://image.pollinations.ai/prompt/{requests.utils.quote(prompt)}?width=512&height=512&nologo=true"
|
||||
|
||||
response = requests.get(url)
|
||||
if response.status_code == 200:
|
||||
with open("test_pollinations_portrait.png", "wb") as f:
|
||||
f.write(response.content)
|
||||
print("Success")
|
||||
else:
|
||||
print("Error:", response.status_code)
|
||||
15
scratch/update_karyla.py
Normal file
15
scratch/update_karyla.py
Normal file
@ -0,0 +1,15 @@
|
||||
import sqlite3
|
||||
import base64
|
||||
|
||||
db = sqlite3.connect("config/tts_creator.db")
|
||||
c = db.cursor()
|
||||
|
||||
with open("/home/sparky/.gemini/antigravity/brain/a40bef3c-201f-47af-95f1-c3c67e7f8973/karyla_avatar_1786889767286.png", "rb") as f:
|
||||
img_data = f.read()
|
||||
|
||||
b64 = base64.b64encode(img_data).decode('utf-8')
|
||||
data_uri = f"data:image/png;base64,{b64}"
|
||||
|
||||
c.execute("UPDATE characters SET image=? WHERE name='Karyla'", (data_uri,))
|
||||
db.commit()
|
||||
print("Karyla updated in DB.")
|
||||
BIN
test_flux_portrait.png
Normal file
BIN
test_flux_portrait.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
test_pollinations_portrait.png
Normal file
BIN
test_pollinations_portrait.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Loading…
Reference in New Issue
Block a user