Dialogue System¶
Dynamic conversation engine with strategies, handlers, and NPC interactions.
This document will be auto-updated by Minh when code changes are detected.
Architecture Overview¶
The dialogue system uses the Strategy Pattern to allow flexible, extensible NPC dialogue handling. Core packages live under dialogue/ in TheSmithy:
| Package | Purpose |
|---|---|
dialogue/core/ |
Interfaces, formatters, dialogue sources |
dialogue/handlers/ |
Input routing, session management, system selection |
dialogue/strategies/ |
Pluggable dialogue strategy implementations |
dialogue/services/ |
Condition evaluation, command execution, input matching |
dialogue/models/ |
Data models (e.g. DialogueResult) |
dialogue/interfaces/ |
Abstract interfaces for command executors, condition evaluators, dialogue sources, freeform matchers |
Strategy Pattern¶
All dialogue strategies extend DialogueStrategy (dialogue/strategies/base.py), which defines:
can_handle(npc) -> bool— Whether this strategy applies to the NPC.get_priority() -> int— Priority (0–100, highest checked first).start_dialogue(caller, npc)— Begin a conversation.handle_input(caller, npc, input_text)— Process player input during dialogue.end_dialogue(caller, npc)— Clean up when dialogue ends.
Registered Strategies¶
The DialogueStrategyRegistry (dialogue/strategies/registry.py) lazily initializes and registers the default strategies on first access:
| Strategy | Priority | Description |
|---|---|---|
ConditionalDialogueStrategy |
90 | API-based conditional dialogue |
StructuredDialogueStrategy |
80 | JSON-file-driven passage-based dialogues |
SimpleDialogueStrategy |
50 | Basic dialogue handling |
LegacyDialogueStrategy |
30 | Backward-compatible dialogue menus |
NPCs can also provide a custom strategy (priority 100) by implementing get_dialogue_strategy(caller) or get_dialogue_handler() on their typeclass. The caller argument enables per-(caller, npc) strategy decisions — for example, FaeFamiliar returns a TeachBackDialogueStrategy when the caller has a pending teach-back at the current room, and falls through to the registry default otherwise.
TeachBackDialogueStrategy¶
TeachBackDialogueStrategy (dialogue/strategies/teach_back_strategy.py) is an additional strategy that is not registered in the global DialogueStrategyRegistry. Because its routing decision requires caller context (whether the caller has a pending teach-back at the current room), it cannot use the registry's can_handle(npc) signature. Instead, FaeFamiliar.get_dialogue_strategy(caller) returns an instance when teach-back should fire.
When active, the strategy delegates to TeachBackOrchestrator:
start_dialogue(caller, npc)— Looks up the pending teach-back location viaorch.get_pending_location_for_player(caller)and callsorch.deliver_prompt(caller, pending_loc, npc=npc). ReturnsFalse(with a warning log) if no pending location exists at start time.handle_input(caller, npc, input_text)— ReturnsFalse; teach-back input flows throughTeachBackOrchestrator's owndialogue_messagehandler.end_dialogue(caller, npc)— Callsend_active_dialogue(caller, reason="teach_back_end").
System Selector¶
DialogueSystemSelector (dialogue/handlers/system_selector.py) is the entry point for starting NPC dialogue. Resolution order:
- NPC custom strategy via
npc.get_dialogue_strategy(caller)(highest priority). - Registry lookup — iterates registered strategies by descending priority and returns the first whose
can_handle(npc)returnsTrue. - Fallback message if no strategy matches.
class CustomNPC(BaseNPC):
def get_dialogue_strategy(self, caller):
return MyCustomDialogueStrategy()
Handlers¶
input_router.py— Routes player input to the active dialogue handler.session_manager.py— Manages active dialogue sessions per player/NPC pair.system_selector.py— Selects the appropriate dialogue strategy (see above).
Interaction Dispatch (CmdInteract)¶
CmdInteract (commands/interact.py) is the player-facing command that initiates all object and NPC interactions. After target resolution, it applies the following dispatch chain:
InteractableMixin¶
Any typeclass that inherits InteractableMixin (typeclasses/mixins/interactable.py) is routed through a single generic dispatch branch. The mixin defines an at_interact(caller) contract that subclasses must implement. Current implementations include Telescope, L2RBeacon, and MendableBoat.
Adding a new interactable object requires no changes to CmdInteract — just inherit the mixin and implement at_interact:
from typeclasses.mixins.interactable import InteractableMixin
class MyNewProp(InteractableMixin, Object):
def at_interact(self, caller):
# ... your response ...
If at_interact raises an exception, CmdInteract catches it, logs the error, and sends a generic failure message to the caller.
Note:
BoardGameObjectstill uses a dedicated branch inCmdInteractbecause its player-selection / challenge-OOB flow has not yet been migrated to the mixin pattern.
Pre-Dialogue Hook (at_pre_dialogue)¶
Before starting the dialogue strategy, CmdInteract checks whether the NPC defines an at_pre_dialogue(caller) method. This hook lets NPCs inspect caller state, transfer items, set flags, or perform other mutations before the dialogue widget opens or the dialogue strategy evaluates conditional state.
- Return
Trueto suppress the rest of the interaction (no preamble, no dialogue strategy). Use this for NPCs that fully handle the interaction without a dialogue widget. - Return
False/None(or don't define the hook) to proceed to dialogue normally. - If the hook raises an exception, it is logged and dialogue continues.
class Arturo(BaseNPC):
def at_pre_dialogue(self, caller):
# Scan caller's inventory for puzzle items and place them
# on the Stone Table before dialogue opens.
self._check_puzzle_items(caller)
return False # Proceed to dialogue
Note: Choosing an alternate dialogue strategy (e.g. teach-back) belongs in
NPC.get_dialogue_strategy(caller), not inat_pre_dialogue.
Full Resolution Order¶
- BoardGameObject — dedicated branch (player-selection / challenge-OOB flow).
- InteractableMixin — generic
at_interact(caller)dispatch (Telescope, L2RBeacon, MendableBoat, etc.). No dialogue. - Non-NPC rejection — objects that are neither interactable nor NPCs get a "can't interact" message.
at_pre_dialoguehook — NPC mutation/inspection before dialogue. May suppress dialogue.- Client dialogue-start messages — OOB messages sent to iOS/Cascade/Reader clients.
DialogueSystemSelector.start_dialogue(caller, npc)— strategy resolution (custom → registry → fallback).
Narrator (Wisp) State Machine¶
The Narrator NPC ("Wisp") uses a state machine to manage its lifecycle during narrated storylines. The WispState class (typeclasses/npcs/narrator.py) replaces the previous simple narrator_mode boolean with explicit states and validated transitions.
States¶
| State | Value | Description |
|---|---|---|
WispState.IDLE |
"idle" |
In shared rooms, available for chat and storyline offers |
WispState.FOLLOWING |
"following" |
Moving with player between rooms |
WispState.NARRATING |
"narrating" |
Actively delivering story chunks |
WispState.PAUSED |
"paused" |
Narrator paused, awaiting resume |
All valid states are available as WispState.ALL.
State Property¶
The Narrator class exposes a state property that validates transitions and keeps the legacy db.narrator_mode boolean in sync for backward compatibility:
from typeclasses.npcs.narrator import Narrator, WispState
wisp = character.search("Wisp", global_search=True)
# Read current state
wisp.state # -> "idle"
# Set state (validated — invalid values are logged and rejected)
wisp.state = WispState.NARRATING
# Legacy attribute still works (derived from state)
wisp.db.narrator_mode # -> True (when NARRATING or PAUSED)
The setter logs state transitions at INFO level: [NARRATOR] Wisp state: idle -> narrating.
State Transitions¶
State transitions are managed by NarratorModeManager (storybook/services/narrator_services/mode_manager.py):
| Action | Method | State Change |
|---|---|---|
| Enter narration | mode_manager.enter_narrator_mode() |
→ NARRATING |
| Exit narration | mode_manager.exit_narrator_mode() |
→ IDLE |
| Pause narration | mode_manager.pause_narration() |
→ PAUSED |
| Resume narration | mode_manager.resume_narration() |
→ NARRATING (via enter) |
ensure_idle()¶
The ensure_idle() method on Narrator provides a safe reset to IDLE. It cancels any active narration (delegating to mode_manager.exit_narrator_mode() if narrative data exists) and sets the state to IDLE. This is used when the player moves away from the narrator during an active narration:
Display Name¶
Wisp's display name reflects the current state:
| State | Display |
|---|---|
IDLE or FOLLOWING |
Wisp |
NARRATING |
Wisp (narrating) |
PAUSED |
Wisp (paused) |
Narrative Commands¶
Defined in commands/narrative_commands.py. These commands control pacing during narrated storylines and are available to all players (cmd:all()).
| Command | Aliases | Usage |
|---|---|---|
continue |
next, c |
Advance to the next narrative chunk |
back |
previous, prev, b |
Return to the previous chunk |
pause |
— | Toggle auto-advance on/off |
resume |
continue story, play |
Resume from a saved position |
leave story |
exit story, quit, leave |
Exit narration and return to main world |
narrative |
narrative status, story, story status |
View narrative progress |
Narrator Lookup¶
The _find_narrator(caller) helper locates the Narrator NPC for a character:
- Room contents (fast path) — checks
caller.location.contentsfor an object withdb.is_narrator = True. - Global search — queries all
Narratortypeclasses and matches bydb.current_narrative.character. Only returns narrators whosestateisNARRATINGorPAUSED(i.e., actively engaged with the caller). If found remotely, the narrator is moved to the caller's room.
State Checks in Commands¶
All narrative commands that require an active narration check wisp.state against WispState values rather than the legacy db.narrator_mode boolean:
from typeclasses.npcs.narrator import WispState
# Commands check for active narration states
if wisp.state not in (WispState.NARRATING, WispState.PAUSED):
caller.msg("The narrative isn't currently active.")
return
The resume command checks state.get("narrator_mode") on the NarrativeState dict (not the Wisp attribute) to determine if the narrative is already active before attempting to resume.
Player Commands — Object Nicknames¶
Defined in commands/nickname.py (Issue #1066). Available to all players (cmd:all()). Lets a player assign a personal short handle to any game object — useful for distinguishing cantrip cartridges or frequently referenced items.
| Command | Aliases | Usage |
|---|---|---|
nickname |
callit |
nickname <name> = <item> — set a personal handle |
nickname/clear |
— | nickname/clear <name> — remove a nickname |
nickname |
— | nickname (no args) — list your nicknames |
Nicknames are implemented as Evennia object nicks (category "object"), mapping the player's label to the target's dbref. Because caller.search() expands object nicks automatically, nicknames work transparently with commands that use it — including CmdInteract for NPC target resolution (e.g. interact buddy after nickname buddy = Pearl).
The inventory commands CmdGet and CmdPut (commands/inventory_enhanced.py) use manual matching in addition to caller.search(), so they expand nicks explicitly via a _denick() helper that calls caller.nicks.nickreplace(name, categories=("object",)).
Each player keeps their own nicknames — other players are unaffected.
Admin Commands — Dialogue¶
Defined in commands/admin/dialogue_admin.py. All require perm(Builder) unless noted.
| Command | Aliases | Usage |
|---|---|---|
@load_dialogue |
@loadstory, @dialogue_load |
@load_dialogue <npc> = <dialogue_file> |
@test_dialogue |
@testdialogue, @dialogue_test |
@test_dialogue <npc> |
@clear_dialogue |
@cleardialogue, @dialogue_clear |
@clear_dialogue <target> or @clear_dialogue/all <npc> |
@list_dialogues |
@listdialogues, @dialogues |
@list_dialogues [npc_name] |
@dialoguestats |
@dialogue_stats |
@dialoguestats |
@cleanupdialogue |
@cleanup_dialogue |
@cleanupdialogue [days] or @cleanupdialogue disconnected |
@state_dialogue |
@dialoguestate, @dialogue_state |
@state_dialogue <player> [= type:name:value] |
@reset_dialogue_state |
@resetdialoguestate, @dialogue_reset |
@reset_dialogue_state <player> [= <npc>] |
@setconditional |
— | @setconditional <npc> = <on/off> (requires Admin) |
Player Dialogue State¶
@state_dialogue exposes the PlayerDialogueState manager (utils/dialogue/state.py), which tracks per-player:
- Flags — boolean dialogue flags (e.g.
helped_pearl) - Counters — numeric counters (e.g.
reputation) - Relationships — per-NPC relationship values
- Quest stages — quest progress keyed by quest ID
- Discovered topics — topics unlocked per NPC
- Completed dialogues — list of finished dialogue IDs
@state_dialogue Alice = flag:helped_pearl:true
@state_dialogue Bob = counter:reputation:50
@state_dialogue Carol = relationship:pearl:25
Admin Commands — Knowledge Repositories¶
Defined in commands/admin/knowledge_repo.py. Manages personal knowledge repos for personalized vocabulary learning (Issue #523). COPPA-compliant — only vocabulary content is synced, player IDs are anonymized.
| Command | Aliases | Lock | Usage |
|---|---|---|---|
@linkknowledge |
@linkk |
Builder | @linkknowledge <player> = <gitlab_url> [; <repo_key>] |
@unlinkknowledge |
@unlinkk |
Builder | @unlinkknowledge <player> [= all] |
@vocabstatus |
@vocab |
Player | @vocabstatus [player] |
@syncknowledge |
@synck |
Builder | @syncknowledge <player> [= force] |
Knowledge Repo Utilities (refactored)¶
As of this update, the utility functions extract_repo_key(), validate_gitlab_url(), and the SPYDER_API_URL constant have been extracted from knowledge_repo.py into a dedicated module:
commands/admin/knowledge_repo_utils.py
This refactor (Issue #523) resolves circular imports and Evennia initialization issues that occurred when Django URL patterns loaded during server startup. The functions are unchanged; only their import location has moved:
from commands.admin.knowledge_repo_utils import (
SPYDER_API_URL,
extract_repo_key,
validate_gitlab_url,
)
SPYDER_API_URL— Resolved fromNPC_API_SERVERenv var (default:https://app.glassumbrella.io).extract_repo_key(gitlab_url)— Extracts a short key from a GitLab URL (e.g."beckett"from…/beckett-the-builder-knowledge-repo).validate_gitlab_url(gitlab_url)— HEAD request to verify a GitLab repo is accessible. UsesGITLAB_URL_VALIDATION_TIMEOUT_SECONDS(5 s) fromutils/knowledge_repo_constants.py.
Admin Commands — Lore Export¶
Defined in commands/admin/lore_export.py. All require perm(Admin) or perm(Developer).
These commands export game state from the Evennia DB back to lore repository files on disk. Changes are written locally; use git to review and commit.
Prerequisite: Lore repositories must be configured in LORE_REPOS in settings.py, and room-description frontmatter must exist for room mapping resolution (via build_room_mapping).
| Command | Aliases | Usage |
|---|---|---|
@exportobjects |
@objexport |
@exportobjects <repo_key> [--dry-run] |
@exportnpcs |
@npcexport |
@exportnpcs <repo_key> [--dry-run] |
@exportrooms |
@roomexport |
@exportrooms <repo_key> [--dry-run] |
@export |
@exportlore, @loreexport |
@export <repo_key> [--dry-run] [--pull] |
@exportobjects¶
Exports objects tagged with object_id to YAML. Placement is room-aware: objects in rooms with a known folder mapping go to locations/{folder}/objects/{id}.yaml; otherwise worldbuilding/objects/{id}.yaml. Existing YAML files are merged — known fields are overwritten while unknown sections (meta, etc.) are preserved.
@exportnpcs¶
Exports all BaseNPC subclass instances to worldbuilding/npcs/{faction}/{name}/{name}.json. Serializes cascade data (asset_id, position, rotation, scale) and room_id. Companion/familiar NPCs are written with cascade.spawn_mode = "companion" so the UE5 room content editor skips them. Existing JSON files are merged, preserving dialogue refs, custom_attributes, and meta sections.
@exportrooms¶
Exports rooms with cascade layout data (room_layout) to room-data.json files. Creates new files for rooms not yet in the repo; updates existing ones while preserving metadata (book, storyline, category, etc.).
@export (combined)¶
Runs all three exporters in sequence (objects → NPCs → rooms). Supports --pull to run git pull on the lore repo before exporting.
Error cases¶
- Repository not found — If
repo_keyis not inLORE_REPOS, the command prints an error and lists available repos. - No room mappings — If no
room-description.mdfrontmatter files are found, a warning is shown but export proceeds (objects go toworldbuilding/fallback paths). - git pull failure —
@export --pullaborts ifgit pullfails (timeout, merge conflict, git not on PATH).
Admin Commands — L2R Artifact Regeneration¶
Defined in commands/admin/regen_l2r.py. Requires perm(Admin). Thin wrapper around utils.l2r_artifact_pipeline.regenerate — the pipeline does the heavy lifting (reading-level text adaptation, Spyder narration, S3 upload, lore-repo write-back); the command exposes it in-game for ad-hoc backfill or refresh. Issue #967, Stage 2.
| Command | Aliases | Usage |
|---|---|---|
@regen_l2r |
@regenl2r |
@regen_l2r <room_key> [<beat_id>] [<level>] |
Switches:
| Switch | Effect |
|---|---|
/force |
Regenerate even when text_hash matches the existing audio_artifacts entry (otherwise skipped as idempotent) |
/dry |
Dry-run: report what would change without writing back to lore-repo or calling Spyder beyond text adaptation |
Arguments:
room_key— lore-repo room directory (e.g."old-persey")beat_id(optional) — specific beat (e.g."rivers-patience"); omit for all beats in the roomlevel(optional) — specific reading level:emergent,early,transitional, orfluent; omit for all four sub-advanced levels.advancedis intentionally not supported — canonical artifacts already serve it.
@regen_l2r old-persey # All beats, all 4 levels
@regen_l2r old-persey rivers-patience # One beat, all 4 levels
@regen_l2r old-persey rivers-patience fluent # One beat, one level
@regen_l2r/force old-persey rivers-patience # Force even when text_hash matches
@regen_l2r/dry old-persey # Preview changes (no audio gen / write-back)
L2R Artifact Pipeline¶
The core pipeline (utils/l2r_artifact_pipeline.py) orchestrates per-(beat, level) artifact generation:
- Text adaptation — Computes the level-adapted text via
ReadingLevelService(same code path room descriptions use). - Idempotency check — SHA-256 hashes the adapted text; skips if the existing
audio_artifacts[level].text_hashalready matches (unlessforce=True). - Spyder narration — Calls
SpyderNarrationService.generate()which uploads audio to S3 and returns{audio_url, duration, alignment}. Alignment is stored inline (not as an S3 sibling) to avoid an HTTP round-trip at delivery time. - Lore-repo write-back — Writes the updated
audio_artifacts[level]block (text, audio_url, alignment, text_hash) back toworldbuilding/locations/{room_key}/l2r.yamlviaruamel.yamlround-trip mode (preserving comments and formatting). Busts the in-memory_l2r_cacheso subsequentdeliver_beatcalls pick up the new artifacts.
The pipeline generates artifacts for four reading levels: emergent, early, transitional, and fluent. The advanced level is intentionally omitted — canonical narration is already at advanced level, so resolve_beat_artifacts falling back to canonical for advanced readers ships the correct bundle without duplicating S3 storage.
Batch Regeneration Script¶
For bulk backfill across many rooms, prefer the headless script at scripts/regen_l2r.py (invoked via evennia shell <):
# Local
evennia shell < scripts/regen_l2r.py
# Production via SSH
ssh -i "$PROD_SSH_KEY_PATH" "$PROD_SSH_USER@$PROD_SSH_HOST" \
"cd $PROD_GAME_PATH && /home/ubuntu/.local/bin/evennia shell" \
< scripts/regen_l2r.py
Configuration is via environment variables:
| Variable | Default | Description |
|---|---|---|
REGEN_ROOM_KEY |
all DEFAULT_ROOMS |
Specific room key, or omit to process all known L2R rooms |
REGEN_BEAT_ID |
all beats | Specific beat ID |
REGEN_LEVEL |
all 4 sub-advanced | Specific reading level |
REGEN_FORCE |
false |
"1" / "true" to skip text_hash idempotency |
REGEN_DRY_RUN |
false |
"1" / "true" to skip Spyder calls + lore-repo write |
The script's DEFAULT_ROOMS list ("old-persey", "the-passion") defines rooms that currently ship L2R beats. When new rooms get L2R beats, they must be added to this list (or an auto-discovery pass implemented).
Error cases¶
- No
l2r.yaml— Ifget_l2r_beats(room_key)returns no data, the pipeline reports"no l2r.yaml for room"and returns an empty summary. - Beat not found — If
beat_idis specified but not present in the room'sl2r.yaml, the pipeline reports the error without processing other beats. - Unknown reading level — If
levelis not a validReadingLevelvalue, the pipeline returns immediately with an error. - Narration generation failure — Per-(beat, level) errors are captured and reported in the summary; the pipeline continues processing remaining beats/levels.
- Write-back failure — If the lore-repo path cannot be resolved (e.g.
LORE_REPOSnot configured or repo not on disk), aRuntimeErroris raised and reported. The admin must ensure the grit lore repository is registered inLORE_REPOSinsettings.pyand accessible on disk (or via theLORE_REPO_GRIT_PATHenvironment variable).
Data Files¶
Dialogue JSON files are stored under data/dialogues/ in TheSmithy. These files define passage-based conversations loaded via @load_dialogue and parsed by DialogueLoader / DialogueParser from utils/dialogue.
Lore Repository Dialogue Loading¶
NPCs can also load conditional dialogue directly from lore repositories at creation time or on demand, using get_content_path() from utils/lore_repo_config.py. This is an alternative to the @load_dialogue admin command — the NPC typeclass resolves the dialogue file path from a configured lore repo and stores the parsed JSON on its db attributes.
Pattern (as implemented by Arturo in typeclasses/npcs/arturo.py):
_load_arturo_dialogue()callsget_content_path("worldbuilding/npcs/human/arturo/dialogue/conditional.json", repo="grit")to resolve the file from the grit lore repo.setup_conditional_dialogue()loads the data and setsdb.conditional_dialogue,db.use_conditional_dialogue, anddb.dialogue_enabledon the NPC.at_object_creation()callssetup_conditional_dialogue()so dialogue is loaded when the NPC is first created.setup_conditional_dialogue()can also be called on existing NPC instances to refresh dialogue from the lore repo without recreating the object.
If the lore repo path cannot be resolved or the JSON fails to load, the NPC logs a warning and falls back to legacy dialogue.
Prerequisites: The target lore repository must be registered in LORE_REPOS in settings.py and accessible on disk (or via the LORE_REPO_<KEY>_PATH environment variable). See utils/lore_repo_config.py for path resolution details.
See Also¶
typeclasses/npcs/narrator.py—Narratorclass andWispStatestate machinecommands/narrative_commands.py— Player-facing narrative pacing commandscommands/interact.py—CmdInteractinteraction dispatch and pre-dialogue hookscommands/nickname.py—CmdNicknamepersonal object handles; nicks are expanded bycaller.search()(used byCmdInteractfor NPC target resolution) and by_denick()in inventory commands (#1066)storybook/services/narrator_services/mode_manager.py—NarratorModeManagerlifecycle servicestorybook/services/narrator_services/scene_navigator.py— Scene transition handlingtypeclasses/characters/player.py—PlayerCharacter.at_post_moveusesensure_idle()on narrator when player moves awaytypeclasses/mixins/interactable.py—InteractableMixincontract for interact-routable typeclasses (Telescope, L2RBeacon, MendableBoat, etc.)dialogue/strategies/teach_back_strategy.py—TeachBackDialogueStrategyfor teach-back prompt delivery viaTeachBackOrchestratorutils/dialogue/state.py—PlayerDialogueStatemanagerutils/dialogue/cleanup.py— Session cleanup and stats utilitiesutils/dialogue/npc_integration.py—StructuredDialogueMixinfor dynamic NPC dialogue supportutils/lore_repo_config.py— Lore repository path resolution and validationutils/knowledge_repo_constants.py— Constants for quiz API, URL validation, and answer evaluation thresholdscommands/room_sync.py—CmdGetRoomClusterroom-cluster data for the UE5 client; conditional exits (those with non-all()view locks) are included in the exits array with"hidden": trueand anasset_configcontaining"start_hidden": "true"and"visibility_mode": "none", delegating client-side hiding toSpawnExitPortalviaParseVisibilityFromConfigtypeclasses/objects/l2r_beacon.py—L2RBeaconworld-anchored narrative delivery; crossing completion sendsexit_revealedwithasset_configvia delayed OOB (reactor.callLater)commands/admin/regen_l2r.py—CmdRegenL2Radmin command for in-game L2R artifact regenerationscripts/regen_l2r.py— Headless batch script for bulk L2R artifact regeneration viaevennia shellutils/l2r_artifact_pipeline.py— L2R artifact pipeline: text adaptation, Spyder narration, S3 upload, lore-repo write-backutils/l2r_service.py— L2R beat delivery service;resolve_beat_artifactsresolves per-level audio/text/alignment bundles produced by the pipeline
Feature Repositories¶
TheSmithy→ project_id:the-smithy1/TheSmithy
Code Paths to Explore¶
data/dialogues/**inTheSmithycommands/**inTheSmithydialogue/**inTheSmithy