Character-card import specification
A SillyTavern character card (PNG with an embedded ccv3 or chara chunk, or its JSON; spec V2 and V3, V2 is normalised to V3 first) becomes a new world. The importer writes native world files; if the result cannot start a story yet, it remains visible as a repairable draft rather than losing the work.
Flow
- Upload (Worlds page, Import). The host reads the card (
host/import/card.ts, at mostimportMaxUploadBytes, 100 MB by default), keeps it in a temporary import folder and shows what it holds. - Choose what the importer is told: the world's name, which lorebook entries are switched off (they are not passed on), and which one opening is used (the first message or one alternate greeting).
- Start. A world is created, the upload is moved into its
.import/folder, its authoring chat is created and the plugin sends the message/import-st .import/card.json …for the person. The authoring agent recognises the command (agent/pre-step), is told the procedure instead of an ordinary request and may not end the turn before the import is done. - The three tools of the import, each doing its work in narrow agents of their own (
runChild, one small job per call, several at once up toimportConcurrency):import_survey: itemise every piece of the card (fields, the opening, lorebook entries, long ones cut at paragraph or sentence ends), have each piece classified (what it is: character, player, speech, place, faction, thing, world, scenario, instruction, generator, template, irrelevant, and which named things it is about), and merge the names into a roster of characters, places, factions and things (a model call, with a deterministic fallback that folds names contained in others).import_build: extract one document per entity from the pieces that are about it, and the background pieces as lore entries. Documents are checked with the same parsers the world uses, and refused with the reasons until they are valid.import_finish(then a review): stage everything, write the native files with the description, tags, opening message and cover picture, and check the whole world with the same validation a session start uses. The same turn then reviews the world like a careful editor with the authoring tools (places and their ways, starting places, duplicates) and corrects what the narrow jobs could not see without asking for each edit (reviewingin.import/state.jsonlifts the approval gate until that turn ends). If problems remain, the files stay as a draft and the assistant continues repairing them.
- The phases are kept in
.import/state.json(ready,surveyed,built,repair,done), so an interrupted import resumes where it was and each step refuses to run out of order.
The narrow jobs use the plugin-wide Imports and authoring assistants settings. They can use a separate model and reasoning level from the authoring chat, run with bounded concurrency, and stop after importTimeoutMinutes rather than holding an import indefinitely. importMaxTokens is a ceiling: classification, roster, entity, lore, style and translation jobs each request only the smaller budget their result needs. If a helper fills that budget with prose instead of calling its result tool, the run stops immediately; retrying the same truncated answer would spend the same budget again. import_build may run only twice in one authoring turn, which prevents a model from repeatedly rebuilding an import that is not converging.
Decisions
- Where an entry is in the card, or whether it is constant or keyed, says little about what it is: cards use lorebooks as world background, as reminders for one character, or as the whole definition. Every piece is therefore judged by its content, and the lorebook is never imported as it is.
- When
importDecisionsisdecision, the configured decision service first checks whether a piece is only an AI instruction or irrelevant reader-facing material. A result at or aboveimportConfidenceis accepted without a language-model classification. Uncertain answers and decision-service failures fall back to the language model, and the survey reports both how many pieces were settled and any fallback problem. The decision model is never trusted alone for a piece that may describe the story. - Tracked values (
extractVariables) are read in groups. Panels, formats and state specifications are split into sections of at most 6 000 characters, and each section is handled separately.- A request declares at most
importVariablesPerRequestvalues (default 25). While a request fills its share, it is asked again and told which keys are declared already. The first declaration of a key stands, and a world has no limit on how many values it tracks. - A declaration that cannot be used (a choice with one option, bands that do not rise) is left out with its reason. It does not cost the rest of the answer.
- A constrained reply that ran into the output limit in the middle of a long list is closed after its last whole entry, and the next round asks for the rest.
- Putting all of the text in one request made the model write on until its limit without answering (found live on a card with long variable rules), and every variable was lost.
- A helper request may write up to
importMaxTokens. Its default, 0, is a tenth of the model's context window (at least 16 000), and each job adds room for the analysis the model tends to write first.
- A request declares at most
- A character's starting possessions come back as
carries, using the names in the card (at most 12). A name that is not already a thing in the roster becomes an item held by that character throughheld_by. Things the character merely knows about remain inknows. - Casting passages become world mechanics (
host/import/casting.ts, promptimport/casting). The importer reads those passages together, then writes commands and triggers from them:commands.jsonholds the slash words the card names. A command that no trigger uses is not kept.triggers.jsongets entries whose effect is a beat that has the world author make the person (dispatch: author, aneed, and a drawn option from the card's list indraw).- A trigger starts from a command's flag, or from a statement about an event, written in the past tense, that a model judges (
decisions.triggerschooses who judges). - A mechanism with nothing to start it is left out, with a note.
- A model-free import uses
host/import/structure.ts,structureImportinrun-import.ts, andPOST /worlds/import/structure?id=. Code arranges the card without asking a model to read it:- The card's own character comes from its description, personality and example lines (
characterDocument). - Each lorebook piece becomes lore only the game master reads, in the card's own words (
asWritten). - The system prompt and the post-history instructions become
roleplay/style, with${and{{made inert. - The same
finishImportwrites the world. The description and tags come from the card's own fields (ctx.noModel). - The import ends as
done, or asrepairwhen the world would not start yet, andprovenance.jsonrecords it. There is no chat and no review.
- The card's own character comes from its description, personality and example lines (
- Authoring checks and import provenance are recorded alongside the files.
- Every authoring write (elements, relationships, plots, triggers, world info) returns, next to its result, what the whole world still needs before a story could start:
check_worldin the result isok, or up to eight open problems and how many more. It is advice, not a refusal, because a half-made world is a normal state in between. .import/provenance.json(schema: dsh-roleplay-import/v1) keeps one line per import or update: the time, the card's name, a SHA-256 of the card as it was read, the language and, for an import, the counts and the number of notes. A file with another schema is refused and never rewritten.
- Every authoring write (elements, relationships, plots, triggers, world info) returns, next to its result, what the whole world still needs before a story could start:
- Standing rules and casting guidance come from
extractEntitiesand theimport/generatorprompt. Each substantial piece gets one question with two result lists. Passages are copied word for word and checked against their source.- Casting is how people, places or things are made up in play. It becomes lore only the game master reads ("how people are made up").
- Rules are standing rules and mechanics the game master applies: checks, combat steps, tables, how state is tracked. Rules from a lorebook entry the card always sends (
constant) are joined into the world's promptroleplay/standing, which the game master carries on every turn (at most 12 000 characters, with${and{{made inert). Other rules, and any that do not fit, become lore only the game master reads ("rules and mechanics"). - Before classification, the classifier is shown an outline of the whole card and examples that tell close roles apart.
- A name that holds the player marker is read as that name with the player's word, and is no longer refused.
- EJS control code is taken out of every piece, and the text of its branches stays.
- Pieces that name a variable-update tool (
stat_data,[mvu_,<UpdateVariable>, JSON Patch) always reach the tracked-values step. - A name that turns up only once, inside pieces about something else, does not become an entity.
- Draft repair is implemented in
host/import/draft.ts,run-import.tsandworlds/world-check.ts.import_finishfirst mends what code can settle. Dangling faction members, relationships, known places, parents, ways and starting places are dropped, each with a note.- It then writes the world even if
checkWorld(the validation a session start runs) still finds problems. The import state becomesrepair, the result carriesdraft, andWorldSummary.draftmarks the card. - The assistant is told to repair the world with
check_worldand the original card. When a turn that could change the world stops while the world would still not start, the import hooks send it back (import/repair-again, at most 5 times). endReviewleaves the state atrepairwhile problems remain, andsettleDraftmakes the world an ordinary one oncecheckWorldpasses.GET /worlds/import/draft?id=gives the status and the problems.POSTtakes anaction:ask(the import command is sent again and the turn runs with the approval gate lifted,startRepair),keep(keepDraft) ordiscard(the folder goes to<root>/.trash/worlds).- Files are written with
atomicWrite(host/atomic-write.ts), andworld.jsonchanges go throughupdateWorldFile, which serialises them per world.
- An interrupted import keeps its card and extracted data in
.import/. This covers a server restart, a refused finish and a chat that ended early.POST /roleplay-api/worlds/import/resume?id=<world id>sends the import command to the world's chat again, and the hooks continue from the saved phase. The endpoint returns 422 when no import is waiting. - Guidance for making people up uses the
generatorrole: pools of kinds and situations, templates, tables, required profile fields and rules for introducing someone. It stays word for word ascustomlore withlistedscope. The game master and world author can read it, but characters cannot. The lore job does not rewrite it, andisGeneratorOnlykeeps it out of the entity roster. A piece that mixes generator guidance with other content keeps both. During play, the game master reads this lore before callingexpand_world; the world author searches for it and follows it. A card with no fixed cast becomes a world with no characters, so the player starts it with an avatar. - Instructions to the AI are partly carried.
import_buildhas one narrow job (import/style) read the card's system prompt, its post-history instructions and the pieces judged to be instructions, and keep only guidance on how the story is written and how the characters behave (voice, tone, perspective, staying in character, how a character declines). That becomes the world's prose promptsroleplay/styleandroleplay/character-style(Extracted.prompts), written byimport_finishintoprompts/<language>/roleplay/for every shipped language, because the card's wording does not depend on the interface language. Rules of the game, formats, status panels, scripts and anything about specific entities are not carried; what the job named as left out is a note in the report. A text that is too long or uses a placeholder the prompt is not given is refused and the instructions are then not carried (a note says why); the import goes on. - Pictures the card points to (
host/import/pictures.ts). Every string of the card, scripts and extensions included, is scanned for web addresses (NormalizedCard.urls). Those that name a PNG, JPEG, WebP or GIF file, or have no file type at all, are fetched in the background from the first import step on (pictures.jsonin the import folder;import_finishwaits for the run) into the world'simages/library/as if the person had uploaded them: reusable, undescribed until a session's interpreter reads them, copied into every session started from the world. A failure here never stops an import. Safety: only http/https on ports 80 and 443 without credentials; a literal private address is refused; names are resolved at connection time and refused when any answer is loopback, private, link-local, shared, multicast, reserved or an IPv6 form that embeds one; every redirect (at most four) is vetted again; no proxy. A HEAD request first drops what answers 404/410 or declares a non-image type; a service that refuses HEAD or declares nothing is fetched anyway. The download has a size cap (20 MB) and a timeout, ignores the declared type and keeps a file only when its first bytes are an image of a supported format and it is not tiny (1 KB) or a copy of another picture of the card or of one already in the library (compared bypictureHash: the pixels' PNG chunks without text, time and EXIF chunks, so the metadata written into saved files does not hide a copy). There is no limit on how many addresses are tried (cards can hold thousands); eight run at a time. - Tracked values.
import_buildhas one narrow job (import/variables) read the card's panels and formats (pieces judged to be templates) and its instructions, and declare what the card keeps track of (numbers with limits and bands, flags, choices; feelings are never declared, they exist already) as the world'svariables.json. Declarations are checked as the world checks them (variableProblems) and refused with the reasons until valid; keys are lowercase ASCII with underscores, and the names of characters, places and things it is limited to are turned into their keys. Initial values per entity are not extracted: a default applies. A failure is a note, never a stop. - What characters know of each other and the world. A character's
knows(places, things and ways between two places from the roster) becomesknowledgein its file (locations,items,exits), pruned to what was written and to ways the places really have (pruneKnowledge); the lore entries about a character are added to what that character knows, and a character named in someone's relationships gets a plainacquaintancerelationship back when it has none (completeAwareness), because in a closed world a missing edge means not knowing the other exists. - Narration presets. A SillyTavern prompt preset can be applied to an existing world: the Worlds page's Import narration preset uploads it to
<world>/.import/preset.json(POST /worlds/import/preset), the world's authoring chat is asked to callimport_preset, and the sameimport/stylejob turns the enabled prompts, in the preset's order (readPresetreads both the prompt-list layout and the older fixed keys), into the world's prose prompts. The call asks the person first because it replaces the world's own versions. The same job also fills a library preset: see Presets in the prompt system specification (import_narration_preset,POST /presets/import). Ticking Without the assistant in the dialog skips the conversion: no model runs and no question is asked, the ticked prompts become the world's narration style (roleplay/style) word for word in every shipped language, and the preset's sampling is written to the world. A style the world already carries is first moved to the world's.trash. - Lorebooks. A SillyTavern lorebook (an export with
entries, or a card'scharacter_book) is imported into the lorebook library, not into a world:POST /roleplay-api/lorebooks/import?name=&language=reserves an empty library entry, keeps the file in its.import/lorebook.json, opens the lorebook's authoring chat and tells it to callimport_lorebook. Entries that are off or empty are dropped and the tool's macros replaced. A narrow job (import/lorebook) decides for batches of at most eight entries (within a text budget) what each is: knowledge (kept word for word; the model gives a title, a kind, a one-sentence summary, and a scope withknown_byonly when the entry clearly states who knows it, otherwiseeveryone), steering (an instruction to the AI, rewritten as short guidance for a target prompt: style, character-style, persona, game-master or character-rules) or skip. Every entry must be decided or the answer is refused; the import fails as a whole when a batch never gives a usable answer, so nothing half-made is written. Knowledge becomes entries of the book (the entry's keywords go into its summary); steering is kept in.import/steering.json.apply_steeringthen makes a new library preset in which each target prompt keeps its shipped text and the guidance follows it, in every shipped language; it asks the person first, with a warning when a target is not a prose prompt. A catalogue entry of typesillytavern/lorebooktakes the same path (POST /roleplay-api/catalogue/convert/lorebook). - Other greetings. The card's other greetings (the first message, and the alternates that were not chosen as the opening) become the world's
alternate_openings, which the start dialog offers; the player marker and the card's markup are cleaned as for the chosen opening. - Portrait. The card's picture is also the canonical reference portrait of the character that carries the card's name (or the only character), written with metadata as an image the person brought.
- There is no player character unless the card supplies one. A world without a playable character shows a warning in the editor and cannot be started until one is added.
- The player in the card's text (
{{user}}) is the marker[PLAYER]for the models and is replaced by the player's character or a plain word at the end.{{char}}is the card's own name. - Ids are made from names in any script. Folder names, ids and image paths accept the letters and digits of every language, never separators or dot segments.
- Several characters in one card are supported: the roster decides who they are.
- Cards are the property of their authors: tests use invented cards (
tests/support/cards.ts), never content of a real one. - The card stays.
.import/is not removed after an import, and the authoring chat of the world has read-only tools over it:card_overview(every field, greeting and lorebook entry with its size, what the survey took it to be and which entities it was made into, the roster and the report's notes),card_search(phrase, then word, matches in any script, snippets),card_read(one piece word for word, 6000 characters a turn) andcard_origin(the pieces an entity was built from, recomputed with the samesourcesForthe build used; for a name the import made nothing of, the pieces that mention it). They exist while.import/card.jsondoes, so deleting the folder turns them off. The procedure prompt tells the assistant to look the card up, to treat the world's files as canon and the card as evidence, and to check the import's records against the text. The card is not shared by anything the plugin exports or translates (translation skips.import; there is no other export); sync carries it with the world. Play never reaches it. - What the card brought, and what became of it (
dialect.ts,outcomes.ts,job-cache.ts).dialectOfreads the machinery of the tool that made the card and runs none of it: regular-expression and helper scripts (counted when the card is read,NormalizedCard.ignored.regexScriptsandhelperScripts),{{macro}}names other than the player's and the character's, and tag blocks that are not ordinary markup. The review page (ImportPreview.dialect), the message that starts an import (sourceSummary: the source format, entry and opening counts, what is not carried) andImportReport.leftOut.dialectsay so.pieceOutcomesgives every piece of the card one outcome from the import's records:carried(an opening, word for word),converted(characters, places or things were made from it),guidance(instructions to the narrator),read(read for the world, nothing named came of it),omitted(notes, formats, pieces the person left off) orawaiting(the import has not reached it).card_overviewreturns the outcome of each piece and the totals, andImportReport.pieceskeeps the totals. Every job's answer is kept in.import/jobs/under a hash of its instructions, question and answer shape, so a build run again after a failure or a stop asks only for the jobs that did not finish; a new survey and a finished import clear it. - Updating a world from a newer card (
host/import/update.ts,update-tools.ts,card-diff.ts,snapshot.ts). Reviewing a card (ImportPreview.targets, fromfindTargets) lists every world whose.import/card.jsonexists with a finished import, those whose name equals the card's first (sameName);editedcompares the world's files with.import/files.json, a SHA-256 snapshot written when the import turn ends (endReview;images/and top-level dot entries excluded),unknownwhen there is none. A world's own page offers the update through the same review (GET /worlds/import/update?id=says whether the world was made from a card, whether an update waits and whether the last one can be undone).POST /worlds/import/startand/catalogue/convert/stagedtakeupdate: <world id>;startUpdatekeeps the world's files (notimages/, not dot entries) and the import's records in.import/before-update/, moves the upload into.import/update/, opens the authoring chat and sendsupdateMessage, a plain request with no special turn, so every edit is approved like any other (authoringApprovalapplies);updateHookssends an update turn back once if it stops with the update still waiting (a model ended a live turn on a change it found no place for), and lets a second stop through so a question can end the turn.card_diff(read-only) pairs the pieces of the two cards (matchPieces: fields by id; entries by title and keys, then by text, then by number with half their words shared), diffs changed pieces by sentence (diffText, at most 1500 sentences), adds the entities each old piece was made into (entitiesByPiece, the import's records; for a dropped piece also the ones only it made,madeOnlyFromIt), names what the card changed about the world itself (worldFieldChanges: description, tags, author, opening, other openings), whether the card's own picture and the picture addresses changed (pictureChange,addressChanges) and the files edited since. Undoing an update does not touch pictures. The assistant carries those withedit_world_info(which changes theworld.jsonfields ofWORLD_INFO_FIELDS, asks like every other edit, and refuses the name, preset, lorebooks, sampling and tool switches) and removes what only a dropped piece made unless the person edited it.card_readandcard_searchtakeversion: new.card_update_finishkeeps the old card, choices and picture in.import/previous, writes the new ones, carriesclassification.jsonover to the new piece ids and forgets dropped pieces, fetches the picture addresses the old card did not have and those the savedpictures.jsonshows failed for a reason that may pass (addressesToFetch,worthAskingAgain; with no record, the old card's addresses count as settled), merges the answers intopictures.json(mergeReports), lists fetched pictures whose address the card dropped (droppedFromTheCard, kept in the library), replaces the portrait that is still the card's picture (reference image with originuploadedand original namepicture.png) by the new picture, and takes a new snapshot.DELETE /worlds/import/update?id=(revertUpdate) puts the kept files and records back, moving what the world holds to.trash/update-<time>. The editor's default is update for a same-name world withedited: noand a new world otherwise; the world keeps its name; a catalogue release records its new version as the world's origin.
The check suite
host/checks/ holds one suite for everything that asks whether a world is right, in kits (shared/checks.ts: consistency, fidelity, quality, playability). The user-facing description is Checking a world.
- One contract. A check is
{ id, kit, mode, needs, description, run(input) }(check.ts) and returns findings:severity(error,warning,info),code,where,message, optionalhint.modeiscode(the program decides),judged(a model reads evidence the program gathered) ordecision(a yes-or-no a decision model may settle).needsiscardand/ormodel. A check that cannot run on a world throwsCheckSkippedand the report lists it with the reason instead of counting it as clean.runChecks(run.ts) runs the kits asked for; checks that are notcoderun only withdeep(deep: 'decision'also runs thedecisionones), and are listed as skipped otherwise. A run reads each part of the world once for all its checks (loaded.ts, thememoof the run's input). - Consistency (
consistency.ts) wrapscheckWorld(the start-time validation),lintWorldover a scratch story made from the world's files (worlds/scratch.ts, shared with the trigger check), tracked values with the same meaning for the same subjects (duplicateValues, a small synonym table), alias and name clashes, the world's own prompts (copyWorldPromptsinto a scratch folder), and fields no parser reads (unread-fields:Reader.unreadByruns each entity's real parser and returns the keys it never looked at, so nothing is listed twice). - Fidelity (
fidelity.ts,card-evidence.ts) works onCardEvidence: the card the world was made from, or the newer one waiting to update it together with the older one the import's records are about.madeIntosays which parts of the world each piece became: entities by the import's roster and classification (originOf), lore andworld.jsonby the share of their terms found in the piece (terms.ts; lore keeps no source of its own). Code checks: coverage of the pieces the import read, names found in the card, numbers kept, and with a newer card the changes the world does not say yet (matchPieces,diffText). The judgedcard-fidelityasks per document for dropped and invented facts, each with a quoted passage that must occur in the text it comes from (comparisonProblem), at most 40 documents and six facts of each kind per document. - Quality (
quality.ts) has three code checks (cast, places, what the world says about itself),sheet-playable(decision: the import's decision model throughverdictOfwhen it is sure, else the language model) andworld-gaps(judged over a digest of the world; each gap must name a key of the digest or the world). - Playability (
playability.ts) looks at the first scene of each playable character:first-scene(code: a start place, a way out, company or things) andopening-playable(decision: the opening and that scene, throughverdictOf). - Finding by meaning (
similar.ts,semantic.ts).ImportContext.embedembeds with the selected embedding provider (it rejects withNoEmbeddingServicewithout one).candidatesreturns the closest pairs that a check's own filter allows, above a deliberately low cosine cut and capped at 12, because closeness picks what to ask and decides nothing.duplicate-values-meaning,name-near(also by nested names) andlore-conflict(entries with disjoint dates are never paired) aredecisionchecks over those pairs:verdictOffirst, andlore-conflictthen asks the language model for a verdict with a quote from each entry that must be found in it (conflictProblem).card-coveragestays acodecheck but, when the run has a model, asks about the closest documents of each uncovered piece before reporting it. Vectors are kept by text hash in<world>/.checks/vectors.json(float32, base64, the newest 4000), and are dropped when the model or the size changes. The assistant'screate_world_elementandreplace_world_elementpassrefuseUnread, which usesReader.unreadByto refuse fields no parser reads. - Judging reuses the import's runner. A judged check is given the
ImportContextof the calling tool (contextOf), so the chat's model,importReasoning,importTemperature, the token cap, the timeout, concurrency andimportDecisionsapply. Nothing new is configured. - Results are stored per check in
<world>/.checks/results.json(results.ts); a run reports how many findings are new and which are gone against the stored result of the checks that ran. Dot folders are never part of a published world or a translation, as for.import. - Tools and commands.
check_world,check_card(withversion),check_quality,check_play(each withdeep) andcheck_listare tools of the world authoring chat (tools.ts). The commands/check,/check-all,/check-world,/check-card,/check-qualityand/check-playare read by one parser (shared/authoring-commands.ts, also used for/import-st);hooks.tsadds the noticechecks/noticeto the first step of the turn. - In the import flow.
finish,structureImportandcard_update_finishrun every check the program decides throughcheckImportedand store the result (with a decision model in the import, thedecisionchecks run too);import_finishandcard_update_finishreturn the findings aschecks. The review prompt tells the assistant to act on them and to run the deep checks when the card or the findings call for it. The update message tells the assistant to runcheck_cardwithversion: newbefore it finishes.
Prompts
All instructions of the import are files under import/ (see the prompt-system specification), in the language the person was using when they started it. Labels constructed by the host for the classifier and roster use that language too.
The review that follows an import may adjust those and also write the prose prompts of the world (roleplay/persona, roleplay/style, roleplay/character-style, documented in the prompt system specification) when the card asks for a narration style or for how the characters behave; the other prompts are rules and are refused to it.