What was built on top of the original "AIgency" ChatGPT template, why, and how to operate it. Written for whoever maintains this project next (including future-you).
cards/<slug>/card.json)php/cards.phpjson/ifairy.jsonscripts/generate_ifairy_cards.pyThis project started from AIgency ("Igency"), a commercial ChatGPT-employees template by Polar Games (see the original documentation.pdf in this folder for the vendor's own docs on API keys, DALL-E, text-to-speech, etc. — that part of the app is untouched). The template shipped with 100 demo characters ("Ryan Johnson", "Marina Lee", ...) in json/employees.json, each pointing at an image that did not exist on disk — 0 of the 100 image paths resolved to a real file. That data was dead on arrival.
Separately, the employees/ folder (now ifairy/) contained 301 avatar images with no character data behind them: 270 had meaningful slug filenames like biologia-critter-code.jpg (clearly an intended edutainment roster covering school subjects, life skills and EN/FR/PL language content), and 30 had random upload-hash filenames (e.g. 689a34dc1d9de.jpg) plus one avatars.zip — those 31 carry no usable name and were left alone.
What changed:
| Path | What it is |
|---|---|
| cards/<slug>/card.json | One character's data. 270 folders, one per character. |
| cards/<slug>/image.* | That character's avatar image, living next to its card.json. |
| php/cards.php | Scans cards/ and returns all characters as one JSON array (the "cards" data source). |
| json/ifairy.json | Flat array with the same 270 characters, generated from cards/ (the "json" data source). |
| json/employees.json.legacy-backup | The original dead template data, kept only as a reference/backup. Not loaded by the app. |
| ifairy/ | Renamed from employees/. Raw source images (all 301, including the 31 unused ones) live here. Not served directly by the app anymore — cards/ holds the copies actually in use. |
| scripts/generate_ifairy_cards.py | The generator/import/sync tool described below. |
| json/config.json | Holds ifairy_data_source, the server-side default switch ("cards" or "json"). |
| json/lang.json | UI translation strings (EN/PT/ES), including the new Settings labels. |
Each character is a folder cards/<slug>/, where <slug> is the original avatar filename without its extension (e.g. cards/biologia-critter-code/). Inside:
card.json — the character's data (schema below)..jpg .jpeg .png .webp .gif .svg. The loader auto-detects it — no fixed filename required — but the generator always writes image.<ext>.| Field | Meaning |
|---|---|
name | Display name (from the filename's first hyphen-segment, e.g. "Biologia"). |
description | Third-person bio shown in the "about" modal. |
welcome_message | First message the character sends when a chat opens. |
expert | Short role/tagline shown under the name (from the filename's remaining segments, e.g. "Critter Count"). |
training | The system prompt sent to the model to roleplay this character. |
display_welcome_message | Boolean, whether to show welcome_message automatically. |
temperature, frequency_penalty, presence_penalty | Model sampling parameters. |
chat_minlength, chat_maxlength | Allowed length of a user's message. |
max_num_chats_api | How many past messages are sent to the API as context. |
API_MODEL | OpenAI model id, e.g. gpt-4o-mini. |
google_voice, google_voice_lang_code | Optional text-to-speech voice; null if unset. |
Note: card.json does not contain an image field — the image is whatever image file is found in the same folder. The image field only gets added when the data is exported into the flat array shape (php/cards.php's response, or json/ifairy.json).
A small PHP endpoint (no build step, no framework) that:
cards/.card.json — folders without one are skipped silently."image": "cards/<slug>/<file>" to the card data.employees.json/new ifairy.json, so the frontend doesn't need to know which source it's talking to.This is the "cards" data source. It re-reads the filesystem on every request, so editing or adding a cards/<slug>/card.json by hand takes effect immediately — no regeneration step needed.
This is a single JSON array with all 270 characters, in the original template's flat-file format. It exists so the "json" data source keeps working exactly like the template did originally, and so you can view/diff the whole roster in one file. It is generated from cards/ — don't hand-edit it, because the next time scripts/generate_ifairy_cards.py runs it will be overwritten. Edit the character's cards/<slug>/card.json instead, then re-run the script (or just switch the app to "cards" mode, which reads folders directly and needs no regeneration at all).
A standalone Python 3 script (stdlib only, no dependencies) with two jobs, run from anywhere:
python3 scripts/generate_ifairy_cards.py # import new avatars from ifairy/ + resync ifairy.json
python3 scripts/generate_ifairy_cards.py --overwrite # also regenerate text for characters that already have a card
python3 scripts/generate_ifairy_cards.py --sync-only # skip import, just rebuild json/ifairy.json from cards/
Scans ifairy/ (the renamed raw-avatar folder) for image files. A filename is treated as a real, named avatar unless its base name is a bare hex string of 10–16 characters (e.g. 689a34dc1d9de.jpg) — those are unnamed upload artefacts and get skipped. For every remaining filename that doesn't already have a cards/<slug>/card.json, it:
root-tagline-words, e.g. biologia-critter-code.jpg → root biologia, tagline Critter Code.root in a hand-authored dictionary of ~265 entries mapping it to a short subject/domain phrase (e.g. biologia → "biology and living creatures"). This dictionary is the actual creative content — it's what makes the generated text topically accurate instead of generic. If a root isn't in the dictionary (shouldn't happen for the current 270, but matters for any new avatar you drop in later), it falls back to a generic phrase built from the tagline itself.description, welcome_message and training — child-friendly (ages 6–12) copy: simple language, short sentences, explains big words, always stays in character.cards/<slug>/image.<ext> and writes card.json.Existing cards are left untouched by default — this is what makes it safe to re-run after you've hand-edited a character's bio; pass --overwrite only when you deliberately want the generator to replace hand-written text.
Always runs (unless nothing changed matters): rebuilds json/ifairy.json by reading every cards/<slug>/card.json currently on disk and its matching image. This keeps the flat "json" data source in sync with whatever is really in cards/, including any manual edits.
Text quality note: the 270 bios were produced by this template/dictionary approach, not written individually by hand. They're consistent and topically correct, but formulaic — treat them as a solid first draft. If a specific character deserves a richer, bespoke bio, edit its cards/<slug>/card.json directly and don't re-run with --overwrite on it.
Per request, every occurrence of "employee"/"employees" was renamed to "ifairy" across the project:
| Before | After |
|---|---|
employees/ (folder) | ifairy/ |
json/employees.json | retired to json/employees.json.legacy-backup; replaced functionally by json/ifairy.json |
employee_data_source (config.json key) | ifairy_data_source |
array_employees, employee_name, employee_expert, employee_image, employee_training, etc. (JS variables) | array_ifairy, ifairy_name, ifairy_expert, ifairy_image, ifairy_training, etc. |
.toggle_employees_list (CSS class, index.html + app.css) | .toggle_ifairy_list |
list_employees_label and its PT/ES translations (lang.json) | list_ifairy_label, re-translated ("ifairy list" / "Lista ifairy") |
Files touched: js/app.js, index.html, style/app.css, php/cards.php, json/config.json, json/lang.json.
Click the gear icon in the header to open a modal where you can pick the data source:
php/cards.php, i.e. live from the cards/ folders (the default).json/ifairy.json file.The choice is written to localStorage (key ifairy_data_source_override) in your own browser, then the page reloads. This intentionally does not write back to json/config.json on the server — the app has no login/auth layer, so a public write endpoint would let any visitor change the setting for everyone. json/config.json's ifairy_data_source value is still what every other visitor gets by default; change it there directly if you want to change the site-wide default.
Relevant code: js/app.js — the loadData() function checks the override before deciding whether to fetch php/cards.php or json/ifairy.json; the #modalSettings block wires up the two radio buttons.
Fastest path — bypass the generator entirely and just write the folder yourself:
cards/my-new-character/..jpg .jpeg .png .webp .gif .svg.cards/my-new-character/card.json following the schema in section 3 (easiest: copy an existing one and edit it).python3 scripts/generate_ifairy_cards.py --sync-only to fold it into json/ifairy.json.To bulk-import from filenames the way the original 270 were created, drop new avatar images into ifairy/ with a root-tagline-words.ext filename and run python3 scripts/generate_ifairy_cards.py.
The original AIgency template was hard-wired to a single OpenAI key. This update decouples the AI backend from any one provider — the app can now route chat requests to OpenAI, Anthropic Claude or Google Gemini, switchable live from the Settings modal without touching any server files.
| File | Change |
|---|---|
json/config.json |
Added ai_provider (server-side default, e.g. "openai"),
per-provider default model keys (openai_default_model,
anthropic_default_model, google_default_model),
model catalogues (anthropic_models_available,
google_models_available — 4 models each with descriptions),
and a top-level max_tokens (previously hardcoded in
php/api.php). The existing API_MODEL_options_available
(OpenAI models) is unchanged and still used by the dropdown.
|
php/key.php |
Split the single $API_KEY into three named keys:
$OPENAI_API_KEY, $ANTHROPIC_API_KEY,
$GOOGLE_API_KEY.
A legacy alias $API_KEY = $OPENAI_API_KEY keeps any
third-party code that still reads the old variable working.
|
php/api.php |
Full rewrite. Reads $_POST["provider"] (defaults to
"openai") and routes to one of three branches.
All three branches normalise their streaming output to OpenAI SSE
format (data: {"choices":[{"delta":{"content":"…"}}]}
+ data: [DONE]) so streamChat() in
js/app.js is unchanged.
Anthropic messages are split into a system field plus
messages array (as the Anthropic API requires).
Google Gemini messages use role: "model" for assistant turns
and a systemInstruction field. max_tokens and
temperature are read from POST, not hardcoded.
|
index.html #modalSettings |
Extended the Settings modal with a new section above the existing
data-source radio: three provider radios (OpenAI / Anthropic / Google AI)
and a <select id="aiModelSelect"> populated dynamically
by JS based on the active provider. The existing cards/json source radio
is untouched below an <hr>.
|
js/app.js |
Four targeted changes:
buildModelDropdown(provider) function and event
listeners on the provider radios and model select manage Settings modal
state and persist choices to localStorage.
|
The AI provider setting and the character data-source setting are independent. Every combination works:
| Data source | OpenAI | Anthropic | Google AI |
|---|---|---|---|
cards | ✓ | ✓ | ✓ |
json | ✓ | ✓ | ✓ |
api.php receives only model, provider
and array_chat — it has no knowledge of, and no dependency on,
which source loaded the character roster.
| Key | Values | Effect |
|---|---|---|
ai_provider_override | openai · anthropic · google | Overrides ai_provider in config.json for this browser session. |
ai_model_override | any model id | Overrides the provider's default model. Cleared automatically when the provider radio changes. |
ifairy_data_source_override | cards · json | Existing key — unchanged. |
Edit php/key.php and replace the placeholder strings:
$OPENAI_API_KEY = "sk-…"; // platform.openai.com/account/api-keys
$ANTHROPIC_API_KEY = "sk-ant-…"; // console.anthropic.com/settings/keys
$GOOGLE_API_KEY = "AIza…"; // aistudio.google.com/app/apikey
Keys you don't use can stay as placeholders — the PHP routing only calls the branch that matches the active provider.
The full list lives in json/config.json under
anthropic_models_available and google_models_available
(OpenAI models remain under the original API_MODEL_options_available).
To add a new model, add one line to the relevant object — the Settings modal
dropdown re-reads loadedConfig on every open, no deploy needed.
sandbox.galczynski.guru/ifairy/ started as a stripped-down copy of this app and has since grown into its own actively-developed flagship prototype — a real 18-character roster imported from a production database export, a multi-provider AI engine (OpenAI/Anthropic/Google), and a visual identity pulled directly from the live ifairy.co.uk brand. It has diverged enough from this codebase that duplicating its details here would just go stale — see its own documentation page for the current, accurate state.