14 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
Still placeholder/future:
archivestage behaviornotifystage 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
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.
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)
- 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