Skip to main content

Data Model

Onboarding V4 keeps state in two places: the PostgreSQL ml schema, which is authoritative, and a local state/ tree that mirrors summaries as JSON files and stores the binary artifacts (adapters, audio) that do not belong in a database.

Schema

The schema name is fixed. resolve_db_schema() raises at import time if DB_SCHEMA is anything other than ml, and every connection is opened with search_path=ml.

TableOwner of
app_runsThe onboarding + training lifecycle. One row per run.
avatarsFinal avatar state, including the runtime prompt and adapter path. Read by LIS 3.
app_jobsGeneration, finalization, and training jobs.
app_eventsAppend-only audit trail.
app_sources, app_source_contents, app_source_chunksKnowledge ingestion. Created but unused by this codebase.

app_runs

ColumnTypeNotes
run_idtext PKrun_ + 12 hex characters.
creator_idtextMigrated in place from a former user_id column.
avatar_iduuid
statustext NOT NULLSee Run Lifecycle.
stagetext NOT NULLDerived from status: workflow, training, completed, failed, cancelled.
domain_idtextSet once the domain is settled.
request_payloadjsonbThe creator's request plus the domain decision.
review_payloadjsonbGenerated samples, review state, review window, avatar prompt.
artifact_manifestjsonbSummaries, final dataset, training manifest, quality results, artifact paths.
current_job_idtextThe job that owns the run right now.
created_at, updated_at, completed_attimestamptzcompleted_at is stamped on completed and failed.

Index: idx_app_runs_status.

avatars

ColumnTypeNotes
avatar_iduuid PKSupplied by the caller.
uiduuid NOT NULLgen_random_uuid() default, uniquely indexed.
creator_idtext
nametextSystem name, derived from the domain's display name.
user_avatar_nametextCreator's override; wins over name in API responses.
descriptiontextThe domain's first description.
tagsjsonb[vertical], or [].
preview_urltextNot written by this service.
customizationjsonbHolds character_story.
collection_nametextNullable; empty strings are normalized to NULL.
is_publishedbooleanCreator-controlled.
lora_quality_passedbooleanThe evaluation verdict. GET /v1/avatars?trained_only=true filters on it.
lora_quality_scoredouble precisionAverage judged accuracy.
lora_pass_reporttextPath to lora_quality_report.json.
lora_adapter_pathtextLocal adapter directory.
system_prompttext NOT NULL, default ''The refined runtime prompt.
voice_clone_path, voice_clone_texttextUploaded reference audio and its script.
elevenlabs_voice_idtextSelected roster voice.
statustext NOT NULL, default activeactive or deleted.
date, updated_attimestamptzdate is the creation timestamp.

Indexes: idx_avatars_uid_unique, idx_avatars_creator on (creator_id, updated_at DESC), and a partial idx_avatars_active_catalog on (updated_at DESC, avatar_id) WHERE status = 'active'.

upsert_avatar is field-preserving: NULL or empty-string inputs keep the stored value instead of clearing it, [] keeps existing tags, {} keeps existing customization, and the ON CONFLICT branch carries WHERE avatars.status = 'active' so a soft-deleted avatar is never silently resurrected.

app_jobs

ColumnTypeNotes
job_idtext PKjob_ + 12 hex characters.
run_idtext FK → app_runsON DELETE SET NULL.
source_idtext FK → app_sourcesUnused.
job_typetext NOT NULLsmart_generate, smart_finalize, train.
statustext NOT NULLqueued, running, completed, failed, cancelled.
attempt_nointeger NOT NULL, default 1Incremented on every task retry.
payloadjsonbJob input; training jobs also carry the training manifest.
error_messagetextFailure or retirement reason.
created_at, updated_at, finished_attimestamptzupdated_at doubles as the liveness heartbeat.

Indexes: idx_app_jobs_status, idx_app_jobs_claim on (status, job_type, created_at ASC), idx_app_jobs_run_active on (run_id, status, job_type).

Eligibility is enforced in SQL, not only in Python. common/db_utils.py maps each job type to the run statuses it may run in, and both the claim query and the maintenance sweeps apply that map plus a current_job_id guard:

job_typeEligible run statuses
smart_generatesmart_queued, smart_running
smart_finalizereview_approved, postprocess_running
trainthe seven training_* statuses plus legacy training_ready

app_events

ColumnTypeNotes
event_idbigserial PK
run_id, source_id, job_idtext FKAll ON DELETE SET NULL.
event_typetext NOT NULLSee the event vocabulary.
severity, actor_type, actor_idtextCurrently always info / service / unset.
messagetextHuman-readable summary.
payloadjsonbFull context — often the whole training manifest.
created_attimestamptz

Index: idx_app_events_run on (run_id, created_at DESC).

Events are also a fallback data source: instance cleanup reconstructs provider state from the most recent training_started / training_payload_uploaded / training_instance_ready / training_instance_requested payload when both the run manifest and the job payload have gone stale.

JSONB writes

All JSONB goes through _jsonb(), which strips \x00 from every string, converts sets and tuples to lists, and serializes anything else with default=str — so a datetime or UUID never breaks a write.

Two merge helpers do shallow top-level merges with the || operator:

  • merge_run_artifacts(run_id, patch)artifact_manifest = artifact_manifest || patch;
  • merge_job_payload(job_id, patch)payload = payload || patch.

Because the merge is shallow, a patch replaces a whole top-level key rather than merging into it. This is why the training stages rebuild the manifest and copy forward runpod, vast, payload, command, monitor, and artifacts explicitly.

