narratio
narratio is a Go orchestration application for processing D&D session audio into transcripts and generated artifacts.
Current Implementation
Implemented now:
- strict config loading/validation (
pipeline.ymlandsession.yml) - local workspace/session layout, locking, and manifest persistence
- resumable stage control (
run,plan,resume,run-stage,status) - real
prepare,transcribe,merge,polish,normalize,trim, andanalyzestages - real WhisperX, Seriatim, and Audita adapters
- real Scriptorium subprocess adapter
- optional Scriptorium render diagnostics (
render_debug)
Not implemented yet:
notifystage behavior- additional analyze artifacts beyond
session_recap - generic DAG orchestration
Config Files
Narratio expects two YAML files:
pipeline.yml: pipeline/workspace settingssession.yml: per-session settings
Pipeline config lookup for CLI commands:
- if
--config <path>is provided, Narratio uses that path - if
--configis omitted, Narratio searches in this order:/usr/local/etc/narratio/pipeline.yml/etc/narratio/pipeline.yml
Session config lookup for CLI commands:
- if
--session <path>is provided, Narratio uses that path - if
--sessionis omitted, Narratio searches in this order:./session.yml/usr/local/etc/narratio/session.yml/etc/narratio/session.yml
Session template support:
- Narratio renders
session.ymltemplates before strict YAML decode. --session-id <value>provides thesession_idtemplate variable.- Supported placeholder forms:
{{session_id}}{{ session_id }}
- unresolved template placeholders fail with a clear error.
- strict YAML validation still runs after rendering.
- concrete
session.ymlfiles without templates remain fully supported.
Optional secrets-from-files config:
pipeline.secrets.env_dirmay point to a directory of secret files- each top-level file with an env-var-style name is loaded as an environment variable:
- file name = env var name
- file contents = env var value (trailing newline/CRLF trimmed)
- process environment wins: existing env vars are not overwritten
- if configured, Narratio fails fast when
env_diris missing/unreadable - relative
env_dirvalues resolve from Narratio’s current working directory
YAML decoding is strict (KnownFields(true)), so unknown fields fail fast.
Storage And Archive Foundations
Narratio now includes configuration and path-model foundations for archive support, plus implemented prepare-stage S3 audio input.
Implemented foundations:
pipeline.storage.s3config shape (bucket,root_prefix,region,endpoint,force_path_style,access_key_id_env,secret_access_key_env)pipeline.spoolconfig shape (root,delete_audio_after_archive)pipeline.archiveconfig shape (enabled,upload_run,promote_artifacts)- promotion-rule validation (
from/torequired, relative-only paths, traversal rejected) session.campaignrequirement for campaign-aware path construction- optional
session.inputs.audio_s3.prefixmodeling and prepare-stage S3 audio download - run ID generation and S3/local path helper foundations
- manifest run/path identity fields
Current defaults:
pipeline.storage.s3.root_prefix:dndpipeline.storage.s3.access_key_id_env:OBJECT_STORAGE_KEY_IDpipeline.storage.s3.secret_access_key_env:OBJECT_STORAGE_KEYpipeline.workspace.cleanup_after_archive:falsepipeline.spool.root:/var/spool/narratiopipeline.spool.delete_audio_after_archive:falsepipeline.archive.enabled:truepipeline.archive.upload_run:true- default
pipeline.archive.promote_artifacts:transcripts/trimmed.json->transcripts/trimmed.json(required: true)artifacts/session_recap.md->artifacts/session_recap.md(required: true)
Current boundaries:
- local development audio (
audio_dir/audio_files) still works audio_dir/audio_filesandaudio_s3are mutually exclusive- real S3-compatible backend now exists in the storage adapter package
- storage backend tests use fake storage and do not require live S3
- archive uploads successful run records under
runs/{run_id}/ - archive does not upload local audio by default
- archive uploads promoted outputs to session-level keys using
archive.promote_artifacts - archive uploads
current/manifest.json - archive uploads
current/run_id.txtlast as the effective commit marker - required missing promotions fail archive
- optional missing promotions are skipped and recorded
- cleanup remains conservative and opt-in:
pipeline.spool.delete_audio_after_archive: trueremoves only the run-scoped spool audio directory after successful archive commitpipeline.workspace.cleanup_after_archive: trueremoves only the run-scoped local workdir after successful archive commit- cleanup executes only after all selected stages for the command invocation succeed
- cleanup does not run for failed, incomplete, skipped, or unarchived runs
- local development
audio_dir/audio_filessource inputs are never deleted by spool cleanup - S3 credentials are resolved from configured env-var names when both are present; if either is missing, Narratio falls back to the AWS SDK default credential chain
S3 input details and current boundaries are documented in docs/s3-audio-input.md.
Remote Storage Backend
Narratio includes an object-store backend layer for future prepare/archive work:
List(ctx, prefix)Download(ctx, key, localPath)Upload(ctx, localPath, key, opts)Exists(ctx, key)
Implemented backends:
- fake storage backend for deterministic tests
- S3-compatible backend built from
pipeline.storage.s3
Key invariant:
- callers pass full bucket-relative object keys
- storage backends do not prepend
root_prefixand do not infer session/campaign paths
Current boundary:
prepareusesList+Downloadthrough the backend whensession.inputs.audio_s3is configuredarchiveusesUploadthrough the backend for successful run-record uploadsarchivealso usesUploadfor promotion writes and current pointers- no failed or incomplete runs are uploaded
- local audio is not re-uploaded by default
Archive run-upload details and boundaries are documented in docs/archive-storage.md.
Canonical Stage Order
preparetranscribemergepolishnormalizetrimanalyzearchivenotify
Transcript Tiers
transcripts/merged.json: canonical deterministic merged transcript from Seriatim mergetranscripts/processed.json: full raw Audita-polished transcript outputtranscripts/normalized.json: Seriatim-normalized transcript from the normalize stagetranscripts/trimmed.json: gameplay-only normalized polished transcript from trim stage
Seriatim Configuration
pipeline.seriatim configures the Seriatim subprocess adapter used by merge, normalize, and trim.
Minimal behavior:
pipeline.seriatimmay be omitted entirely.- when omitted, Narratio defaults to:
binary: seriatimtimeout: 10moutput_schema: seriatim-intermediatecoalesce_gap: 3.0report: true
Optional overrides in pipeline.seriatim continue to work, including explicit binary paths and advanced env tuning values.
Audita Configuration
pipeline.audita configures the real Audita subprocess adapter used by polish.
Minimal behavior:
pipeline.auditamay be omitted entirely.- when omitted, Narratio defaults to:
binary: auditatimeout: 3hreport: true
Optional:
llm_api_key_env(when set, Narratio requires that env var and passes it to Audita asAUDITA_LLM_API_KEY)modulesoverride list (when empty/omitted, Narratio does not pass--modules)base_url(when omitted, Narratio does not pass--base-url; Audita runtime defaults/config may apply)model(when omitted, Narratio does not pass--model; Audita runtime defaults/config may apply)transcript_descriptionconfig_pathoutput_schema(bare-segmentsoraudita-v1)work_dir_retention(always,auto, ornever)total_llm_concurrency(> 0 when provided)proposal_llm_concurrency(> 0 when provided)validation_modelvalidation_llm_concurrency(> 0 when provided)report(defaults totrue)
Narratio passes only configured optional Audita flags. Omitted optional values are left to Audita runtime defaults/config.
Normalize Configuration
pipeline.normalize is optional. When omitted, Narratio defaults to:
output_path: transcripts/normalized.jsonoutput_schema: seriatim-intermediatereport: true
Allowed normalize.output_schema values:
seriatim-minimalseriatim-intermediateseriatim-full
normalize.output_path is treated as session-workdir-relative when not absolute.
Normalize stage behavior summary:
- normalize runs after
polishand beforetrim - normalize resolves
transcripts/processed.json - normalize runs Seriatim
normalizeto producetranscripts/normalized.json - normalize diagnostics are written to:
artifacts/seriatim.normalize.report.json(when enabled)logs/seriatim.normalize.stdout.loglogs/seriatim.normalize.stderr.logconfig/seriatim.normalize.generated.yml
Trim Configuration
pipeline.trim is optional. If omitted, no trim config is loaded. If trim.enabled is omitted, it defaults to false.
When trim.enabled: true:
trim.output_pathis requiredtrim.bounds.prompt_idis requiredtrim.bounds.transcript_input_nameis requiredtrim.bounds.output_pathis requiredtrim.bounds.timeoutmust be a valid Go duration when providedtrim.bounds.render_debug: truerequirestrim.bounds.render_output_pathtrim.bounds.profile_idmay be empty to use the prompt default profile
Trim paths are treated as session-workdir-relative when not absolute.
Example trim config:
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
Trim behavior summary:
- trim discovers and validates
transcripts/normalized.json - trim uses Scriptorium bounds (
dnd_session.boundsby example config) to produceartifacts/session_bounds.json - bounds IDs are validated against the same normalized transcript ID space that Seriatim trim will consume
- trim converts bounds to Seriatim keep selector (for example
10-868) and runs Seriatim trim - if trim is disabled, Narratio copies normalized transcript to trimmed transcript and records
trim_action=copy_disabled
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:
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 and are not treated as canonical stage output artifact refs.
Scriptorium Configuration
pipeline.scriptorium is optional. When present, Narratio validates and uses it for analyze-stage artifact generation.
Key points:
scriptorium.binarydefaults toscriptoriumwhen section is presentscriptorium.config_pathis optionalscriptorium.timeoutdefaults to10mwhen omittedscriptorium.render_debugenables render diagnostics globally- artifacts are configured under
scriptorium.artifacts(map shape supports multiple artifacts) - enabled artifacts require
prompt_idandoutput_path - artifact
render_debugmay override global render setting varscurrently support boolean and string values
Example session_recap artifact definition:
scriptorium:
binary: "scriptorium"
config_path: "/etc/scriptorium/config.yml"
timeout: "10m"
render_debug: false
artifacts:
session_recap:
enabled: true
prompt_id: "dnd.session_recap"
profile_id: "local-quality" # optional
output_path: "artifacts/session_recap.md"
timeout: "10m"
# render_debug: true # optional per-artifact override
inputs:
transcript:
source: "trimmed_transcript"
required: true
previous_recap:
source: "previous_session_artifact"
artifact: "session_recap"
path: "" # optional; set when available
required: false
vars:
session_id: true
session_date: true
campaign_name: true
previous_session_id: true
output_kind: "session_recap"
Prompt IDs and profile IDs are configuration values. They are not hardcoded in analyze-stage logic.
Do not put secrets in pipeline.yml. If API-key behavior is configured, use env var names only.
If pipeline.secrets.env_dir is configured, keep only references and secret files there; secret values are still not written to manifests, generated configs, or Narratio-managed logs.
Scriptorium Runtime Behavior
Narratio integrates with Scriptorium through the public CLI subprocess contract:
- generation:
scriptorium run - diagnostics/testing:
scriptorium render --format jsonwhenrender_debugis enabled
For the initial implementation, only session_recap generation is supported.
Analyze-stage session recap behavior:
- available transcript input sources for configured artifacts:
processed_transcript,normalized_transcript,trimmed_transcript - session recap should use gameplay-only transcript input (
source: trimmed_transcript) - Narratio resolves
trimmed_transcriptfrom trim manifest output (transcript_trimmed) or fallbacktranscripts/trimmed.json - Narratio resolves
normalized_transcriptfrom normalize manifest output (transcript_normalized) or fallbacktranscripts/normalized.json - missing trimmed transcript fails clearly and advises running trim stage first
normalized_transcriptis the preferred full-transcript source for future table/meta-analysis artifactsprocessed_transcriptremains supported for advanced/debug use cases- optionally includes
previous_recapwhen configured and resolvable - omits optional previous recap when unavailable
- fails if required inputs are missing
- validates output file exists and is non-empty
Expected session output paths:
artifacts/session_recap.mdlogs/scriptorium.session_recap.stdout.loglogs/scriptorium.session_recap.stderr.logconfig/scriptorium.session_recap.generated.ymlartifacts/session_recap.render.jsonwhen render diagnostics are enabled
Examples
Starter files:
examples/pipeline.minimal.ymlexamples/pipeline.audita-overrides.ymlexamples/session.minimal.ymlexamples/session.template.ymlexamples/speakers.yml
Commands
Run tests:
go test ./...
Plan a run:
go run ./cmd/narratio plan --session examples/session.minimal.yml
Use --config <path> to override default pipeline lookup when needed.
Run with a discoverable session template:
go run ./cmd/narratio run --session-id 2026-04-04
Run full pipeline:
go run ./cmd/narratio run --config examples/pipeline.minimal.yml --session examples/session.minimal.yml
Run analyze only:
go run ./cmd/narratio run-stage --config examples/pipeline.minimal.yml --session examples/session.minimal.yml analyze
Resume with a template session ID:
go run ./cmd/narratio resume --config examples/pipeline.minimal.yml --session examples/session.template.yml --session-id 2026-04-04
Operational Note
Checksum-based stale detection is not implemented yet.
If prepared inputs or prompt/runtime config change, rerun the appropriate upstream stages before relying on downstream artifacts.
Examples:
- glossary/autocorrect/speaker-context changes: rerun at least
merge,polish,normalize,trim, andanalyze - trim bounds prompt/profile/config changes: rerun at least
normalize,trim, andanalyze - session recap prompt/profile/input-source changes: rerun
analyze
Roadmap
Near-term roadmap:
- extend analyze to additional configured artifacts
- support workflows where later artifacts consume earlier generated artifacts
- keep orchestration explicit without a generic DAG engine
- implement archive and notify backends