15 KiB
Narratio Architecture
1. Purpose
narratio is a Go orchestrator for D&D session processing. It coordinates a stage-based pipeline from recorded audio through transcript and artifact generation, while preserving durable run state for skip/rerun/resume behavior.
The repository currently implements the orchestration scaffold, local state model, and stage framework, including a real WhisperX HTTP adapter and a real transcribe stage. It intentionally does not yet implement real Seriatim, Audita, analyzer, remote archive, or notification integrations.
2. Design Goals
- Modular boundaries between orchestration, stages, adapters, storage, config, and manifest.
- Config-driven behavior with strict decoding and conservative validation.
- Durable manifest-based control for resumability and idempotent reruns.
- Boring, explicit control flow (no workflow engine/DAG abstraction).
- Observable orchestration via structured logs plus per-tool log file paths.
- Safe extension points for incremental real-stage implementation.
3. Non-Goals
Current and near-term non-goals:
- Reimplementing WhisperX, Seriatim, Audita, or analyzer internals.
- Building a generic distributed workflow engine.
- Adding a DB dependency for v1.
- Embedding D&D prompt logic in orchestration core.
- Implementing checksum-based stale detection (planned, not implemented).
4. Current Implementation Status
Implemented now:
- CLI entrypoint and command dispatch (
run,plan,status,resume,run-stage). - Strict
pipeline.yml/session.ymlloading and validation. - Local workdir and artifact-store abstraction with lock-file support.
- Durable local JSON manifest store with atomic writes.
- Stage interface, canonical stage ordering, and runner main loop.
- Real
preparestage (input resolution/materialization/provenance). - Real
transcribestage (prepared-audio discovery, WhisperX adapter execution, JSON transcript validation, provenance metadata). - Real WhisperX HTTP adapter implementation (multipart POST + retries + timeout + atomic output writes).
- Placeholder downstream stages (
normalize..notify) with adapter contract calls. - Adapter interfaces and fake/no-op implementations for all external boundaries.
- Reusable subprocess helper and generated YAML/config writing helper.
- Test coverage across config, manifest, artifacts, planning, runner control, and adapters.
Still planned/future:
- Real Seriatim adapter.
- Real Audita adapter.
- Real analyzer integration.
- Real remote archive/storage backend (S3/SFTP/etc).
- Real notification backend.
- Stale detection based on input/config checksums.
5. High-Level Pipeline
Canonical stage order is fixed in code:
preparetranscribenormalizemergepolishanalyzearchivenotify
Execution status:
prepareis implemented with real local filesystem behavior.transcribeis implemented and validates raw transcript JSON outputs.normalize/merge/polish/analyze/archive/notifyremain placeholders.
6. CLI Commands
cmd/narratio/main.go is intentionally thin and delegates to internal/app.
Current command behavior:
run --config <pipeline.yml> --session <session.yml> [--force]- Loads + validates config, ensures workdir, acquires lock, loads/creates manifest, executes full stage plan.
- Skips already-succeeded stages unless
--force.
plan --config <pipeline.yml> --session <session.yml> [--force]- Loads + validates config, ensures workdir, loads manifest if present, prints
run/skipaction per stage and totals.
- Loads + validates config, ensures workdir, loads manifest if present, prints
resume --config <pipeline.yml> --session <session.yml> [--force]- Resumes from first non-succeeded stage based on manifest (or full run when forced).
run-stage --config <pipeline.yml> --session <session.yml> [--force] <stage>- Executes exactly one named stage; unknown stage is an error.
status --manifest <path>- Loads manifest and prints session ID, updated timestamp, and stage statuses.
Invalid usage returns non-zero with usage/error text.
7. Configuration
Package: internal/config
Files:
pipeline.ymlsession.yml
Key behavior:
- Strict YAML decoding via
yaml.Decoder.KnownFields(true). - Unknown fields are rejected.
- Combined resolved config type keeps source paths (
PipelinePath,SessionPath) for provenance/errors. - WhisperX optional fields are defaulted during load for deterministic resolved config values.
WhisperX config keys:
pipeline.whisperx.transcribe_url(required)pipeline.whisperx.language(default:en)pipeline.whisperx.timeout(default:30m)pipeline.whisperx.retries(default:3)pipeline.whisperx.retry_delay(default:2s)pipeline.whisperx.concurrency(default:2)
Seriatim config keys:
pipeline.seriatim.binary(required)pipeline.seriatim.timeout(default:10m)pipeline.seriatim.output_schema(default:seriatim-intermediate; allowed:seriatim-minimal|seriatim-intermediate|seriatim-full)pipeline.seriatim.coalesce_gap(default:3.0, must be>= 0)pipeline.seriatim.report(default:true)- optional tuning under
pipeline.seriatim.env.*(must be> 0when provided)
Validation currently enforces:
pipeline.workspace.rootis required.pipeline.whisperx.transcribe_urlis required and must be a valid URL.pipeline.whisperx.timeoutandpipeline.whisperx.retry_delaymust parse as Go durations.pipeline.whisperx.retriesmust be>= 0.pipeline.whisperx.concurrencymust be> 0.pipeline.seriatim.binaryis required.pipeline.seriatim.timeoutmust parse as Go duration.pipeline.seriatim.output_schemamust be one ofseriatim-minimal|seriatim-intermediate|seriatim-full.pipeline.seriatim.coalesce_gapmust be>= 0.- optional
pipeline.seriatim.env.*values must be> 0when provided. session.session_idis required.session.inputs.speakers_file,autocorrect_file,glossary_fileare required.- At least one audio source:
session.inputs.audio_diror non-emptysession.inputs.audio_files. - Configured timeout fields must parse as Go durations when present.
Validation scope is intentionally conservative (no deep business defaults, no full schema semantics, no remote connectivity checks).
8. Manifest and Stage Status
Package: internal/manifest
Model includes:
ManifestStageRecordArtifactRecordInputRecordErrorRecordStageStatus
Supported stage statuses:
pendingrunningsucceededfailedskippedstale(defined but not actively produced by runner yet)interrupted(defined for future use)
Store behavior:
- Local JSON store with create/load/save.
- Save is atomic (same-directory temp file + rename).
updated_atrefreshed on save.
Helpers implemented:
MarkStageRunningMarkStageSucceededMarkStageFailedMarkStageSkipped
Runner currently records running/succeeded/failed transitions and preserves existing succeeded stage metadata when skipping.
Manifest versioning note:
- Current manifest model includes optional
pipeline_version, but does not include an explicit manifest schema/version field. - Before manifests become long-term compatibility-sensitive across releases/storage backends, add explicit manifest schema/versioning and migration policy.
9. Workdir and Artifact Model
Package: internal/artifacts
Canonical local paths are resolved by SessionPaths under:
{workspace.root}/work/{session_id}/
Subpaths:
inputs/audio/transcripts/raw/transcripts/normalized/artifacts/config/logs/manifest.json.lock
Implemented store capabilities:
- Ensure layout.
- Resolve session paths.
- Copy inputs.
- Existence checks.
- SHA-256 checksums.
- Atomic file writes.
- Session lock acquire/release (
.lock, conflict-safe viaO_EXCL).
Ref supports local path info plus optional future remote key/checksum metadata.
10. Stage Architecture
Package: internal/stage
Core contracts:
Stageinterface withName,Declares, andRun(ctx, env, manifest).IODeclfor declared input/output artifact intent.StageResultfor outputs, logs, generated configs, and metadata.
Current Declares role:
Stage.Declaresis currently contract metadata; the runner does not yet enforce declared inputs/outputs at execution time.- Real-stage implementation work should strengthen enforcement by validating declared inputs before execution and validating declared outputs after execution.
Current stage behaviors
prepare(real):- Ensures layout.
- Resolves session-relative input paths.
- Materializes required input files into
inputs/. - Writes
pipeline.resolved.yml. - Resolves/copies
.flacaudio inputs fromaudio_diroraudio_files. - Computes checksums and records deterministic
manifest.inputs. - Uses checksum-aware write/copy reuse for idempotency.
transcribe(real):- Discovers prepared
.flacaudio inputs frommanifest.inputs(kind=audio) orwork/.../audiofallback. - Derives per-speaker output files at
transcripts/raw/{audio_basename}.json. - Calls
whisperx.Clientwith bounded parallelism frompipeline.whisperx.concurrency. - Validates each output file exists and is valid JSON before stage success.
- Records output refs plus stage metadata (
audio_files_count, language/concurrency/retry settings, output paths, per-file attempts/status/duration/path).
- Discovers prepared
normalize..notify(placeholder):- Return placeholder metadata.
- Optionally call adapters using expected request/result contract shapes.
11. Application Environment and Adapters
internal/app uses stage.Env as the shared dependency container to avoid duplicate environment definitions.
Contained dependencies:
- Resolved config
- Artifact store
- Manifest store
- Logger
- Adapter interfaces
Adapter boundaries (internal/adapters/*):
whisperx.Clientseriatim.Runneraudita.Runneranalyzer.Runnerstorage.Backendnotify.Sender
All adapters currently have fake/no-op implementations for tests/scaffold execution. WhisperX additionally has a real HTTP adapter implementation under internal/adapters/whisperx/http.go.
Subprocess helper
internal/adapters/subprocess provides reusable subprocess scaffolding:
- Context cancellation + optional timeout.
- Explicit executable/args, working dir, env overrides.
- Stdout/stderr log file handling.
- Exit code and timing capture.
- Actionable error wrapping.
- Atomic file/YAML writers for generated config/log scaffolding.
12. Run Control, Locking, Logging, and Long-Running Stages
Run control
internal/app runner is sequential and manifest-driven:
- Loads/creates manifest.
- Computes per-stage action (
runvsskip). - Skip rule today: succeeded + not forced => skip.
--forcereruns previously succeeded stages.resumestarts at first non-succeeded stage.
Stale detection is intentionally TODO (run_control.go) pending checksum-based invalidation logic.
Locking
run/resume/run-stage execution paths acquire a session lock via artifact store and release it with defer.
Logging
- Structured logger is initialized via
internal/logging(slogtext handler). - Runner emits concise stage lifecycle logs (skip/start/success/fail + manifest save points).
- Placeholder subprocess adapter requests include distinct stdout/stderr log paths to preserve future boundary design.
Long-running stage expectations
The architecture expects long-running stages (especially WhisperX and Audita). WhisperX already uses context timeout/cancellation and retry logic inside its HTTP adapter; other long-running integrations are still pending.
13. Testing Strategy
Current tests verify scaffold behavior without real external services:
- Strict config load/validate behavior and error quality.
- Manifest store round trips and transition semantics.
- Workdir layout, atomic writes, checksums, and lock behavior.
- Plan order and stage selection.
- Run/skip/force/resume/run-stage control behavior.
- Prepare-stage input materialization/provenance/idempotency.
- Real transcribe-stage audio discovery/concurrency/failure handling/output validation/provenance.
- Adapter fake behavior and error propagation.
- WhisperX HTTP adapter behavior (request shape, retry policy, timeout/cancel, JSON validation, atomic writes).
- Subprocess helper behavior (success/failure/timeout/log capture).
Tests intentionally avoid hardcoding arbitrary operational default values.
14. Future Work / Implementation Roadmap
Recommended implementation sequence (one focused boundary at a time):
- Implement real
normalizetranscript transformation/validation. - Implement real
mergeusing Seriatim adapter + generated config + subprocess logs. - Implement real
polishusing Audita adapter + checkpoint/log handling. - Implement real
analyzeadapter integration and artifact validation. - Implement real
archiveremote backend behavior. - Implement real
notifybackend. - Add checksum-based stale detection and stale status transitions.
- Add selective parallelism where architecturally safe (
transcribefan-out and/or downstream-safe boundaries).
Each step must preserve existing package boundaries and manifest-based control flow.
Real Stage Implementation Checklist
For each stage moved from placeholder to real behavior:
- Implement and/or use the appropriate adapter behavior for that stage boundary.
- Generate and use config/log paths needed by the adapter/tool invocation.
- Validate expected stage inputs before execution.
- Write outputs atomically where practical (temp + promote/rename).
- Validate expected outputs after execution before marking success.
- Record outputs, logs, generated configs, and provenance metadata in manifest stage records.
- Preserve
skip/force/resumesemantics (do not regress run-control behavior). - Add fake-adapter tests and at least one explicit failure-path test for stage behavior.
15. Architectural Invariants
The following invariants are mandatory for future changes:
- Stage orchestration logic lives in
internal/app; stage business logic lives ininternal/stage. - External tool details remain behind adapter interfaces.
- Config loading is strict and validation-first.
- Manifest is the source of truth for stage status and resume/skip decisions.
- Stage outputs/provenance are recorded durably and deterministically.
- Local workdir is the primary execution area; remote storage is an adapter concern.
- Main entrypoint stays thin (
cmd/narratiodelegates tointernal/app). - Pipeline behavior remains explicit and maintainable; no generic DAG engine introduction.
- Every stage must consume declared artifacts, produce declared artifacts, validate them, record provenance, and remain safely skippable/rerunnable.