11 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
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:
preparetranscribemergepolishtrimanalyzearchivenotify
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
pipeline.scriptorium is optional. Existing pipelines without Scriptorium continue to work.
pipeline.trim is optional. Existing pipelines without trim config continue to work.
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 three transcript tiers:
transcripts/merged.json: canonical deterministic merged transcript from Seriatim mergetranscripts/processed.json: full polished transcript from Audita polish (includes pre/post-game content)transcripts/trimmed.json: gameplay-only polished transcript from trim stage
Trim reads transcripts/processed.json, validates bounds IDs against that same transcript ID space, and writes transcripts/trimmed.json.
7. Trim Stage (Current Implementation)
Trim stage behavior:
- stage order position: after
polishand beforeanalyze - discovers processed transcript from manifest polish outputs (
transcript_processed) when present, elsework/<session_id>/transcripts/processed.json - validates processed transcript JSON shape (
segmentsarray required) - when
trim.enabled: false(or trim config omitted), deterministically copies processed 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 processed 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 processed 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.
8. 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 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.jsonprocessed_transcriptinput is resolved from manifest (polishoutput kindtranscript_processed) when available, otherwise fallback pathwork/<session_id>/transcripts/processed.jsonprocessed_transcriptremains available for future table/meta-analysis artifacts- 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
9. 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
10. 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
11. 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,trim, andanalyze - trim prompt/profile changes require rerunning at least
trimandanalyze - session recap prompt/profile/input-source changes require rerunning
analyze
12. 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