← Back to the app

ifairy — System Documentation

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).

  1. Overview: from AIgency template to ifairy
  2. File & folder map
  3. The card system (cards/<slug>/card.json)
  4. The loader: php/cards.php
  5. The flat mirror: json/ifairy.json
  6. The generator script: scripts/generate_ifairy_cards.py
  7. The employee → ifairy rename
  8. The Settings switch (cards vs. json)
  9. How to add one new character
  10. Multi-provider AI engine (OpenAI · Anthropic · Google)
  11. The sibling project: ifairy/

1. Overview: from AIgency template to ifairy

This 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:

2. File & folder map

PathWhat it is
cards/<slug>/card.jsonOne character's data. 270 folders, one per character.
cards/<slug>/image.*That character's avatar image, living next to its card.json.
php/cards.phpScans cards/ and returns all characters as one JSON array (the "cards" data source).
json/ifairy.jsonFlat array with the same 270 characters, generated from cards/ (the "json" data source).
json/employees.json.legacy-backupThe 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.pyThe generator/import/sync tool described below.
json/config.jsonHolds ifairy_data_source, the server-side default switch ("cards" or "json").
json/lang.jsonUI translation strings (EN/PT/ES), including the new Settings labels.

3. The card system

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 schema

FieldMeaning
nameDisplay name (from the filename's first hyphen-segment, e.g. "Biologia").
descriptionThird-person bio shown in the "about" modal.
welcome_messageFirst message the character sends when a chat opens.
expertShort role/tagline shown under the name (from the filename's remaining segments, e.g. "Critter Count").
trainingThe system prompt sent to the model to roleplay this character.
display_welcome_messageBoolean, whether to show welcome_message automatically.
temperature, frequency_penalty, presence_penaltyModel sampling parameters.
chat_minlength, chat_maxlengthAllowed length of a user's message.
max_num_chats_apiHow many past messages are sent to the API as context.
API_MODELOpenAI model id, e.g. gpt-4o-mini.
google_voice, google_voice_lang_codeOptional 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).

4. The loader: php/cards.php

A small PHP endpoint (no build step, no framework) that:

  1. Lists the subfolders of cards/.
  2. For each one, reads card.json — folders without one are skipped silently.
  3. Finds the first file in that folder with a recognised image extension.
  4. Adds "image": "cards/<slug>/<file>" to the card data.
  5. Returns the whole thing as one JSON array, in the exact same shape as the old 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.

5. The flat mirror: json/ifairy.json

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).

6. The generator script: scripts/generate_ifairy_cards.py

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/

Job 1 — Import

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:

  1. Splits the filename on hyphens: root-tagline-words, e.g. biologia-critter-code.jpg → root biologia, tagline Critter Code.
  2. Looks up 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.
  3. Fills a template to produce description, welcome_message and training — child-friendly (ages 6–12) copy: simple language, short sentences, explains big words, always stays in character.
  4. Copies the image into 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.

Job 2 — Sync

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.

7. The employee → ifairy rename

Per request, every occurrence of "employee"/"employees" was renamed to "ifairy" across the project:

BeforeAfter
employees/ (folder)ifairy/
json/employees.jsonretired 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.

8. The Settings switch (cards vs. json)

Click the gear icon in the header to open a modal where you can pick the data source:

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.

9. How to add one new character

Fastest path — bypass the generator entirely and just write the folder yourself:

  1. Create cards/my-new-character/.
  2. Drop an image in it, any name, any of .jpg .jpeg .png .webp .gif .svg.
  3. Create cards/my-new-character/card.json following the schema in section 3 (easiest: copy an existing one and edit it).
  4. If the app is set to "cards" mode, it's live immediately — no restart, no build. If it's set to "json" mode, run 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.

10. Multi-provider AI engine (OpenAI · Anthropic · Google)

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.

10.1 What changed and where

FileChange
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:
  1. New globals ai_provider, ai_model_override and two localStorage key constants.
  2. loadData() reads both overrides from localStorage immediately after the data-source override.
  3. getResponse() appends provider and max_tokens to the POST payload.
  4. displayChat() overrides API_MODEL with the provider's active model (from override or config default) and sets is_model_turbo = true unconditionally — safe because api.php always returns the choices[0].delta shape regardless of provider.
A new buildModelDropdown(provider) function and event listeners on the provider radios and model select manage Settings modal state and persist choices to localStorage.

10.2 Data-source × provider orthogonality

The AI provider setting and the character data-source setting are independent. Every combination works:

Data sourceOpenAIAnthropicGoogle 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.

10.3 localStorage keys

KeyValuesEffect
ai_provider_overrideopenai · anthropic · googleOverrides ai_provider in config.json for this browser session.
ai_model_overrideany model idOverrides the provider's default model. Cleared automatically when the provider radio changes.
ifairy_data_source_overridecards · jsonExisting key — unchanged.

10.4 Adding API keys

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.

10.5 Supported models

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.

11. The sibling project: ifairy/

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.