18 KiB
Narratio Architecture
1. Purpose
narratio is a Go orchestrator for D&D session processing. It runs a stage-based local pipeline from audio input through transcript processing and artifact generation, with manifest-based skip/force/resume behavior.
Narratio integrates with Scriptorium through the public CLI (scriptorium run and scriptorium render) via synchronous subprocess execution.
2. Current Status
Implemented:
- strict
pipeline.yml+session.ymlloading with strict YAML field checking (KnownFields(true)) - local workspace/session layout, lock file handling, artifact path helpers, checksums, and atomic writes
- manifest store and stage status transitions for resumable runs
- real
prepare,transcribe,merge, andpolishstages - real WhisperX HTTP adapter
- real Seriatim subprocess adapter
- real Audita subprocess adapter
- real Scriptorium subprocess adapter
- real
normalizestage producingtranscripts/normalized.json - real
trimstage producingtranscripts/trimmed.json - real
analyzestage for initialsession_recapgeneration - optional Scriptorium render diagnostics (
render_debug) before production run - storage/archive configuration and validation foundations for:
pipeline.storage.s3pipeline.spoolpipeline.archivepromotion rulessession.inputs.audio_s3
- run identity and path-model foundations:
- run ID generation (
YYYYMMDDTHHMMSSZ-xxxxxxxx) - S3 session/run/current key builders
- campaign/session/run local work/spool path helpers
- manifest run/path identity fields (
campaign,run_id, local and S3 prefixes)
- run ID generation (
- remote storage backend layer:
- narrow object-store interface (
List,Download,Upload,Exists) - fake storage backend for deterministic tests (no network dependency)
- S3-compatible backend using AWS SDK v2
- config-based object-store construction helper
- narrow object-store interface (
Still placeholder/future:
archivestage behavior- prepare-stage S3 audio download behavior
- archive-stage S3 upload/promotion behavior
notifystage behavior- additional Scriptorium artifact types beyond
session_recap - artifact-to-artifact workflows beyond the initial single-artifact implementation
- generic stale detection based on input/config checksums
3. Pipeline and Stage Boundaries
Canonical stage order:
preparetranscribemergepolishnormalizetrimanalyzearchivenotify
Boundary rules:
- orchestration logic lives in
internal/app - stage business logic lives in
internal/stage - external-tool CLI construction lives in adapter packages
- Scriptorium CLI details stay in
internal/adapters/scriptorium
4. Scriptorium Integration Model
Integration mode:
- public CLI subprocesses only (no Scriptorium internal Go packages, no HTTP API)
- production generation uses
scriptorium run - diagnostics/testing render uses
scriptorium render --format json
Run invocation shape used by adapter:
scriptorium run --prompt <prompt_id> --input name=path --out <output_path>
Optional flags passed when configured:
--config <path>--profile <profile_id>- repeated
--var name=value - repeated
--input name=path --timeout <duration>--api-key-env <ENV_NAME>when configured
Render invocation shape used by adapter:
scriptorium render --prompt <prompt_id> --input name=path --format json --out <render_output_path>
Adapter behavior:
- always passes
--out - captures stdout/stderr separately
- writes generated invocation metadata YAML (redacted, no secrets)
- treats exit code
0as success - treats exit code
1as failure - treats exit code
2as failure withvalidation_failed=trueand preserves output metadata when available - validates successful output files exist and are non-empty
- does not treat non-empty stderr as failure by itself
5. Configuration Contract
CLI pipeline config path resolution:
- when
--config <path>is provided, that path is used - when
--configis omitted, Narratio searches defaults in order:/usr/local/etc/narratio/pipeline.yml/etc/narratio/pipeline.yml
Optional pipeline secrets directory:
pipeline.secrets.env_direnables loading environment variables from local files before command execution- file name = env var name; file contents = env var value (trailing newline/CRLF trimmed)
- only env-var-style file names are considered; other entries are ignored
- existing process environment values are preserved (not overwritten)
- if configured, unreadable/missing
env_dirfails command execution early - relative
env_dirvalues are resolved from current working directory
Storage and archive foundations:
pipeline.storage.s3is available for modeling S3 coordinates:bucketroot_prefix(defaultdnd)regionendpointforce_path_style(defaultfalse)
pipeline.spool.rootdefaults to/var/spool/narratiopipeline.spool.delete_audio_after_archivedefaults tofalse(cleanup behavior not implemented yet)pipeline.archiveis optional and defaults to:enabled: trueupload_run: true- default
promote_artifacts:transcripts/trimmed.jsonartifacts/session_recap.md
- archive promotion rules enforce safe relative paths:
fromandtoare required- absolute paths are rejected
- traversal segments such as
..are rejected
Session input foundations:
session.campaignis required- local audio remains supported through
session.inputs.audio_dirorsession.inputs.audio_files - optional S3 audio input shape is modeled as
session.inputs.audio_s3.prefix audio_dir/audio_filesandaudio_s3are mutually exclusive- S3 input execution (object listing/downloading) is not implemented yet
Cross-config validation scope:
pipeline.storage.s3.bucketis required only when an S3-dependent feature is explicitly configured (for current foundations, that includessession.inputs.audio_s3, and archive upload intent when usingstorage.backend: s3)- no AWS credentials are stored in Narratio config; credential resolution remains an external runtime concern
Remote object-store backend scope:
- remote storage APIs are isolated to
internal/adapters/storage - AWS SDK types remain contained within the S3 backend implementation package
- S3 key/session path semantics remain outside the backend, with this invariant:
- callers pass full bucket-relative object keys
- backend methods do not prepend
root_prefixor infer campaign/session/run paths
- the backend layer is available for future prepare/archive usage, but no stage currently invokes
List/Download/Upload/Exists
pipeline.scriptorium is optional. Existing pipelines without Scriptorium continue to work.
pipeline.trim is optional. Existing pipelines without trim config continue to work.
pipeline.normalize is optional. Existing pipelines without normalize config continue to work.
pipeline.audita drives the real Audita subprocess adapter for the polish stage.
Audita required fields:
binarytimeoutbase_urlmodel
Audita optional fields:
llm_api_key_env(enforced only when configured)modulesoverride list (when omitted/empty, Narratio does not pass--modules)transcript_descriptionconfig_pathoutput_schema(bare-segmentsoraudita-v1)work_dir_retention(always,auto,never)total_llm_concurrency(> 0 when provided)proposal_llm_concurrency(> 0 when provided)validation_modelvalidation_llm_concurrency(> 0 when provided)report(defaulttrue)
Narratio passes only configured optional Audita flags; omitted optional values defer to Audita defaults.
When pipeline.normalize is omitted, defaults are applied:
output_path: transcripts/normalized.jsonoutput_schema: seriatim-intermediatereport: true
When pipeline.normalize is present:
output_pathmust be non-emptyoutput_schemamust be one ofseriatim-minimal,seriatim-intermediate, orseriatim-full- relative
output_pathvalues are session-workdir-relative paths - Seriatim binary settings still come from
pipeline.seriatim
When pipeline.trim is present:
enabledis optional and defaults tofalsewhen omitted- relative
output_path,bounds.output_path, andbounds.render_output_pathvalues are session-workdir-relative paths - do not store secrets in trim config values
When pipeline.trim.enabled: true:
output_pathis required and non-emptybounds.prompt_idis required and non-emptybounds.transcript_input_nameis required and non-emptybounds.output_pathis required and non-emptybounds.timeoutmust parse as a Go duration when providedbounds.render_debug: truerequires non-emptybounds.render_output_pathbounds.profile_idmay be empty to use the prompt default profile- prompt IDs are config values, not hardcoded stage logic
When pipeline.scriptorium is present:
binaryis required and non-emptyconfig_pathis optional; when provided it must be non-emptytimeoutis optional; when provided it must parse as a Go duration- default
timeoutis10m - unknown YAML fields fail strict decode
Artifacts are configured as a map under pipeline.scriptorium.artifacts so multiple artifacts are possible in the config shape.
For each artifact definition:
enabled: truerequires non-emptyprompt_idenabled: truerequires non-emptyoutput_pathtimeoutmust parse as Go duration when present- optional per-artifact
render_debugmay override globalscriptorium.render_debug inputsare named and each input requires non-emptysource- inputs may be optional (
required: false) varsvalues currently supportstringandbool
Prompt IDs and profile IDs are configuration values, not hardcoded stage logic.
Trim config shape:
trim:
enabled: true
output_path: "transcripts/trimmed.json"
bounds:
prompt_id: "dnd_session.bounds"
profile_id: ""
transcript_input_name: "transcript"
output_path: "artifacts/session_bounds.json"
timeout: "10m"
render_debug: false
render_output_path: "artifacts/session_bounds.render.json"
seriatim:
report: false
6. Transcript Tiers
Narratio currently produces and uses four transcript tiers:
transcripts/merged.json: canonical deterministic merged transcript from Seriatim mergetranscripts/processed.json: full raw Audita-polished transcript output (includes pre/post-game content)transcripts/normalized.json: normalized transcript generated by Seriatim normalizetranscripts/trimmed.json: gameplay-only normalized polished transcript from trim stage
Trim reads transcripts/normalized.json, validates bounds IDs against that same transcript ID space, and writes transcripts/trimmed.json.
7. Normalize Stage (Current Implementation)
Normalize stage behavior:
- stage order position: after
polishand beforetrim - discovers processed transcript from manifest polish outputs (
transcript_processed) when present, elsework/<session_id>/transcripts/processed.json - validates processed transcript JSON shape (
segmentsarray required) - runs Seriatim
normalizeto produce normalized transcript - validates normalized transcript JSON shape (
segmentsarray required) - validates normalize report JSON when enabled
Expected normalize outputs and diagnostics:
transcripts/normalized.jsonartifacts/seriatim.normalize.report.json(when normalize report is enabled)logs/seriatim.normalize.stdout.loglogs/seriatim.normalize.stderr.logconfig/seriatim.normalize.generated.yml
8. Trim Stage (Current Implementation)
Trim stage behavior:
- stage order position: after
normalizeand beforeanalyze - discovers normalized transcript from manifest normalize outputs (
transcript_normalized) when present, elsework/<session_id>/transcripts/normalized.json - validates normalized transcript JSON shape (
segmentsarray required) - when
trim.enabled: false(or trim config omitted), deterministically copies normalized transcript totranscripts/trimmed.jsonand recordstrim_action=copy_disabled - when
trim.enabled: true:- runs Scriptorium bounds prompt using configured
trim.bounds.prompt_id - writes bounds output to configured path (typically
artifacts/session_bounds.json) - parses and validates bounds output against the same normalized transcript being trimmed
- converts bounds range to Seriatim keep selector (for example
10-868) - runs Seriatim
trimto producetranscripts/trimmed.json - supports no-trim bounds actions (
none/copy) by copying normalized transcript unchanged
- runs Scriptorium bounds prompt using configured
- validates trimmed transcript JSON shape (
segmentsarray required)
Expected trim outputs and diagnostics:
artifacts/session_bounds.jsontranscripts/trimmed.jsonlogs/scriptorium.bounds.stdout.loglogs/scriptorium.bounds.stderr.logconfig/scriptorium.bounds.generated.ymllogs/seriatim.trim.stdout.loglogs/seriatim.trim.stderr.logconfig/seriatim.trim.generated.yml- optional bounds render-debug outputs when enabled:
artifacts/session_bounds.render.jsonlogs/scriptorium.bounds.render.stdout.loglogs/scriptorium.bounds.render.stderr.logconfig/scriptorium.bounds.render.generated.yml
Render-debug files are diagnostics. They are recorded in stage metadata/log/config refs and are not treated as canonical stage output artifact refs.
9. Analyze Stage (Current Implementation)
The current real analyze implementation supports only scriptorium.artifacts.session_recap.
Behavior:
- if
pipeline.scriptoriumis missing, analyze returns a skipped result with metadata - if no Scriptorium artifacts are enabled, analyze returns a skipped result with metadata
- if enabled artifacts exist but
session_recapis not enabled, analyze fails clearly - available transcript input sources for configured artifacts:
processed_transcript,normalized_transcript,trimmed_transcript session_recapshould usetrimmed_transcriptinput (transcripts/trimmed.json) for in-universe recap generationtrimmed_transcriptinput is resolved from manifest (trimoutput kindtranscript_trimmed) when available, otherwise fallback pathwork/<session_id>/transcripts/trimmed.jsonnormalized_transcriptinput is resolved from manifest (normalizeoutput kindtranscript_normalized) when available, otherwise fallback pathwork/<session_id>/transcripts/normalized.jsonprocessed_transcriptinput is resolved from manifest (polishoutput kindtranscript_processed) when available, otherwise fallback pathwork/<session_id>/transcripts/processed.jsonnormalized_transcriptis the preferred full-transcript source for future table/meta-analysis artifactsprocessed_transcriptremains available for advanced/debug use cases- transcript inputs are validated as JSON with top-level
segmentsarray - configured inputs are resolved by source
- optional
previous_recapis omitted when unavailable - required
previous_recapfails before invocation when unavailable - vars are built from config + session metadata
render_debugcontrols pre-runscriptorium renderdiagnostics- render failure stops stage before production run
- render output is validated as JSON
- production call uses Scriptorium adapter
RunArtifact - successful run output must exist and be non-empty
- missing
trimmed_transcriptinput for configuredtrimmed_transcriptsource fails clearly with guidance to run trim stage first - manifest records output refs, logs, generated config paths, and non-secret provenance metadata
10. Session Recap Paths
Current expected paths for session_recap:
- artifact output:
artifacts/session_recap.md - run stdout log:
logs/scriptorium.session_recap.stdout.log - run stderr log:
logs/scriptorium.session_recap.stderr.log - run generated invocation/config:
config/scriptorium.session_recap.generated.yml - render output (when enabled):
artifacts/session_recap.render.json - render stdout log:
logs/scriptorium.session_recap.render.stdout.log - render stderr log:
logs/scriptorium.session_recap.render.stderr.log - render generated invocation/config:
config/scriptorium.session_recap.render.generated.yml
11. Security and Privacy
- do not store secrets in pipeline YAML, generated invocation YAML, logs, or manifest metadata
- if API-key integration is configured, pass env var names only (never raw key values)
- with
pipeline.secrets.env_dir, secret file values are loaded into process env only and are not persisted in manifest metadata or generated configs - avoid logging transcript content or rendered prompt content by default
- treat generated artifacts and logs as potentially sensitive session material
12. Operational Caveat (Pre-Stale-Detection)
Checksum-based stale detection is not implemented yet.
If prepared inputs or prompt/runtime configuration change (for example glossary files, prompt IDs, profile IDs, or relevant pipeline settings), rerun the appropriate prior stages to refresh downstream artifacts.
Examples:
- glossary or autocorrect changes usually require rerunning at least
merge,polish,normalize,trim, andanalyze - trim prompt/profile changes require rerunning at least
normalize,trim, andanalyze - session recap prompt/profile/input-source changes require rerunning
analyze
13. Roadmap
Planned next steps:
- extend analyze beyond
session_recapto additional configured artifacts - support artifact inputs that consume prior generated artifacts
- keep this composable without adding a generic DAG engine in the near term
- implement real
archivebackend behavior - implement real
notifybackend behavior - add checksum-based stale detection and stale transitions
Architectural invariants remain:
- strict config decoding/validation
- manifest-driven run control
- clear stage/adapter separation
- configuration-driven prompt/profile/input/vars/output mapping
- Scriptorium integration through public CLI subprocess contract