The output gallery is the unified review-and-derive surface that sits at the
end of every Isis generation. Once a workflow class has produced an image,
video, narration, music track, 3D mesh, caption-dub, or accessibility pass, the
artifact lands as an OutputRecord that the gallery can filter, trace through a
lineage tree, branch, replay-with-tweak, compare side-by-side, and bulk-act on.
It serves both the customer Studio (looking back over their own renders) and
operators (governing the full corpus). It is also the join point where the
generation pipeline meets the
Editorial Calendar and Asset & Media Library,
Living Scenes lineage, and the trust-&-safety
takedown machinery. The whole subsystem is real, pure-function, and unit-tested
in libs/isis/output-gallery/ against §24.10.
Where this sits in V1#
The output gallery is the consumer-facing tail of the same release-gated
pipeline described in Isis Generation Control.
Isis dispatches a guarded generation; the live provider (Stability SD3.5,
ElevenLabs, Suno, fal.ai-hosted LTX-Video; see
External Model Intelligence and Execution Providers
and
Creator Surfaces, Voice, Music, and 3D Generation)
returns bytes; a provenance bundle and a cost-ledger entry are stamped; and the
result becomes an OutputRecord the gallery surfaces. Which gallery a customer
sees, and what actions are available, are governed by the same four-tier
resolver as the rest of generation — see
Generation Audience Tiers and Surface Boundaries.
The dedicated operator gallery is itself one of the 28 named surfaces
(output-gallery-admin), reachable only by the operator-admin tier.
Everything below is implemented in @isis/output-gallery
(libs/isis/output-gallery/src/), whose package description is verbatim
"Unified output gallery: filters, lineage, branch/replay, compare grid, bulk
actions (§24.10)". The library's public surface is exactly five modules:
| Module file | Exports (real symbols) | Responsibility |
|---|---|---|
output-record.ts |
OutputRecord, OutputDomain, OutputKind, OUTPUT_DOMAINS, OUTPUT_KINDS, GalleryFilter, GalleryQuery, GalleryPage, applyGalleryFilter |
The faceted record + pure filter/pagination function |
lineage.ts |
OutputLineage, LineageEdge, LineageEdgeKind, LineageTreeError |
Per-output upward/downward edge graph + cascade walk |
branch-replay.ts |
planBranch, validateBranchDelta, ParameterAllowedRange, BranchDelta, OriginalInputs, BranchRunRequest, BranchValidationError |
Range-bounded branch/replay derivation |
compare-grid.ts |
buildCompareGrid, DiffMetric, CompareSlot, CompareGridLayout, CompareGridError |
2-up / 4-up / n-up compare with per-kind diff metrics |
bulk-actions.ts |
BulkAction, BulkActionInvocation, validateBulkAction, recordBulkAction, undoBulkAction, AuthorizationCheck, BulkActionError |
Operator-audited, undoable selection actions |
The pure-function design is deliberate: the filter, the lineage walk, the branch planner, and the bulk-action validators take their inputs and return their outputs with no I/O, so they unit-test deterministically and can run identically in the BFF, the admin service, and the client.
The OutputRecord — one faceted row per render#
Every artifact the gallery surfaces is an OutputRecord (output-record.ts).
Its fields are the filter facets — the gallery filter UI exposes exactly the
fields the record carries, so there is no drift between "what you can filter on"
and "what is stored." The schema:
interface OutputRecord {
outputId: string;
tenantId: string;
userId: string;
domain: OutputDomain; // 'tara' | 'veritas' | 'nyx' | 'nisaba' | 'metis' | 'arete' | 'sophia'
personaId: string | null;
workflowClassId: string;
kind: OutputKind; // see below
model: string;
generatorKey: string;
locale: string;
entitlementTier:
| 'contemplative'
| 'curated-creator'
| 'aaa-creator'
| 'operator-admin';
licenseTag: string;
producedAtUnixSeconds: number;
takenDownAtUnixSeconds: number | null;
assetUrl: string;
thumbnailUrl: string;
costCents: number;
tags: readonly string[];
}
Two enumerations are frozen constants the rest of the codebase keys off:
OUTPUT_DOMAINS— the seven generating domains:tara,veritas,nyx,nisaba,metis,arete,sophia. (These mirror the V1 domains documented in domain-tara.md, domain-veritas.md, domain-nyx.md, domain-nisaba.md, domain-metis.md, domain-arete.md, and Sophia Grounding.)OUTPUT_KINDS— the seven artifact kinds:image,video,audio-narration,audio-music,mesh-3d,caption-dub,accessibility-pass. These map one-to-one onto the BFF generation executors (apps/oshun/bff/src/generation/:image,narration,music,video,caption-dub,accessibility-pass, plusexplainer,sky-briefing,curated-generation, andrealtime-music).
Note that entitlementTier on the record is the code-canonical tier
vocabulary — contemplative, not "Customer"; operator-admin, not "Operator".
The earlier prose in ../features.md that called the four tiers
"Customer, Curated-Creator, AAA-Creator, Operator … used verbatim" is
inaccurate against the union in libs/isis/entitlements/src/generation-tier.ts;
the authoritative names are those carried by the record. See
Generation Audience Tiers and Surface Boundaries
for the full reconciliation.
Filtering — a pure function over records#
applyGalleryFilter({ records, query }) is the entire query engine. A
GalleryFilter is an AND of optional facet predicates; every present facet must
match for a record to pass. The facets:
| Facet | Semantics |
|---|---|
domain |
record domain ∈ list |
personaId |
record persona is non-null and ∈ list |
workflowClassId |
∈ list |
kind |
∈ list |
licenseTag |
∈ list |
generatorKey |
∈ list |
model |
∈ list |
locale |
∈ list |
tenantId |
∈ list |
entitlementTier |
∈ list |
tag |
record tags ⊇ all listed tags (every-of, not any-of) |
freshnessWindowSeconds |
producedAtUnixSeconds ≥ now − seconds |
excludeTakenDown |
drops records whose takenDownAtUnixSeconds is set |
The matcher is strict on a couple of edge cases worth calling out. A personaId
filter excludes records whose persona is null (a persona-scoped view never
accidentally shows persona-less renders). The tag facet uses every —
supplying ['retro', 'poster'] returns only records carrying both tags, which
is what an operator narrowing a corpus actually wants, not an any-of union.
Freshness is evaluated against a caller-supplied nowUnixSeconds, which keeps
the function deterministic and testable rather than reading a wall clock
internally.
Results are sorted newest-first by producedAtUnixSeconds, with outputId
breaking ties (so pagination is stable across calls), and paginated with an
opaque integer cursor returned as nextCursor (null when exhausted). The
GalleryPage also carries the full total match count for the filtered set.
Lineage — the per-output edge graph#
OutputLineage (lineage.ts) is a directed graph over output IDs that records
how a render relates to the rest of the corpus. Each output edges upward
to the run/prompt/workflow class that produced it and downward to the
derivative artifacts that consume it. Edges are typed by LineageEdgeKind:
LineageEdgeKind |
Meaning |
|---|---|
derived-from |
a branch/replay child of a parent output |
cited-by-claim |
the output backs a Veritas claim |
used-in-lesson |
the output appears in a Metis lesson |
embedded-in-passage |
the output is embedded in a Nisaba passage |
used-in-scene |
the output is referenced by a Living Scene score |
send-to-editorial |
the output was routed into the editorial queue |
The graph maintains both outgoing and incoming adjacency, so parentsOf and
childrenOf are O(1) lookups. Three integrity properties are enforced at
addEdge time, each raising a typed LineageTreeError:
- Both endpoints must be registered (
unknown-output). You cannot edge to or from an output the lineage has not seen viaregisterOutput. - No cycles (
cycle-detected).wouldCreateCyclewalks the outgoing graph from the proposed target looking for the source before adding; a self-edge is rejected immediately. This guarantees the lineage is always a DAG, which is what makes the descendant walk terminate. - No duplicate edges of the same kind (
duplicate-edge). The samefrom → topair may carry two different kinds (an output can be bothcited-by-claimandused-in-lesson), but not the same kind twice.
descendantsOf(outputId) does an iterative depth-first walk over outgoing edges
and returns every reachable output ID. This is the primitive that powers
takedown cascade: when an output is taken down, the operator surface walks
its descendants so that lessons, claim citations, passage embeddings, and scenes
built on top of it are surfaced for the cascade decision. Because the graph is a
proven DAG, the walk is bounded and the seen set prevents re-visits.
Branch and replay-with-tweak — range-bounded derivation#
A branch derives a new run from an existing output by applying an explicit
parameter delta; replay-with-tweak is the same operation seeded with the
user's original inputs as the starting point. Both go through
branch-replay.ts, and the load-bearing fact the prose elsewhere underplays is
that the delta is range-validated, not free-form.
validateBranchDelta({ delta, allowed }) checks every override in the
BranchDelta.parameterOverrides map against the workflow class's declared
ParameterAllowedRange[]. A range is either a number-range (with min/max)
or an enum (with enumOptions). Validation raises a typed
BranchValidationError with one of three codes:
unknown-parameter— the override names a parameter not on the workflow class's allowed list. You cannot smuggle in a parameter the class never declared.wrong-type— anumber-rangeparameter was given a non-number.out-of-range— a numeric value outside[min, max], or an enum value not inenumOptions.
This is the mechanism behind the docs' phrase "targeted parameter override
within allowed range": the workflow class, not the customer, decides which knobs
are tweakable and how far. A contemplative-tier curated surface can expose a
narrow override set; an aaa-creator graph-editor class can declare a wide one
— the validator is the same code path either way, so the guarantee holds across
tiers.
planBranch({ original, delta, allowed, costPerParameterOverrideCents })
validates first, then merges the overrides over the original parameters and
returns a BranchRunRequest:
interface BranchRunRequest {
parentOutputId: string; // becomes the 'derived-from' lineage parent
inputs: Record<string, number | string>; // original ⊕ overrides
workflowClassId: string; // inherited from the original — branch stays in-class
estimatedCostCents: number; // overrideCount × costPerParameterOverrideCents, floored at 0
}
Two design choices fall out of this. First, workflowClassId is copied straight
from the original, so a branch can never escape the workflow class that produced
its parent — derivation stays "within workflow-class bounds" as the spec
requires. Second, the parentOutputId is exactly what the caller turns into a
derived-from lineage edge once the branch run completes, which is how
replay-with-tweak "appends to the lineage tree." Determinism within a seed (the
spec's branch/replay test target) follows from the merge being a pure override
of the original inputs — same original and same delta → same merged inputs →
same downstream render under a fixed seed.
Compare grid — typed diff metrics per asset class#
buildCompareGrid({ records, layout, diffMetrics }) (compare-grid.ts)
assembles a CompareGridLayout of 2-up, 4-up, or n-up slots and attaches
a typed DiffMetric to each pairwise comparison the caller supplies. The metric
union is asset-class-specific, which is the real contract behind the prose "diff
overlays per asset class":
DiffMetric.kind |
Payload | Asset class |
|---|---|---|
pixel-delta |
meanAbsoluteDelta |
image |
frame-delta |
meanAbsoluteDelta |
video (per-frame) |
audio-rms-delta |
rmsDeltaDb |
audio loudness |
waveform-delta |
correlationCoefficient |
audio waveform |
mesh-vertex-delta |
hausdorffDistance |
3D mesh |
The builder enforces several invariants, each a typed CompareGridError:
- At least two records and exact slot counts for fixed layouts:
2-uprequires exactly 2 records,4-upexactly 4 (invalid-layout). - n-up is capped at 16 slots (
MAX_N_UP_SLOTS = 16,too-many-slots) — a hard upper bound so a compare view can't be asked to diff an unbounded set. - Kinds must be compare-compatible (
mixed-kinds). Same kind is always compatible; the one cross-kind exception isaudio-narration↔audio-music, which share a playback kind and are meaningfully comparable by waveform (e.g. an operator cross-checking a narration against a music bed). Comparing an image against a mesh is rejected.
Diff pairs are built for every (i, j) slot combination, keyed
"<fromOutputId>::<toOutputId>" against the supplied diffMetrics map; a pair
only appears in the layout if the caller actually provided a metric for it. The
metrics themselves are computed upstream (by the asset-comparison machinery) and
handed in — the grid model is the assembly-and-validation layer, not the
pixel/waveform math.
Bulk actions — operator-audited and undoable#
bulk-actions.ts covers selection-scoped operations: re-tag,
send-to-editorial, revoke-consent, and takedown. Each is a discriminated
BulkAction:
type BulkAction =
| { kind: 're-tag'; addTags: string[]; removeTags: string[] }
| { kind: 'send-to-editorial'; queueId: string }
| { kind: 'revoke-consent'; consentRecordId: string }
| { kind: 'takedown'; reason: string };
validateBulkAction enforces both shape and tier authorization via an
AuthorizationCheck { operatorId, tier }:
| Action | Required tier | Extra validation |
|---|---|---|
re-tag |
curated-creator, aaa-creator, or operator-admin |
non-empty add or remove set |
send-to-editorial |
curated-creator, aaa-creator, or operator-admin |
non-empty queueId |
revoke-consent |
operator-admin only |
non-empty consentRecordId |
takedown |
operator-admin only |
non-empty trimmed reason |
The two destructive, rights-bearing actions — revoking a consent record and
taking content down — are locked to operator-admin. A non-empty operatorId
is required on every action for the audit trail. recordBulkAction additionally
asserts the recorded operatorId matches the one in the AuthorizationCheck
(unauthorized if it doesn't), so the audited actor cannot be spoofed to differ
from the authorized one.
recordBulkAction produces a BulkActionInvocation stamped with
performedAtUnixSeconds, the affected outputIds, and an undoWindowSeconds
that defaults to 5 minutes (5 * 60). undoBulkAction flips undone and
sets undoneAtUnixSeconds if called within the window; past the window, it
raises undo-window-elapsed. Re-undoing an already-undone invocation is
idempotent (returns it unchanged). This is the concrete "undo window" the spec
calls for — bounded, audited, and reversible.
Provenance-bundle inspector#
Every output the gallery surfaces is backed by a CanonicalProvenanceBundle
stamped at dispatch time by the Isis control plane — consent ID, prompt, model,
watermark, timestamp, invoking user, tenant, workflow class, and the cost-ledger
entry that mirrors the record's costCents. The gallery's provenance inspector
renders that bundle for any output; the schema and the fail-closed dispatch seam
that produces it live in
Isis Generation Control. The music/audio path
additionally carries a watermark and provenance contract from
@isis/music-generation ("provider abstraction, workflow classes, watermark +
provenance (§24.8)"), whose guardrails.ts constrains per-class
allowedMimeTypes to audio/wav, audio/mpeg, audio/ogg, and audio/flac —
see
Creator Surfaces, Voice, Music, and 3D Generation.
Living Scenes lineage extension#
Kept Living Offerings and shared scenes are first-class lineage nodes: the
used-in-scene edge kind ties an output into a scene score, and
attribution-policy edges travel with reshares. Replay-with-tweak from a public
artifact produces correctly attributed children (the branch's parentOutputId
becomes the derived-from edge), branch-from-this-moment forks a new score,
and reshare cannot escalate scope. The full attribution model is specified in
Keep, Share, Shareability, Takedown, and Lineage
under "Reshare, Lineage, and Remix Attribution."
Who sees which gallery, and the AAA execution substrate#
The customer Studio gallery is exposed under apps/oshun/web/src/app/studio/
alongside the generation and generation-gallery surfaces. The legacy
studio/isis/* segments are now 404-hard-blocked (the studio-boundary
allowlist was emptied). The older description that "AAA-tier routes return a
disclosure + Yemaya signup gate" is therefore partly stale — the legacy isis
segments hard-block rather than serving a CTA. The full AAA creator workspace
lives in the separate apps/yemaya/studio-web and apps/yemaya/studio-desktop
apps; that is where heavyweight derivation runs. studio-web imports
@oshun/render-farm (libs/oshun/render-farm) — the render-farm scheduler that
the gallery's branch and bulk re-render flows feed into. The render farm
provides priority queues, worker-node capability matching, GPU requirement
matching, dependency execution, preemption, checkpoint/resume, cloud-burst, and
cost estimation. It exports RenderJob, RenderAssignment, RenderCheckpoint,
RenderCloudBurstPlan, RenderCostEstimate,
RenderGpuCapability/RenderGpuRequirement, PreemptionDecision,
RenderDashboardSnapshot, and RenderQuotaBreach. It is the AAA-tier execution
scheduler the docs gesture at; the gallery is the review surface over its
outputs.
The dedicated operator gallery (output-gallery-admin) is one of the 28 named
GenerationSurface values, on the operator-admin allowlist only. The
bulk-actions tier checks above are the gallery-local enforcement of that same
boundary — the operator-only destructive actions can only fire from a surface
the operator tier can reach in the first place.
Tests and verification#
output-gallery.test.ts covers each module against §24.10 with
correctness-asserting cases (not shape-only checks): filter-by-domain+class+tag,
taken-down exclusion, freshness windows, newest-first cursor pagination; lineage
cycle refusal, duplicate-edge refusal, descendantsOf reachability, and
parentsOf incoming edges (the takedown-cascade and lineage-integrity targets);
branch rejection of unknown parameters / out-of-range numbers / bad enum values
plus a merged-inputs-with-cost-estimate happy path (the branch/replay
determinism target); bulk-action empty-set / non-admin-revoke / missing-reason /
operator-mismatch rejections and the re-tag undo-within-window vs
undo-after-window pair; and compare-grid 2-up construction, wrong-slot-count
rejection, mixed-kind (image vs mesh) rejection, the narration↔music compare
exception, and the 16-slot n-up cap. These map directly onto the spec's listed
test targets — lineage chain integrity, branch/replay determinism within seed,
takedown cascade reach, provenance display completeness, filter correctness, and
bulk-action authorization.
Honest status#
The gallery model — records, filtering, lineage DAG, branch/replay validation,
compare grid, and bulk actions — is real, in-repo, and unit-tested as pure
functions. What it surfaces depends on the upstream live providers, which are
env-gated and fail-closed by design: with no provider key, the Isis resolver
returns null and the job fails closed with provider_not_configured, so there
is simply no OutputRecord to gallery. See
Isis Generation Control and
External Model Intelligence and Execution Providers
for the OSHUN_STABILITY_*, OSHUN_ELEVENLABS_*, OSHUN_SUNO_*, and fal.ai
deploy-time toggles. A fully rendered AAA Yemaya Studio graph-editor UI driving
the apps/yemaya/studio-{web,desktop} apps over @oshun/render-farm is present
in the tree but not verified end-to-end here. The provenance inspector and
watermark contracts are real schemas; their population assumes a live dispatch.
Related#
- Isis Generation Control
- Generation Audience Tiers and Surface Boundaries
- Creator Surfaces, Voice, Music, and 3D Generation
- External Model Intelligence and Execution Providers
- Editorial Calendar and Asset & Media Library
- Keep, Share, Shareability, Takedown, and Lineage
- Review, Compliance, and Trust & Safety
- Subsystem Glossary
- Hub: ../features.md