Reference
Configuration reference
Every setting, with its default and range: how the face looks and moves (in Wireface Desktop and wireface-core), each desktop face's files and profile.json, environment variables, and the command line.
On this page
Where settings live
Each desktop face keeps its files in a folder of its own. The original face (started without --face)
uses data\ itself; a face started with --face NAME uses data\faces\NAME\, and
one started with --config PATH keeps its other files in data\faces\ under the config file's
name. data\ is in the app's folder: %LOCALAPPDATA%\Programs\Wireface\data when installed,
or the clone's own data\ folder (the WIREFACE_DATA environment variable moves it).
| File | What's in it |
|---|---|
profile.json | The face's settings: its name, the agent, the voice, the room and the phone line (below). Created the first time a setting is saved. |
face.json | How the face looks and moves (below). Changed from the Face tab. |
state.json | The conversation to resume, for each agent. |
secrets.json | API keys pasted into Settings. Never part of profile.json, which is the file people share. |
face_window.json | Where the face and panel windows are, and whether the panel is docked. |
face\ | The picture the face wears (photo.jpg) and where its landmarks are (landmarks.json). |
logs\ | app.log, shell.log, and the activity panel's activity-YYYY-MM-DD.jsonl. Private: they hold what the agent read and ran. |
phone\ | The phone line's transcripts, messages, limits and bookings.json. |
browser\ | The Chrome profile of the agent's own browser. |
Shared by every face: data\sandbox (the default working folder), data\cache (the model
lists), dataackups (a copy of each of the agents' files as it was before the
Tools tab first changed it), and the speech models (deps\models, or
MODELS_DIR).
wireface-core keeps nothing on disk: a face's settings are the config you give
createFace, or a config file you load.
Face settings
How the face looks and moves. These settings are the engine's, so they're the same in wireface-core's config, the
embed's URL parameters and the desktop app's face.json, apart from
the differences listed below. Numbers outside their range are refused: wireface-core
throws a ConfigError naming the setting, and the desktop app rejects the change.
Skin and wireframe
The normal way to choose a look is a skin plus skin_amount: how far the whole face is faded from the
glowing wireframe to the skin.
| Setting | Values | Default | What it does |
|---|---|---|---|
skin | a skin id, or null | null | The skin to wear, from the catalogue: An id is lowercase letters, digits, |
skin_amount | 0 to 1 | 1 | Wireframe (0) to skin (1): a cross-fade over the whole face. In between, the wireframe's lines, points and glow fade as the skin comes in, and the skin itself barely glows. A new value glides in. The Wireframe ↔ Skin slider. It needs a skin on (or |
style | hologram, sculpt, photo | hologram | Kept for compatibility. To choose a look, wear a skin and set |
Colours
| Setting | Values | Default | What it does |
|---|---|---|---|
preset | a preset name | Sets colors.primary, secondary and background at once (presets). It isn't kept itself: the three colours are. | |
colors.primary | #rrggbb | #22d3ee | The wireframe and its glow |
colors.secondary | #rrggbb | #6366f1 | The rim light |
colors.background | #rrggbb | #060b18 | A matching background for your page; the canvas itself stays transparent. In the desktop app, the floating panel's background. |
colors.iris | #rrggbb | #4f86c6 | Eye colour (skins use their own) |
colors.mouth | #rrggbb | #5c1018 | Inside the mouth |
colors.skin | #rrggbb | #c9a48e | The sculpted head's skin |
colors.lips | #rrggbb | #b06a6e | The sculpted head's lips |
Look
| Setting | Values | Default | What it does |
|---|---|---|---|
glow | 0 to 2 | 0.6 | Bloom around the wireframe's lines. With a skin on, a tenth of it is used, so the picture doesn't bloom. |
wire | 0 to 1 | 0.8 | How strong the wireframe's lines are |
fill | 0 to 1 | 0.3 | The surface fill behind the lines |
scanlines | 0 to 1 | 0.25 | Hologram scanlines |
points | 0 to 1 | 0 | Dots on the mesh's corners |
edge_fade | 0 to 1 | 0.5 | A soft edge around the face |
eyes | true, false | true | Eyeballs with irises; false gives hollow wireframe eyes |
Teeth
| Setting | Values | Default | What it does |
|---|---|---|---|
teeth_style | normal, braces, vampire, goofy, sharp, turkey | normal | The photographic teeth a skin shows when the mouth opens. A catalogue skin brings its own: the robot has braces, the monster sharp teeth and Dracula vampire teeth. |
teeth_width | 0.5 to 1.5 | 1 | How far the teeth reach towards the corners of the mouth |
teeth_size | 0.5 to 1.5 | 1 | How tall the teeth are |
Motion and lip sync
| Setting | Values | Default | What it does |
|---|---|---|---|
follow_mouse | true, false | true | The eyes and head follow the pointer |
motion | 0 to 2 | 1 | How much the head moves on its own |
expressiveness | 0 to 2 | 1 | How strongly expressions and gestures show |
mouth_gain | 0.3 to 2 | 1 | How far the lip sync opens the mouth |
sync_offset_ms | -200 to 200, whole | 0 | Lip-sync timing: positive moves the mouth later than the sound, negative earlier |
zoom | 0.5 to 2 | 1 | The face's size in its canvas. In the desktop app, its size in the floating panel. |
Colour presets
| Preset | primary | secondary | background |
|---|---|---|---|
cyan | #22d3ee | #6366f1 | #060b18 |
ember (claude in the desktop app) | #d97757 | #f2c38f | #14100c |
green | #34f5a0 | #0ea5e9 | #03110b |
amber | #fbbf24 | #f97316 | #140c02 |
violet | #a78bfa | #ec4899 | #0e0718 |
rose | #fb7185 | #f59e0b | #16060a |
ice | #e0f2fe | #7dd3fc | #0b1220 |
The desktop app's face.json
Wireface Desktop keeps each face's look in face.json, changed from the Face
tab. It has the face settings above, with these differences:
- No
skinsetting. The face always wears a picture: one of the eight portraits or your own photo, chosen on the Face tab or dropped on the face, and kept in the face'sface\folder. A new face wears Mei.photorecords which (version,w,handportrait); Wireface sets it, not you. styleis alwaysphoto. Aface.jsonfrom an older version that sayshologramis read asphotowithskin_amount0 (the wireframe), andsculptasphoto.- Other defaults: the colours are the
claudepreset,glowis 1,wire0.85,fill0.35 andscanlines0.3. - The presets are
claude,cyan,green,amber,violet,roseandice.
And four settings of its own:
| Setting | Values | Default | What it does |
|---|---|---|---|
mode | floating, windowed | floating | Just the face on the desktop, or the floating panel with captions and buttons |
background | none, solid, gradient, grid, stars | gradient | Behind the face in its floating panel |
captions | true, false | true | Subtitles under the face in its floating panel |
on_top | true, false | true | Keep the face above other windows |
When it loads face.json, Wireface drops anything that's no longer valid and uses the default instead.
Config files for wireface-core
A wireface-core face's settings can live in a small JSON file. Every setting is optional; missing ones take their
defaults. $schema points editors such as VS Code at the JSON Schema, for autocomplete and checking:
{
"$schema": "https://raw.githubusercontent.com/compsmart/wireface-core/main/config/face.schema.json",
"skin": "dracula",
"skin_amount": 0.6,
"teeth_style": "vampire",
"preset": "rose",
"motion": 0.7,
"expressiveness": 1.3
}import { createFace, validateConfig } from '@wireface/core';
const config = await (await fetch('/faces/dracula.json')).json();
const face = createFace(canvas, { config }); // checked: a ConfigError names the bad setting
validateConfig(config); // or check one yourself: returns the full configThe errors say exactly what's wrong: glow: 3 is outside 0..2, colors.primary: expected
#rrggbb, unknown setting "glo". The schema is config/face.schema.json in
wireface-core (or @wireface/core/face.schema.json), and config/examples/ has a few
files to start from. The demo's Config panel shows any face you set up there as a file, with only what differs from
the defaults.
In code, DEFAULTS holds every default, RANGES the limits of every number setting,
PRESETS the presets, and STYLES and TEETH_STYLES the choices.
The desktop app's profile.json
Each desktop face's settings, apart from its look. It's created the first time a setting is saved, and it's easiest
to change from the panel, which saves as you go. To edit it by hand, quit the face first. The copy button next to
Config file on the Agent tab gives you its path. profile.example.json, next to the app, shows the
shape.
- A file that isn't valid JSON is reported, and the defaults are used until it's fixed. If the face saves over it
meanwhile, the broken file is kept as
profile.json.bad. - A setting that's no longer valid (a working folder that's gone, say) is dropped, with a warning in
app.log, and its default used. - Changes made in the panel are checked the same way, and a refused change names the setting.
The face
| Key | Default | Values | What it does |
|---|---|---|---|
name | "Claude" | 1 to 40 characters | The face's name: shown on it, and what you say to interrupt it |
port | 8791 for the original face, 0 for others | 0 to 65535 | The face's local web server. 0 is any free port. |
The original face takes its defaults for these from WIREFACE_NAME and WIREFACE_PORT.
The agent: backend
| Key | Default | Values | What it does |
|---|---|---|---|
backend.type | "claude" | claude, opencode, codex | The agent behind the face |
backend.cwd | data\sandbox | a folder that exists | Where the agent works. WIREFACE_CWD changes the default. |
backend.model | null | a model name, or null | The model; null leaves it to the agent |
backend.permission_mode | "default" | default, plus acceptEdits and plan (Claude Code), own (OpenCode), read-only (Codex) | When the agent asks first (permissions). Not used while permissions are skipped. |
backend.skip_permissions | false | true, false | Never ask before acting (skipping permissions) |
backend.auto_start | true | true, false | Start the agent session with the face |
backend.tools.computer | true | true, false | Claude Code can see the screen and use the mouse and keyboard |
backend.tools.browser | true | true, false | Claude Code can use a Chrome window of its own |
backend.tools.chrome | true | true, false | Claude Code can work in your Chrome (Claude in Chrome) |
backend.options | {} | an object | For the agent: cli_path (its executable) and cli_args (OpenCode and Codex) (details) |
The voice: voice
| Key | Default | Values | What it does |
|---|---|---|---|
voice.on | true | true, false | Speak replies. false: captions only, and no realtime voice. |
voice.listening | false | true, false | The microphone is on when the face starts |
voice.mode | "realtime" | realtime, classic | A realtime speech-to-speech model talks with you, or the classic voice reads the replies |
voice.realtime.provider | "gemini" | gemini, openai | Gemini Live or OpenAI Realtime |
voice.realtime.gemini.model | "gemini-3.8-live" | a model name | The Gemini Live model |
voice.realtime.gemini.voice | "Puck" | a voice name | Its voice |
voice.realtime.gemini.api_key_env | "GEMINI_API_KEY" | a variable name | The environment variable to read the key from (GOOGLE_API_KEY is tried too) |
voice.realtime.openai.model | "gpt-realtime-2.1" | a model name | The OpenAI Realtime model |
voice.realtime.openai.voice | "marin" | a voice name | Its voice |
voice.realtime.openai.api_key_env | "OPENAI_API_KEY" | a variable name | The environment variable to read the key from |
voice.realtime.narration | "normal" | quiet, normal, chatty | How often it reports progress (Updates) |
voice.realtime.persona | "self" | self, relay | Speaks as the agent, or about it |
voice.realtime.barge_in | false | true, false | Interrupt it by talking (use headphones) |
voice.realtime.fallback | true | true, false | The classic voice reads the replies while the realtime voice can't connect; false: they show as text |
voice.hearing.name_interrupt | true | true, false | Saying its name stops it talking, and it listens |
voice.hearing.denoise | true | true, false | Reduce steady background noise before recognition |
voice.hearing.focus | true | true, false | Ignore voices much quieter than yours |
voice.engine | "auto" | auto, local, elevenlabs | The classic voice: ElevenLabs when a key is available, this PC only, or ElevenLabs first |
voice.local.engine | "pocket" | pocket, kokoro | The voice on this PC (TTS_ENGINE sets the default) |
voice.local.voice | "marius" | a voice name | Which of the local model's voices |
voice.elevenlabs.voice_id | null | a voice id, or null | The ElevenLabs voice; null: the first voice on the account |
voice.elevenlabs.model_id | "eleven_flash_v2_5" | a model id | The ElevenLabs model |
voice.elevenlabs.api_key_env | "ELEVENLABS_API_KEY" | a variable name | The environment variable to read the key from (XI_API_KEY is tried too) |
Rooms: room
| Key | Default | Values | What it does |
|---|---|---|---|
room.name | null | up to 40 letters, digits, . _ -, or null | The room this face joins; null: it works alone. --room NAME joins one for a single run. |
room.aliases | [] | up to 10 names, each up to 30 characters | Other ways of saying this agent's name, for speech recognition |
room.lead | "first" | first, or an agent's name | Whoever answers first leads, or that agent always does |
The phone line: phone
| Key | Default | Values | What it does |
|---|---|---|---|
phone.enabled | false | true, false | The phone line |
phone.account_sid | null | AC and 32 hex digits | The Twilio account SID |
phone.number | null | a phone number | The Twilio number, in full with its country code |
phone.auth_token_env | "TWILIO_AUTH_TOKEN" | a variable name | The environment variable with the auth token (or paste it in Settings) |
phone.owner | null | up to 60 characters | Who the line agent works for ("calling on behalf of ...") |
phone.default_prefix | "+44" | a country code | For numbers written without one |
phone.allow_countries | ["+44"] | up to 40 country codes | Where texts and calls may go |
phone.contacts | {} | up to 300, "name": "number" | Who the agent may text and call by name |
phone.texts | "receptionist" | receptionist, notify, off | Incoming texts: answered, passed to you, or ignored |
phone.calls | "receptionist" | receptionist, off | Incoming calls: answered, or not |
phone.public_url | null | an https:// address, or null | The phone server's public address; null: a Cloudflare tunnel |
phone.manage_number | true | true, false | Point the number's calls here on start, and turn off Twilio's demo reply to texts |
phone.check_every_s | 15 | 5 to 300 | Seconds between checks for new texts |
phone.limits.texts_per_day | 20 | 0 to 500 | Texts your agent may send a day |
phone.limits.calls_per_day | 10 | 0 to 200 | Calls your agent may make a day |
phone.limits.replies_per_number_per_day | 20 | 0 to 200 | Texts the line agent answers from one number a day |
phone.limits.call_minutes | 10 | 1 to 60 | The longest a call may last |
phone.models.text | "gemini-3.8-flash" | a model name | The model answering texts (an OpenAI one is used when there's only an OpenAI key) |
phone.models.call_provider | "gemini" | gemini, openai | The service for live calls |
phone.models.call | "gemini-3.8-live" | a model name | The live call model |
phone.models.call_voice | "Puck" | a voice name | Its voice |
phone.receptionist.business | null | up to 80 characters | The business's name, as callers know it |
phone.receptionist.about | "" | up to 4,000 characters | What callers may be told: services, prices, address |
phone.receptionist.bookings | true | true, false | Take bookings |
phone.receptionist.hours | 09:00-17:30 Monday to Friday | mon to sun: "HH:MM-HH:MM" or null | Opening hours; null is closed |
phone.receptionist.slot_minutes | 30 | 5 to 240 | The length of an appointment |
phone.receptionist.max_days_ahead | 60 | 1 to 365 | How far ahead bookings may be |
phone.receptionist.per_caller_per_day | 2 | 1 to 20 | Bookings one caller may make a day |
An example
{
"name": "Claude",
"port": 0,
"backend": {
"type": "claude",
"cwd": "C:\\Users\\you\\projects\\something",
"model": null,
"skip_permissions": false,
"permission_mode": "default",
"auto_start": true,
"options": {}
},
"voice": {
"on": true,
"listening": false,
"mode": "realtime",
"realtime": {
"provider": "gemini",
"gemini": { "model": "gemini-3.8-live", "voice": "Puck", "api_key_env": "GEMINI_API_KEY" },
"openai": { "model": "gpt-realtime-2.1", "voice": "marin", "api_key_env": "OPENAI_API_KEY" },
"narration": "normal",
"persona": "self",
"barge_in": false
},
"hearing": { "name_interrupt": true, "denoise": true, "focus": true },
"engine": "auto"
},
"room": { "name": null, "aliases": [], "lead": "first" }
}Older versions kept a single face's settings in data\settings.json. If the original face has no
profile.json yet, its working folder, permission mode, model, voice settings and conversation are carried
over from there.
Environment variables
| Variable | Default | What it does |
|---|---|---|
GEMINI_API_KEY, GOOGLE_API_KEY | The Gemini key: the realtime voice and the phone line. Also wireface-core's tools.voice. | |
OPENAI_API_KEY | The OpenAI key | |
ELEVENLABS_API_KEY, XI_API_KEY | The ElevenLabs key | |
TWILIO_AUTH_TOKEN | The Twilio auth token | |
WIREFACE_NAME | Claude | The original face's default name |
WIREFACE_PORT | 8791 | The original face's default port |
WIREFACE_CWD | data\sandbox | The default working folder |
WIREFACE_DATA | data in the app's folder | Where the faces keep their files |
WIREFACE_DEPS | deps in the app's folder | The installed runtimes, and the speech models unless MODELS_DIR is set |
MODELS_DIR | deps\models (or B:\models if that folder exists) | The speech and face models, shared by every face |
SPEECH_DIR | speech in MODELS_DIR | The speech models on their own |
TTS_ENGINE | pocket | The default voice on this PC: pocket or kokoro |
TTS_SPEED | 1.05 | How fast the voice on this PC speaks |
KOKORO_SID | 0 | Which Kokoro speaker |
ASR_THREADS | 6 | CPU threads for speech recognition |
CLAUDE_CONFIG_DIR | ~\.claude | Claude Code's folder: its session files (Activity's Raw view), and its settings, MCP servers and skills (the Tools tab). It also moves ~\.claude.json |
CODEX_HOME | ~\.codex | Codex's folder: its config.toml, MCP servers and skills (the Tools tab) |
OPENCODE_CONFIG_DIR | ~\.config\opencode (or opencode in XDG_CONFIG_HOME) | OpenCode's global config and skills (the Tools tab) |
Each key setting can name another variable to read (api_key_env, auth_token_env). A key
pasted into Settings is used before any variable. On Windows, a user or system variable set while Wireface is running
is picked up within seconds.
Command line
Wireface Desktop
Wireface.exe (installed), Wireface.cmd (a clone) and
.venv\Scripts\python -m wireface all take the same options:
| Option | What it does |
|---|---|
--face NAME | Run the face called NAME, with its own files in data\faces\NAME. Without it: the original face. |
--config PATH | Run the face configured by that profile.json; its other files go to data\faces\ under the file's name |
--room NAME | Join that room for this run |
--browser | Don't open the desktop window: print a URL to open in a browser |
--version | Print the version |
From a clone, there are also:
RefreshModels.cmd # download the Model settings' lists again (--face NAME for another face)
.venv\Scripts\python -m wireface.models --check # which speech models are installed or missing (--load: load them too)
.venv\Scripts\python -m wireface.models --install # download what's missing (--with-kokoro: the Kokoro voice too)
scripts\setup.ps1 # install or repair everything (-WithKokoro)wireface-core
npm start # the examples and demo on http://127.0.0.1:5173/ (PORT=8080 npm start for another port)
npm test # node --test: config, schema, assets, loading
python -m tools.voice "Hello there." --voice Kore -o assets/voices/hello # text to a clip (GEMINI_API_KEY)
python -m tools.lipsync speech.wav --text "Hello there." -o speech.json # any audio to a clip
python -m tools.lipsync --get-lexicon # once: the pronunciation lexiconThe clip tools are explained in Making clips.