JSONB payload shapes

request_payload

{
"mode": "train",
"specialty_text": "…",
"seed_examples": ["…"],
"elevenlabs_voice_id": "JBFqnCBsd6RMkjVDRZzb",
"domain_override": null,
"domain_decision": {
"domain_id": "math",
"confidence": 0.91,
"needs_confirmation": false,
"top_matches": [ { "domain_id": "math", "domain": "…", "score": 0.91 } ],
"threshold_used": 0.85,
"margin": 0.07,
"margin_threshold_used": 0.01,
"normalized_input": "…",
"dataset_file": "…/math_base.json",
"sample_count": 41
}
}

review_payload

{
"generated_samples": [ { "instruction": "…", "output": "…", "type": "standalone", "personalization": {} } ],
"review_status": "pending",
"generated_at": "2026-06-29T11:45:23+00:00",
"generation_source": "kimi",
"avatar_prompt": {
"domain_id": "math",
"base_system_prompt": "…",
"refined_system_prompt": "…",
"cleaned_seed_examples": ["…"],
"status": "published",
"error": null
},
"domain_decision": {},
"domain_confirmation": {},
"review_epoch": 1,
"review_window": {
"started_at": "…",
"deadline_at": "…",
"scheduled_in_seconds": { "reminder_24": 86400, "reminder_48": 172800, "finalize": 259200 },
"reminder_24_sent": false,
"reminder_48_sent": false,
"finalized_at": null,
"decision_source": null
},
"review": {
"action": "approve",
"note": "",
"accepted": true,
"needs_regeneration": false,
"reviewed_at": "…",
"decision_source": "manual"
}
}

review_status takes pending, queued, approved, auto_approved, rejected, or edit_request. review.action is approve, edit_request, reject, or auto_approve.

artifact_manifest

KeyWritten by
smart, review, generatedEvery artifact sync — generation and review summaries.
final_datasetsmart.finalize — the blended dataset and its metadata.
final_dataset_summaryDerived summary (path, counts, blend metadata).
trainingEvery training stage — the full manifest.
training_job_id, training_job_statusConvenience mirrors of the training job's id and pipeline status.
quality_gatetraining.complete — the handoff checklist.
lora_qualitytraining.complete — the evaluation verdict and report path.
avatar_profiletraining.sync_artifacts — adapter publication state.
artifact_pathsAbsolute paths of every JSON file written under state/runs/{run_id}/.

The training manifest carries provider state across stages:

{
"job_id": "job_…", "run_id": "run_…", "avatar_id": "…", "domain_id": "math",
"pipeline_status": "training_monitoring",
"canonical_run_status": "training_monitoring",
"provider": "runpod",
"dataset": { "source": "artifact_manifest.final_dataset.examples", "example_count": 236, "ready_for_training": true },
"runpod": { "enabled": true, "status": "ssh_ready", "pod_id": "…", "ssh_host": "…", "ssh_port": 12345 },
"vast": { "enabled": true, "status": "fallback_not_used" },
"payload": { "remote_dataset_path": "…", "remote_output_dir": "…", "remote_log_path": "…" },
"command": { "command": "python …", "remote_train_pid": "1234", "started_at": "…" },
"monitor": { "status": "training_succeeded", "final_adapter_remote_path": "…", "verified_outputs": [] },
"artifacts": { "artifact_dir": "…", "adapter_dir": "…", "files": [] }
}

Runtime state

ensure_runtime_dirs() creates the tree on every process start.

state/
├── config/
│ ├── domains.json # 26-domain catalog
│ ├── domain_datasets/{domain_id}_base.json
│ ├── lora_eval_questions/{domain_id}.json
│ ├── avatar_domain_mapping.json # mutable; excluded from checksums
│ ├── bootstrap_checksums.json # SHA-256 manifest
│ └── domain_candidates/{domain_id}/ # unreleased packs
├── runs/{run_id}/
│ ├── smart_job.json
│ ├── review.json
│ ├── generated.json
│ ├── final_dataset.json
│ ├── final_dataset_summary.json
│ ├── training_job.json
│ ├── avatar_profile.json
│ └── quality_gate.json
├── models/{avatar_id}/
│ ├── adapter_config.json
│ ├── adapter_model.safetensors
│ ├── tokenizer*.json, vocab.json, merges.txt, …
│ ├── training_summary.json
│ ├── training_metadata.json
│ ├── lora_quality_report.json
│ └── train.log # only with TRAINING_DOWNLOAD_REMOTE_LOG=true
├── voice/
│ ├── previews/manifest.json
│ ├── previews/audio/{key}.mp3
│ └── clones/{creator_id}/{avatar_id}/voice_clone_*.{wav,mp3,m4a,ogg,webm}
├── secrets/{runpod,vast}/id_ed25519 # chmod 600, enforced on every use
├── logs/
│ ├── workers/{smart_worker,training_worker}.log
│ ├── training/lora_quality_eval.log
│ └── ops/{start_all,stop_all,status_all}/
└── pids/{api,smart_worker,training_worker}.pid
note

Files under state/runs/ are mirrors, not sources of truth — everything in them also lives in artifact_manifest. Files under state/models/ are not mirrored: an adapter directory is the only copy, and training.sync_artifacts deletes and recreates it on each run, so re-running training for the same avatar replaces the previous adapter.

state/models/{avatar_id} uses the normalized avatar id — lowercased, with anything outside [a-z0-9_-] collapsed into single underscores. Standard UUIDs pass through unchanged.