Format reference
Every file a package can carry, and every field in it, as the engine's reader parses it. Where a specification document describes a different shape, the shape below is the one that installs.
Conventionsnot documented marks a field that appears in real packages but whose consumer was not found in the source this reference was written from. An optional field has a defined default; an undocumented one has an unknown reader. Both are listed — only the first is safe to depend on.
manifest.yaml
Identity.
Required on every package, at the zip root. The engine's ReadPackage treats id as the only hard requirement — a manifest without one is an error and the package is not installed. Every other field parses to its zero value when absent.
Fields are grouped by who reads them.
Read by the engine
| Field | Req. | Default | What it does |
|---|---|---|---|
| id | yes | — | The package's permanent identifier. Lowercase, hyphens. Everything downstream — CLI, tool names, the index, install directories — keys off this and never off name. |
| name | no | "" | The display label ("Whisper Tiny"). Changeable at any time; nothing internal depends on it. |
| version | no | "" | The package's own semver, bumped on every republish. Distinct from naming.version. |
| type | no | "" | model_pipeline | application. Normalised on read — legacy model_pipeline and model both mean pipeline. extension is reserved and appears only in engine test fixtures. Not validated against an enum: a typo passes silently. |
| runtime | no | "" | venv | container. IsVenv() routes a venv package to the engine's own installer; anything else is routed elsewhere. No shipped pipeline declares it. |
| gpu | no | "" | required | optional | none. none — and absent — marks the model CPU-only, which exempts it from VRAM gating and from eviction. |
| min_vram_gb | no | 0 | Minimum graphics memory in GB. Falls back to ui.hardware.min_vram_gb when the top-level field is 0, so a manifest declaring it only under ui: still classifies correctly. |
| recommended_vram_gb | no | 0 | The comfortable amount the resource manager prefers for a full-GPU fit. |
| min_ram_gb | no | 0 | Minimum system RAM, for CPU models. In the spec and in the struct; no shipped manifest declares it. |
| capabilities | no | [] | What this package can do. Each entry is {id, modality}, both strings. The coarse list the engine and marketplace filter on — not the detailed contract, which is ui.capabilities. |
| description | no | "" | One-line description for listings. |
| license | no | "" | A bare SPDX identifier (MIT, Apache-2.0). An object here yields "" — that shape belonged to older v2.0 packages, whose licence travelled inside the UI contract. |
| min_os_version | no | "" | The lowest Hutash OS version this package supports. The engine relays it and never compares it. Absent means no floor, never unknown: a consumer must read empty as compatible. Install is never blocked on it; a consumer above the floor shows a warning and installs anyway. Quote it — unquoted YAML reads 1.0 as a float. |
| source | no | absent | {repo, commit}. For application packages the field the installer actually uses lives in application/config/app.yaml and has a different shape. |
| ui | no | absent | The host-integration contract. Carried by the engine as an opaque tree; it writes it back out as application/manifest.json at install and never interprets it. |
Read by the index builder
Read by build_index.py when the catalogue is published.
| Field | Default | What it does |
|---|---|---|
| metadata.weight_category | — | The weights subfolder, and the index's coarse category for a pipeline (audio, LLM, …). Falls back to the first capability's modality, then to "pipeline". |
| metadata.category | "app" | The index's category for an application (photo, creative, …). Preferred over the top-level category, which is usually the literal string "app" and useless for grouping. |
| metadata.quality_score | null | Published as quality_score, rendered as a 0–10 star score after backend rescaling. |
| metadata.speed | null | Published verbatim as the index's speed. |
| metadata.description | — | Fallback when the top-level description is empty. |
| metadata.internal | false | Gates the package out of user-facing lists. prompt-engine uses it. |
| low_vram_capable | omitted | Whether an oversized model can be split across GPU and RAM. Emitted only when declared — for a CPU-only model the question is inapplicable, not false. |
| language | omitted | The model-level primary language, short code. Emitted only when declared. Distinct from the per-option language under ui:, which narrows a picker. |
| languages | omitted | Every language this build can actually handle. Emitted only when declared. |
Present in shipped manifests, read by neither
Each of these appears in real packages. No reader for them was found — they are documentation, provenance, or consumed somewhere outside the source this reference was written from.
| Field | Consumer | What it appears to do |
|---|---|---|
| hutash_format | not documented | Format-generation stamp. Every shipped pipeline says '1.0'. |
| naming | not documented | The structured metadata id is generated from — see below. |
| platforms | not documented | [windows, linux] on every shipped pipeline. |
| category | not documented | On applications, usually the literal "app". |
| model_shelf | not documented | shared on both shipped applications — they read the one shared model store rather than keeping their own. |
| capabilities_needed | Verticals | Which engine capabilities the app requires (stt, llm, translate). The first-launch setup gate derives what it demands from this list, and the auto-generated Models page filters the marketplace to it. |
| metadata.homepage | not documented | Project URL for the store listing. |
| metadata.shelf_warning | not documented | Present on hutash-studio.hutash as null. Purpose unknown. |
| permissions | Reserved | Not read by the engine or Studio, and absent from the engine's source entirely. If present it must be []; a non-empty list has no defined behaviour. |
manifest.yaml
The naming: block.
Nested under naming: rather than top-level, because manifest.yaml already has its own version: — the package's semver — and these are different numbers.
| Field | What it does |
|---|---|
| family | Groups variants of one model together (whisper, kokoro, gemma). Lowercase. |
| version | The model maker's release number, not the package's. Unchanged when the package is merely rebuilt or re-quantized. |
| variant | Size tier or special edition (tiny, edge). |
| parameters | Parameter count (82m, 1.7b, 4.5b). |
| quantization | Compression type (q4_K_M, q8_0). |
| format | Runtime format: gguf | pytorch | onnx | ctranslate2. |
The rule the block describes: {family}[-{version}][-{variant}][-{parameters}][-{quantization}], joined by hyphens, skipping nulls, all lowercase, never a doubled, leading or trailing hyphen. architecture (dense/moe) and active_parameters (MoE only) are optional catalogue-display fields that describe the model rather than generating its id.
In practice an id is assigned once and never changes. whisper-tiny declares parameters: 39m and kokoro declares version: "1.0" and parameters: 82m, yet both ids are short — both predate the block. Read naming: as documentation of a package, not as the thing that derives its id.
manifest.yaml → ui:
The pipeline UI contract.
Required on a model_pipeline, and not read by the engine for an application. This is what Studio renders as the model's form. The engine carries it opaquely and materialises it at install time as application/manifest.json, which is what the running model server reads and serves at GET /manifest.
Two things to know first. The engine layers identity onto the block on the way out: model_id is always forced to the package id, and display_name, version, description and license are filled in from the manifest's top level only where ui: has not set them. And the block is generated — its authoring source is model.meta.yaml, from which manifest.json is produced. The field names below are the generated ones, which is what a .hutash actually carries.
Model-level fields
| Field | Req. | What it does |
|---|---|---|
| model_id | yes | The package id. Forced by the engine to match; a mismatch cannot survive. |
| display_name | yes | Rendered as the panel-header eyebrow (KOKORO, ACE-STEP). Falls back to model_id. |
| tagline | no | One-line marketing line for the marketplace card. |
| description | no | Multi-line description for the marketplace detail card. |
| license.spdx | no | SPDX identifier. Drives the licence tier badge. |
| license.url | no | Link to the upstream licence text. |
| license.commercial_ok | no | Whether commercial use is permitted. |
| license.attribution_required | no | Whether attribution must be shown. |
| license.attribution_text | no | The exact attribution to display when required. null by default. |
| creator.name / .url | no | Who made the model, and their site. |
| source.upstream_repo | no | The model's own code repository. |
| source.huggingface_repo | no | The HuggingFace repo the weights come from. |
| hardware.min_vram_gb | no | Graphics-memory floor. The engine falls back to this when the top-level min_vram_gb is 0. |
| hardware.recommended_vram_gb | no | Comfortable graphics memory. |
| hardware.supports_cpu | no | Whether the model runs without a GPU at all. |
| hardware.total_install_gb | no | Total disk the install consumes, weights included. |
| capabilities | yes | At least one, keyed by capability id. |
| improve | no | Prompt-improvement strategies. null hides the Improve button; absent falls back to a generic instruction. |
| labels | no | Free-form build provenance (hutash.model.hf_repo, hutash.runtime.backend, hutash.build.date, …). Informational. |
| notes | no | Author's notes about the package. Informational. |
| api.health | no | The model server's health path. Informational — the real health path is launch.yaml's health_endpoint. |
| api.gpu | no | Whether this build uses the GPU. Informational. |
| layout | no | Which Studio page layout to mount. aggregator-3panel on every shipped pipeline. Validated backend-side against a closed list — not documented which values that list holds. |
| image | no | {registry, name, tag, port} — the Docker image, from the container era. In the authoring schema; no shipped package carries it, because pipelines install into a Python virtual environment rather than pull an image. |
Capabilities
ui.capabilities.<id>. The dict key becomes the modality the backend sees and the URL fragment the asset library uses. Studio's linter restricts it to tts, voice, music, stt, clone, voice-clone, sfx; adding another is a coordinated change across the modality map, the linter's valid-id list, the feature loader and the asset library's modality tokens.
| Field | Req. | What it does |
|---|---|---|
| label | yes | The capability's user-facing name ("Text to Speech", "Transcribe"). |
| description | no | One line describing what this capability does. |
| primary | no | The capability the model's page mounts by default. Default false. |
| endpoint | no | The HTTP path on the model server this capability is served at (/tts, /stt). |
| status_message | no | Text on the in-progress card while this capability runs. Defaults to "Generating…". |
| modality | no | Per-capability modality override for the asset-buffer and indexer paths. Defaults to the capability id. Chatterbox's voice-clone sets modality: clone for wire compatibility. |
| timeout_seconds | no | The model's typical-runtime ceiling. Informational only — it was once a hard read timeout, removed on the reasoning that the model is the authority on whether it is stuck, not a timer. |
| prompt_template | no | Template for the asset's display-name fallback when the user submits without typing a prompt. {filename} is substituted at the route layer; "Transcription of {filename}" is the canonical case. |
| inputs | see note | The artefacts the user supplies. |
| controls | see note | The dials the user turns. |
| outputs | no | What the model returns. |
A capability must declare at least one input or one control; neither block is individually required.
Inputs
ui.capabilities.<id>.inputs.<input_id>. An input is the artefact the model operates on. The key becomes the form field name, the API request parameter, and the column in the asset's parameters JSON. Four types are valid; anything else is rejected by the linter, because no registered component means no rendering.
| type | Aliases | What it renders |
|---|---|---|
| string | text | A free-text field — the prompt textarea most generators use. |
| audio_file | audio | A drop-zone that takes an audio file. |
| image_file | image | A drop-zone that takes an image. |
| text_file | — | A drop-zone that takes a text file (a script, a subtitle). |
| Field | Req. | What it does |
|---|---|---|
| type | yes | One of the four above. |
| label | yes | The visible field label. Must be unique across every capability of every model — duplicate labels make automated tests ambiguous. |
| required | no | Whether the form blocks submission without it. Default true. |
| description | no | Help text under the label. |
| placeholder | no | Greyed hint inside an empty text field. |
| max_length | no | Character cap, string inputs. |
| accept | file inputs | Allowed MIME globs and extensions. A file-type input must list at least one MIME glob (audio/*) and at least one extension (wav or .wav): drag-time matching needs the glob, because browsers hide filenames during dragenter, and drop-time matching needs the extension. |
| clear_after_generate | no | true resets the field to its schema default after a successful run — per-job content. false keeps the value — a workspace setting. Default false. |
| role | no | Present on Kokoro's prompt as role: prompt. Marks the field as the model's prompt for prompt-oriented features. not documented beyond that. |
| accept_files | no | Extensions the text field will also accept as a dropped file (txt, srt, md), so a prompt box doubles as a file drop. |
| accept_files_hint | no | The hint shown beside the field for that ("or drop a .txt / .srt file"). |
| auto_fill | no | {via: stt} marks the input as auto-fillable by transcribing another input. Studio's transcriber service reads it and runs whisper-tiny behind the scenes. First consumer: dia-1.6b's voice_ref_transcript, filled from the uploaded voice clip. |
Controls
ui.capabilities.<id>.controls.<control_id>. A control is a dial, not an artefact. Every control needs a type, a label and a default — use null for "no default".
Two vocabularies are in play. The specification names five closed semantic types: number, enum, string, boolean, file. Studio's runtime registry — what the linter enforces — is fifteen names: dropdown, select, text, text_input, string, textarea, slider, number, toggle, boolean, multi_select, file, file_upload, audio_file, image_file. Shipped packages use the registry — whisper-tiny writes type: enum and type: boolean, which resolve through the migration table (select↔enum, toggle↔boolean). Author against the registry and the migration table together; the linter is what will actually stop you.
| Shared field | Req. | What it does |
|---|---|---|
| type | yes | One of the registry types. |
| label | yes | The visible label. Must be unique across every capability of every model. |
| default | yes | The starting value; null means no default. The linter checks the default's type against the control's type, and that a slider's default is inside [min, max]. |
| description | no | Help text under the label. |
| advanced | no | true puts the control in the collapsible Advanced section. |
| null_label | no | The label shown when the value equals a sentinel default — seed: -1 rendering as "Random". |
| implementation_status | no | active | stub | deprecated. stub renders the control disabled with a "coming soon" cue. Default active. |
| clear_after_generate | no | true resets after a successful run; false persists, for a workspace setting tuned once. Default false. |
| Type | Its own fields |
|---|---|
| slider | min and max (both required), default inside that range, optional step and unit (a suffix shown next to the value). |
| select alias dropdown, semantic name enum | options (required) and a default equal to one of the option values. Three option shapes are accepted: bare strings, a mapping of value to label, or an object list. In the object form only value is load-bearing; every other field is open-ended metadata (label, accent, gender, quality, language). A consumer that does not recognise a metadata field ignores it and falls back to label, or to value where there is none — so a plainer interface still constructs a correct invocation. Kokoro's 28-voice picker is why this form exists. |
| text_input aliases string, text | default (use "" for empty), optional placeholder, and validation.min_length / .max_length / .pattern, all applied at submit. |
| textarea | The same fields as text_input, plus rows for the initial visible height. |
| toggle alias boolean | default, a boolean. |
| number | default (number or null), optional min, max, step, unit. Use it where the user benefits from typing an exact value and may legitimately want one outside any obvious bound; use slider for a bounded value tuned by feel. |
| multi_select | options in the same three shapes, a default list whose entries are all valid option values ([] for none), and optional min_selections / max_selections. |
| file_upload aliases file, audio_file, image_file | default must be null — files carry no serialisable default. accept is optional here and defaults to */*, unlike on the inputs side where it is required and shape-checked. max_size_mb is a soft cap that surfaces a warning rather than blocking. |
Outputs
ui.capabilities.<id>.outputs.<output_id>. Outputs declare what the model returns and which renderer draws it.
| YAML type | Becomes | Other fields |
|---|---|---|
| wav, mp3, flac, ogg | audio with that format | sample_rate |
| audio | audio, format optional | format, sample_rate |
| png, jpeg, jpg, webp | image with that format | — |
| image | image, format optional | format |
| text | text | format (e.g. srt, vtt) |
| json | json | schema_hint |
| number | number | unit |
There is no top-level outputs: key on the package. Where a generated file lands is decided at download time by allowlist inversion, not by declaration: the shell checks the extension against a small stable list of model-weight formats (.safetensors, .ckpt, .pt, .pth, .gguf, .onnx, .bin) and routes a match into the shared blob store; everything else falls through to the user's own app output folder. A new output format needs no manifest change.
ui.improve
Controls the Improve button in the prompt textarea, which routes the prompt through a language model.
| Field | What it does |
|---|---|
| default | The strategy id fired on a plain Improve click. Must be one of the declared strategy ids — the linter rejects a mismatch. |
| strategies | The available strategies. Must be non-empty when improve is present. |
| strategies[].id | Stable identifier sent as strategy_id. |
| strategies[].label | The label in the dropdown. |
| strategies[].instruction | The verbatim system prompt sent to the language model — what kind of improvement to make. |
One strategy renders a plain "Improve" button; more than one renders "Improve ▾" with the default on a plain click. improve: null hides the button entirely — Whisper does this, because transcription has no prompt to improve. Omitting the key falls back to a generic "improve grammar and clarity" instruction, so ship it explicitly.
application/packages.yaml
Dependencies.
Present for any runnable package. The engine, not the package, selects a hardware variant at install time.
| Field | Default | What it does |
|---|---|---|
| python | "" | The Python version for this package's virtual environment ('3.11', "3.12"). Quote it. |
| common | [] | Packages installed regardless of hardware. Ordinary requirement specifiers; a direct wheel URL is accepted. |
| variants | absent | Per-hardware alternatives, keyed gpu and cpu. Any other key is accepted but only used as a fallback. |
| variants.<key>.packages | [] | Packages installed only for that variant. |
| variants.<key>.indexes | [] | Extra pip index URLs needed to resolve them — the PyTorch CUDA index, a prebuilt-wheel index. |
| system_packages | [] | OS-level packages the build needs (build-essential, git, curl, espeak-ng). |
| requirements | "" | Path to a requirements.txt inside the package, installed with uv pip install -r. This is the shape the two verticals use; auto-defaulted to requirements.txt for a git source that declares none. |
| extra_requirements | [] | Additional requirement files or specifiers layered on top. |
| frozen | false | This list is a locked set to install with constraints, not requirements to resolve. Set on an external app whose list is a uv pip freeze snapshot captured against the pinned commit. |
| packages | [] | A flat dependency list, on the application side. Skipped entirely for a git source, whose own requirements file owns its dependencies. |
| preserve | [] | Paths inside an installed app that survive a reinstall — a user's custom nodes, extensions, outputs. |
How the variant is chosen. With both gpu and cpu declared, gpu wins only when the host has a GPU and its VRAM meets the package's min_vram_gb; a package declaring no minimum is satisfied by GPU presence alone. Otherwise cpu is selected — deliberately, so a model whose minimum exceeds the card's actual memory installs CPU dependencies instead of GPU ones that would fail at load. With only one variant declared, that one is used regardless of hardware. With none, only common is installed.
frozen: true is specific. It is the dependency-graph equivalent of pinning a commit: capture once at test time, install as a locked set, never re-resolve. Re-freezing happens only when testing a new pinned commit. It is meaningless for the other source types — a self-bootstrapping app resolves nothing of ours, a PyPI source resolves fresh by design, and a local source is hand-written code.
application/launch.yaml
Starting a pipeline.
Model pipelines only. No shipped application package carries one.
| Field | Default | What it does |
|---|---|---|
| command | "" | The executable to run. Every shipped pipeline uses uvicorn. |
| args | [] | Its arguments. {port} is substituted with the engine's pool assignment at launch. |
| env | {} | Environment variables for the model process. {model_dir} and {weights_dir} are substituted with the real paths. |
| port | 0 | A template anchor, not the port served on — the engine assigns from its own pool (49200–65535). Every shipped pipeline declares 8000. |
| health_endpoint | "" | The path polled to decide the model has started. /health on every shipped pipeline. |
| health_timeout | 0 | Seconds to wait for that. 120 on every shipped pipeline. |
The proven launch, identical in every shipped pipeline, runs the shared inference server rather than an entry point of the model's own.
Environment the shared server and inference.py read
| Variable | Set by | What it does |
|---|---|---|
| HUTASH_MODEL_ID | launch.yaml | Which model this process is. |
| HUTASH_MODEL_DIR | launch.yaml, templated | Where inference.py and manifest.json live. |
| HF_HUB_CACHE | launch.yaml, templated | Where the downloaded weights are mounted. |
| HF_HUB_OFFLINE | launch.yaml | "1" — never reach the network at load time. |
| HUTASH_HF_REVISION | launch.yaml | The exact weights commit. The resolver uses it to find the snapshot, and raises rather than falling back to "the only snapshot present", so a drifted mount fails loudly instead of loading the wrong weights. |
| HUTASH_DEVICE | the engine | cuda | cuda:N | auto | cpu | mps. |
| HUTASH_VRAM_BUDGET_MB | the engine | The VRAM the model may claim in Low VRAM mode. |
| HUTASH_MODEL_FRAMEWORK | the engine | huggingface | llama-cpp | faster-whisper | custom — the shared server translates the budget into that framework's own knob, so inference.py needs no Low-VRAM code. |
| HUTASH_GPU_LAYERS | the shared server | The llama.cpp layer count derived from the budget. |
| HUTASH_CONTEXT_LENGTH | the engine | Context window for language models: the Settings override when set, otherwise a tier from total VRAM (under 4 GB → 2048, 4–8 GB → 4096, 8 GB+ → 8192; unmeasurable → 4096). A manifest-declared value always wins. |
application/config/app.yaml
Starting an external application.
Applications only. Holds everything a pipeline splits across launch.yaml, plus the provenance a pipeline has no need for.
| Field | Req. | What it does |
|---|---|---|
| source.type | yes | git | self-bootstrap | pypi | local. |
| source.url | see note | The repository (for git / self-bootstrap) or the PyPI project page (informational, for pypi). |
| source.ref | no | Branch or tag. Informational only — never resolved. The commit is the lock. |
| source.commit | conditional | The exact commit to check out. Hard-required at parse time for git and self-bootstrap — the engine rejects those without one before any clone starts, because a ref alone is a floating pointer and two installs of "the same" entry would resolve differently on different days. |
| source.path | conditional | For local: the folder beside the .hutash file to copy from. Not hard-validated, so a wrong value fails at install rather than at parse. |
| source.bootstrap_cmd | no | self-bootstrap only — the app's own installer, which populates the shared virtual environment itself. Defaults to python launch.py --exit. |
| source.venv_dir_env_var | no | self-bootstrap only — the variable the app's installer reads to find that environment. Defaults to VENV_DIR. |
| entrypoint | yes | The command that starts the app. A literal port number here is a template anchor: the engine rewrites --port 8100 to --port {port} when it registers the app, and substitutes its pool assignment at launch. |
| workdir | no | The working directory to launch from. |
| ports | yes | The ports the app serves on. Real packages write - internal: 7860; a bare int, a string, or a list of ints also parse. The number must match the one in entrypoint — if they disagree the port rewrite never fires and every app tries to bind the same hardcoded port. |
| health.endpoint | no | The path polled to decide the app is up. / for a Gradio app, /health for a FastAPI one. Accepts a bare string in place of the mapping. |
| health.path | no | Accepted alias for endpoint. |
| health.start_period_seconds | no | The grace window before health checks start counting. 60 for an app whose first start loads a model. |
| health.interval / .timeout / .start_period / .retries | no | Further health-check tuning. Parsed, but not observed in any shipped package. |
| build | no | A post-clone step that runs before the app counts as installed — fetching weights the repo does not ship, or building a frontend. HivisionIDPhotos raises at startup with no weights present, so its build is what makes the install a working one. |
| env_vars | no | Environment variables for the app process. {HUTASH_BASE} is expanded to the shared data root. |
| env | no | The same idea under the process spec. Shipped packages use env_vars. |
| managed | no | Whether the engine owns this process's lifecycle — starts it, stops it, injects its environment. Default false. |
| backend | no | venv | container. Default venv. |
| system_packages | no | OS-level packages the install needs. |
| image / compute / mount_paths | no | Container-runtime fields in the parser. No shipped application declares them. |
application/config.yaml
Starting a vertical.
The two verticals carry this instead of config/app.yaml, and both files are identical.
| Field | What it does |
|---|---|
| port | Required. 0 asks the engine to assign one from its pool. |
| health | Required. The health path, as a bare string. |
| managed | Required. true — the engine owns the process and injects its environment, which is how HUTASH_OS_PATH reaches the vertical. |
| entrypoint | Required. The app's own module, relative to application/. |
resources/weights.yaml
What to download.
Model pipelines only. Not used by applications, which manage any downloads of their own.
| Field | Req. | What it does |
|---|---|---|
| sources | yes | The HuggingFace repositories to fetch. |
| sources[].repo | yes | The repo id (Systran/faster-whisper-tiny). An entry with no repo is skipped silently. |
| sources[].revision | no | The pinned commit. main is resolved to a commit hash at download time. |
| sources[].allow_patterns | no | Which files to fetch. Omit and the whole repo comes down — including demo audio, eval charts and documentation that is never read. |
| extra | no | Companion repositories, same entry shape. Appended to the same weights list. |
| download_size_gb | no | The total download, shown before the user commits to it. Published to the index as disk_size_gb. |
application/vertical.yaml
A whole interface as data.
Verticals only. This file is the frontend: the shared renderer reads it and draws navigation, page layouts, every widget, the Models marketplace and the Settings form. The vertical's own source holds a single <VerticalApp /> call and nothing else. A widget named here that the catalogue does not have renders an "unknown widget" note rather than silently nothing — which is the point: a vertical cannot quietly grow bespoke interface code.
Top level
| Key | Req. | What it does |
|---|---|---|
| app.title | no | The shell header title. A vertical declaring no app: block renders no title. |
| app.description | no | A one-line description beside it. |
| pages | no | The page-based interface. Absent falls back to the older single-workspace layout: block. |
| workflows | yes | The registry of what this app can run. |
| settings | yes | The settings schema, not its values. The Settings page is generated from exactly this. |
| layout | yes | The legacy single-workspace panel frame (type, left, center, right, each {content, width, label}). Superseded by pages:. |
| capabilities | no | Mirrors manifest.yaml's capabilities_needed; what the generated Models page filters to. |
| setup | no | The first-launch gate. Absent means no gate. |
| project.subfolders | no | Folders created inside every project. |
| project.transcript_path | no | The one file the editor auto-saves and restores, and where a run's primary subtitle output is written. Declared once so the two sides cannot drift to separate files. |
| project.working_file | no | The one file a project's whole run is recorded in. Empty for a vertical that declares none — which is what left Podcast's show notes and chapters on disk and out of reach until it was added. |
| ui_overrides | no | Accepted and otherwise ignored. The old slot-override mechanism was replaced by customWidgets, which overrides by widget type. The key stays so a parsed config still matches what the backend sends. |
A page
| Key | Default | What it does |
|---|---|---|
| id | required | The page's identifier; every to: in a navigate action resolves against it. |
| label | — | Its name in the navigation bar. |
| icon | — | Its glyph. |
| default | false | The landing page. |
| hidden | false | Keeps it out of the navigation while still reachable by navigating to it — which is how clicking a project opens the editor. |
| overlay | false | Opens on top of the current page, which stays mounted. Settings and Models are overlays so opening them from a half-edited transcript cannot unmount the editor and discard the edits. |
| overlay_size | md | md | lg. lg for panels whose cards need the room — the marketplace, the caption gallery. Ignored without overlay. |
| dialog | false | An overlay that draws its own content start to finish, with no title bar above it. The close button still renders. |
| reset_inputs | inferred | Whether opening this overlay clears the form. Unset, it is inferred from whether the page binds anything — right for a dialog collecting input for a new run, wrong for a panel over work already in progress. Set false on a panel whose page underneath still needs those inputs. |
| confirm_leave | — | The question asked before navigation away commits. |
| layout | single | single | two-panel | three-panel | editor. Anything else falls back to single. |
| left_width / right_width | — | Column width. A bare number is a design unit at a 16px root (240 → 15rem); a string passes through verbatim ("18rem", "65%"). |
| top_height / middle_height | — | The same, for the rows of an editor layout. |
| widgets | [] | What is on the page. |
| layout | Valid position values |
|---|---|
| single | main |
| two-panel | left, right, toolbar, middle |
| three-panel | left, center, right |
| editor | toolbar, top, middle, bottom |
A widget
| Key | Req. | What it does |
|---|---|---|
| type | yes | Selects the component from the catalogue. |
| position | no | The layout slot. |
| bind | no | Input widgets only: the workflow input id this widget fills. The value goes into shared form state, which is how action_button finds it with nothing wired between them. |
| props | no | The widget's own configuration. A prop the widget does not read is ignored — see the widget catalogue. |
confirm_leave behaviour. Exactly one dialog, and it never changes shape while it is up. Nothing unsaved means no dialog at all, because a prompt on a departure that loses nothing is one users learn to dismiss without reading. Unsaved work with a project open offers Save and leave / Leave without saving / Cancel, where Save and leave navigates only if the write succeeded. Unsaved work with no project offers the warning and Leave / Cancel — there is no third button because there is nowhere to put the work. The auto-save debounce is held while the dialog is open, and the dialog freezes its decision when it opens, so a write landing mid-decision cannot rewrite the question under the button the user was reaching for.
A settings field
| Key | Req. | What it does |
|---|---|---|
| type | yes | model_selector | dropdown | folder_picker are the types the two verticals use. The renderer falls back to a text input for an unrecognised type, on the reasoning that an unknown type is usually a typo and a visible field is a better signal than a missing one. |
| label | yes | The field label. |
| description | no | Help text under it. |
| capability | no | model_selector only — which capability's models to offer. |
| options | no | dropdown only — {value, label} entries, plus any other fields the source declared. |
| default | no | The initial value. |
Settings save per field. A single whole-form submit would send every field's last-rendered value, so editing one setting could quietly revert another.
A workflow registration, and the setup gate
| Key | What it does |
|---|---|
| workflows[].id | Required. How an action_button names it. |
| workflows[].file | Required. Path to the workflow YAML, relative to application/. |
| workflows[].label | Required. Its display name. |
| workflows[].icon | Required. Its glyph. |
| workflows[].description | Required. One line on what it does. |
| setup.required_capabilities[].capability | Required. The engine capability a model must declare. |
| setup.required_capabilities[].label | The human name. Defaults to the raw capability id. |
| setup.required_capabilities[].recommended_model | Offered first. Falls back to the smallest listed. |
| setup.required_capabilities[].optional | Offered but never blocking — renders "Optional — skip for now." |
vertical.yaml refines the gate; it never extends it. What the gate requires is derived from manifest.yaml's capabilities_needed, and an entry here for a capability the manifest does not declare is dropped rather than honoured — gating an app on something it never said it needed is a gate nobody can open. What counts as satisfied is at least one installed model declaring the capability: not a configured one, because settings can name a model since removed, and not a catalogue entry, because a thing you could install is not a thing you have. Two cases deliberately open the app rather than blocking it: no declared capabilities at all, and an unreachable engine.
application/workflows/*.yaml
What an app can do.
One file per workflow. The same YAML generates the web form, the CLI, and the tool surface — add an input here and it appears in all three.
| Key | What it does |
|---|---|
| id | Matches the id in vertical.yaml's workflows:. |
| name | Display name. |
| version | The workflow's own version. |
| inputs | What the run needs, keyed by input id. |
| steps | What it does, in order. |
| outputs | What it produces, keyed by output id. |
Inputs
| Key | Default | What it does |
|---|---|---|
| type | required | file | text | dropdown | slider | toggle | number | model_selector | folder_picker. |
| label | required | The field label. |
| required | true | Whether the run blocks without it. |
| default | null | The initial value. |
| options | null | dropdown only — {value, label} entries. |
| accept | null | file only — MIME globs and extensions. |
| capability | null | model_selector only. |
| min / max / step | null | slider and number. |
Steps
| Key | What it does |
|---|---|
| id | Required. How later steps refer to this one's result (${extract_audio.output}). |
| name | Required. What the progress display calls it. |
| type | Required. capability | ffmpeg | formatter | workflow | file_read | file_write. |
| everything else | Passed through to the step handler as config. Which keys matter depends on type — capability/model/input/params for a capability step, action/input/params for ffmpeg, format/input for a formatter, workflow/inputs for a sub-workflow. |
Values interpolate: ${inputs.<id>} reads a run input, ${settings.<id>} reads a settings value, ${<step_id>.output} reads an earlier step's result, and ${<step_id>.outputs.<id>} reads a named output of a sub-workflow step.
Outputs
| Key | Default | What it does |
|---|---|---|
| type | required | file | text | subtitle_editor. |
| source | required | The variable reference this output's value comes from. |
| label | required | Its display name. |
| formats | null | The formats it can be serialized to. |
| primary | false | This output is the workflow's deliverable — what an editor opens and an export writes when nothing names an output explicitly. At most one per workflow; none is fine for a single-output workflow. Without it a consumer can only guess by declaration order, which for transcribe-translate silently shows the untranslated text. |
| words_from | null | Another output of this workflow holding the same cues with their per-word timings. Set when this output is a rendered string, because SRT and VTT cannot express a word time — so animated caption styles would otherwise have nothing to animate. Declared rather than discovered: in a dubbing workflow, "whatever other output holds segments" would find the original transcript and animate it over the translated line. |
index.json
The catalogue.
One discovery surface over every package that exists. The engine fetches it once at startup and caches it in SQLite; every consumer filters the one cached copy by type rather than fetching its own. Studio filters pipeline; OS filters application.
The spec describes four fields per entry, on the principle that everything else lives in the package's own manifest. The shipped builder emits considerably more, and for a stated reason: a consumer building a full catalogue row from the index alone would otherwise have to fetch every package's zip, and the extra fields are read from the manifest the builder has already opened — so omitting them would not shrink the index, it would move the zip fetch downstream to break invisibly.
| Identity field | What it does |
|---|---|
| id | The package id, matching its own manifest. |
| type | pipeline | application | plugin. Plugins are future — no plugin package exists. This is what every consumer filters on. |
| category | A coarse grouping (audio, image, LLM, photo, …). For pipelines it doubles as the weights subfolder name. |
| hutash | Path to the package's .hutash file, relative to the repo root, resolved against wherever the index was fetched from. |
The derived display summary, also per entry: name, description, version, license, min_vram_gb, disk_size_gb, quality_score, speed, modality, hardware_label ("GPU (4GB+)" or "CPU", derived from gpu and min_vram_gb), internal, hf_repo, hf_revision, allow_patterns, weights_external. Four more appear only when the manifest declares them, because a default would be a claim: low_vram_capable, language, languages, min_os_version.
| Top-level block | What it holds |
|---|---|
| modalities | One entry per generation modality: {color, label, subfolder} — the identity a modality carries across the interface, and the asset folder its outputs land in. |
| features | The interface routing table: {id, name, description, modality, primary_model_id, icon, version, layout, accepts, permissions} — which primary model handles which generation feature. |
Both blocks ride alongside packages because they describe the generation system rather than any one package. The builder never regenerates their content; it carries them forward verbatim across a rebuild. Never hand-edit the packages list — run python scripts/build_index.py and --check it.