Skip to main content

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​

  1. Upload (Worlds page, Import). The host reads the card (host/import/card.ts, at most importMaxUploadBytes, 100 MB by default), keeps it in a temporary import folder and shows what it holds.
  2. 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).
  3. 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.
  4. 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 to importConcurrency):
    • 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 (reviewing in .import/state.json lifts the approval gate until that turn ends). If problems remain, the files stay as a draft and the assistant continues repairing them.
  5. 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 importDecisions is decision, the configured decision service first checks whether a piece is only an AI instruction or irrelevant reader-facing material. A result at or above importConfidence is 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 importVariablesPerRequest values (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 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 through held_by. Things the character merely knows about remain in knows.
  • Casting passages become world mechanics (host/import/casting.ts, prompt import/casting). The importer reads those passages together, then writes commands and triggers from them:
    • commands.json holds the slash words the card names. A command that no trigger uses is not kept.
    • triggers.json gets entries whose effect is a beat that has the world author make the person (dispatch: author, a need, and a drawn option from the card's list in draw).
    • 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.triggers chooses who judges).
    • A mechanism with nothing to start it is left out, with a note.
  • A model-free import uses host/import/structure.ts, structureImport in run-import.ts, and POST /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 finishImport writes the world. The description and tags come from the card's own fields (ctx.noModel).
    • The import ends as done, or as repair when the world would not start yet, and provenance.json records it. There is no chat and no review.
  • 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_world in the result is ok, 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.
  • Standing rules and casting guidance come from extractEntities and the import/generator prompt. 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 prompt roleplay/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.ts and worlds/world-check.ts.
    • import_finish first 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 becomes repair, the result carries draft, and WorldSummary.draft marks the card.
    • The assistant is told to repair the world with check_world and 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).
    • endReview leaves the state at repair while problems remain, and settleDraft makes the world an ordinary one once checkWorld passes.
    • GET /worlds/import/draft?id= gives the status and the problems. POST takes an action: ask (the import command is sent again and the turn runs with the approval gate lifted, startRepair), keep (keepDraft) or discard (the folder goes to <root>/.trash/worlds).
    • Files are written with atomicWrite (host/atomic-write.ts), and world.json changes go through updateWorldFile, 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 generator role: pools of kinds and situations, templates, tables, required profile fields and rules for introducing someone. It stays word for word as custom lore with listed scope. The game master and world author can read it, but characters cannot. The lore job does not rewrite it, and isGeneratorOnly keeps 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 calling expand_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_build has 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 prompts roleplay/style and roleplay/character-style (Extracted.prompts), written by import_finish into prompts/<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.json in the import folder; import_finish waits for the run) into the world's images/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 by pictureHash: 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_build has 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's variables.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) becomes knowledge in 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 plain acquaintance relationship 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 call import_preset, and the same import/style job turns the enabled prompts, in the preset's order (readPreset reads 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's character_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 call import_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 with known_by only when the entry clearly states who knows it, otherwise everyone), 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_steering then 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 type sillytavern/lorebook takes 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) and card_origin (the pieces an entity was built from, recomputed with the same sourcesFor the build used; for a name the import made nothing of, the pieces that mention it). They exist while .import/card.json does, 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). dialectOf reads 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.regexScripts and helperScripts), {{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) and ImportReport.leftOut.dialect say so. pieceOutcomes gives 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) or awaiting (the import has not reached it). card_overview returns the outcome of each piece and the totals, and ImportReport.pieces keeps 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, from findTargets) lists every world whose .import/card.json exists with a finished import, those whose name equals the card's first (sameName); edited compares 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), unknown when 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/start and /catalogue/convert/staged take update: <world id>; startUpdate keeps the world's files (not images/, not dot entries) and the import's records in .import/before-update/, moves the upload into .import/update/, opens the authoring chat and sends updateMessage, a plain request with no special turn, so every edit is approved like any other (authoringApproval applies); updateHooks sends 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 with edit_world_info (which changes the world.json fields of WORLD_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_read and card_search take version: new. card_update_finish keeps the old card, choices and picture in .import/previous, writes the new ones, carries classification.json over to the new piece ids and forgets dropped pieces, fetches the picture addresses the old card did not have and those the saved pictures.json shows failed for a reason that may pass (addressesToFetch, worthAskingAgain; with no record, the old card's addresses count as settled), merges the answers into pictures.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 origin uploaded and original name picture.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 with edited: no and 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, optional hint. mode is code (the program decides), judged (a model reads evidence the program gathered) or decision (a yes-or-no a decision model may settle). needs is card and/or model. A check that cannot run on a world throws CheckSkipped and the report lists it with the reason instead of counting it as clean. runChecks (run.ts) runs the kits asked for; checks that are not code run only with deep (deep: 'decision' also runs the decision ones), and are listed as skipped otherwise. A run reads each part of the world once for all its checks (loaded.ts, the memo of the run's input).
  • Consistency (consistency.ts) wraps checkWorld (the start-time validation), lintWorld over 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 (copyWorldPrompts into a scratch folder), and fields no parser reads (unread-fields: Reader.unreadBy runs 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 on CardEvidence: 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. madeInto says which parts of the world each piece became: entities by the import's roster and classification (originOf), lore and world.json by 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 judged card-fidelity asks 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 through verdictOf when it is sure, else the language model) and world-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) and opening-playable (decision: the opening and that scene, through verdictOf).
  • Finding by meaning (similar.ts, semantic.ts). ImportContext.embed embeds with the selected embedding provider (it rejects with NoEmbeddingService without one). candidates returns 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) and lore-conflict (entries with disjoint dates are never paired) are decision checks over those pairs: verdictOf first, and lore-conflict then asks the language model for a verdict with a quote from each entry that must be found in it (conflictProblem). card-coverage stays a code check 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's create_world_element and replace_world_element pass refuseUnread, which uses Reader.unreadBy to refuse fields no parser reads.
  • Judging reuses the import's runner. A judged check is given the ImportContext of the calling tool (contextOf), so the chat's model, importReasoning, importTemperature, the token cap, the timeout, concurrency and importDecisions apply. 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 (with version), check_quality, check_play (each with deep) and check_list are tools of the world authoring chat (tools.ts). The commands /check, /check-all, /check-world, /check-card, /check-quality and /check-play are read by one parser (shared/authoring-commands.ts, also used for /import-st); hooks.ts adds the notice checks/notice to the first step of the turn.
  • In the import flow. finish, structureImport and card_update_finish run every check the program decides through checkImported and store the result (with a decision model in the import, the decision checks run too); import_finish and card_update_finish return the findings as checks. 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 run check_card with version: new before 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.