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.