Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ name = "griff"
path = "src/main.rs"

[dependencies]
griff-core = { path = "../core" }
clap = { workspace = true }
serde_json = { workspace = true }
griff-core = { path = "../core" }
griff-pattern = { path = "../pattern" }
griff-swang = { path = "../swang" }
clap = { workspace = true }
serde_json = { workspace = true }

[dev-dependencies]
# Used only by the S0 characterization suite to synthesise `.mid` fixtures
Expand Down
1 change: 1 addition & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
#![doc(hidden)]

pub mod generation_input;
pub mod rhythm_pattern;

pub use griff_core::generation_input::primary_voice_note_count;
157 changes: 143 additions & 14 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{
use clap::{Parser, Subcommand};
use griff_cli::generation_input::{load_corpus_material, CorpusMaterial, GenerationInputError};
use griff_cli::primary_voice_note_count;
use griff_cli::rhythm_pattern;
use griff_core::generation_input::{ranked_candidates, GenerationAsk, RankedSet};
use griff_core::{
boundary,
Expand Down Expand Up @@ -123,6 +124,46 @@ enum Command {
/// gesture statistics (wall-to-wall writing).
#[arg(long)]
no_gesture: bool,
/// ASCII kernel literal (S16 transport syntax): rows of `X`/`.`
/// separated by `/`, e.g. `X.X/XX./.XX`. Compiles into an explicit
/// rhythm palette that overrides corpus and source rhythms.
#[arg(
long,
value_name = "KERNEL",
requires = "rhythm_fractal_depth",
requires = "rhythm_traversal",
requires = "rhythm_unit"
)]
rhythm_kernel: Option<String>,
/// Exact fractal expansion depth (depth 0 is the kernel itself).
#[arg(long, value_name = "DEPTH", requires = "rhythm_kernel")]
rhythm_fractal_depth: Option<u8>,
/// Density decay in basis points (0..=10000); requires --rhythm-seed.
#[arg(
long,
value_name = "BPS",
requires = "rhythm_kernel",
requires = "rhythm_seed"
)]
rhythm_density_bps: Option<u32>,
/// Structural pruning seed — independent of --seed by law.
#[arg(long, value_name = "SEED", requires = "rhythm_kernel")]
rhythm_seed: Option<u64>,
/// How the expansion reads into a line: row-major or snake.
#[arg(long, value_name = "ORDER", requires = "rhythm_kernel")]
rhythm_traversal: Option<rhythm_pattern::TraversalChoice>,
/// Time unit per pattern slot, e.g. 1/16.
#[arg(long, value_name = "NOTE", requires = "rhythm_kernel")]
rhythm_unit: Option<String>,
/// Cell budget for the expansion (CLI default: 4096).
#[arg(long, value_name = "CELLS", requires = "rhythm_kernel")]
rhythm_max_cells: Option<u64>,
/// Incomplete-final-bar policy: reject (default) or rest-pad.
#[arg(long, value_name = "POLICY", requires = "rhythm_kernel")]
rhythm_tail: Option<rhythm_pattern::TailChoice>,
/// Write the versioned expansion artifact (JSON) to this path.
#[arg(long, value_name = "PATH", requires = "rhythm_kernel")]
emit_rhythm_expansion: Option<PathBuf>,
},

/// Arrange a complementary part (S13) for a tab's primary track — a second
Expand Down Expand Up @@ -209,17 +250,42 @@ fn run() -> Result<(), CliError> {
corpus,
candidates,
no_gesture,
} => cmd_generate(
&input,
&output,
&GenerateOpts {
seed,
bars,
corpus: corpus.as_deref(),
candidates,
no_gesture,
},
),
rhythm_kernel,
rhythm_fractal_depth,
rhythm_density_bps,
rhythm_seed,
rhythm_traversal,
rhythm_unit,
rhythm_max_cells,
rhythm_tail,
emit_rhythm_expansion,
} => {
// clap's `requires` guarantees depth/traversal/unit accompany the
// kernel; the unwraps below never fire without it.
let rhythm = rhythm_kernel.map(|kernel| rhythm_pattern::RhythmPatternArgs {
kernel,
fractal_depth: rhythm_fractal_depth.unwrap_or(0),
density_bps: rhythm_density_bps,
rhythm_seed,
traversal: rhythm_traversal.unwrap_or(rhythm_pattern::TraversalChoice::RowMajor),
unit: rhythm_unit.unwrap_or_else(|| "1/16".to_owned()),
max_cells: rhythm_max_cells.unwrap_or(4096),
tail: rhythm_tail.unwrap_or(rhythm_pattern::TailChoice::Reject),
});
cmd_generate(
&input,
&output,
&GenerateOpts {
seed,
bars,
corpus: corpus.as_deref(),
candidates,
no_gesture,
rhythm: rhythm.as_ref(),
emit_rhythm_expansion: emit_rhythm_expansion.as_deref(),
},
)
}
Command::Complement {
input,
output,
Expand Down Expand Up @@ -591,6 +657,8 @@ fn cmd_generate(input: &Path, output: &Path, opts: &GenerateOpts<'_>) -> Result<
corpus,
candidates,
no_gesture,
rhythm,
emit_rhythm_expansion,
} = *opts;
let data = fs::read(input)?;
let score = import::import_score_auto(&data)?;
Expand All @@ -600,6 +668,30 @@ fn cmd_generate(input: &Path, output: &Path, opts: &GenerateOpts<'_>) -> Result<
print_corpus_summary(m, no_gesture);
}

// The pattern plan compiles before any pitch generation, so the artifact
// shows the structural delta in isolation (spec §1.14).
let plan = rhythm
.map(|args| rhythm_pattern::compile_pattern(args, &score, bars))
.transpose()?;
if let (Some(plan), Some(path)) = (&plan, emit_rhythm_expansion) {
// The artifact writes before generation: aliasing the input would
// clobber the user's tab, aliasing the output would be silently
// overwritten by the MIDI moments later. Canonical paths when they
// resolve, lexical comparison otherwise (the output may not exist).
let clashes = |other: &Path| -> bool {
match (fs::canonicalize(path), fs::canonicalize(other)) {
(Ok(a), Ok(b)) => a == b,
_ => path == other,
}
};
if clashes(input) || clashes(output) {
return Err(CliError::Argument(
"--emit-rhythm-expansion must not alias INPUT or OUTPUT".to_owned(),
));
}
fs::write(path, &plan.artifact_json)?;
Comment thread
PhysShell marked this conversation as resolved.
}

// The shared compiler: the cockpit's Generate panel enters here too, so the
// two cannot drift.
let set = ranked_candidates(
Expand All @@ -611,18 +703,22 @@ fn cmd_generate(input: &Path, output: &Path, opts: &GenerateOpts<'_>) -> Result<
variants_per_strategy: candidates,
gesture: !no_gesture,
},
None,
plan.as_ref().map(|p| p.templates.as_slice()),
)?;
let RankedSet {
ranked,
base,
source_rhythms,
rhythm_explicit,
gesture,
policy,
..
} = &set;

print_rhythm_diagnostics(source_rhythms, &base.constraints, gesture.is_some());
if *rhythm_explicit {
print_explicit_rhythm_diagnostics(source_rhythms, &base.constraints);
} else {
print_rhythm_diagnostics(source_rhythms, &base.constraints, gesture.is_some());
}

let winner = ranked
.first()
Expand Down Expand Up @@ -679,6 +775,27 @@ fn print_corpus_summary(m: &CorpusMaterial, no_gesture: bool) {
/// no corpus, `source_rhythms` is the input's own first-bar rhythm (one
/// template, so `1 loaded / 1 effective`), so the two A/B legs are directly
/// comparable. `effective == 0` means the quarter fallback was used.
/// Prints the explicit palette's diagnostics — uncompressed by law (ADR-0029
/// §7): every template counts, silent bars included, no quarter fallback.
fn print_explicit_rhythm_diagnostics(
palette: &[generate::RhythmTemplate],
constraints: &generate::GenerationConstraints,
) {
let Ok(bar_duration) =
generate::bar_duration_ticks(constraints.time_signature, constraints.ticks_per_quarter)
else {
return;
};
let diag = generate::explicit_rhythm_diagnostics(palette, bar_duration);
let hexes: Vec<String> = diag.fingerprints.iter().map(|h| format!("{h:x}")).collect();
println!(
"rhythm: explicit palette, {count} templates — per-bar strategies rotate them verbatim, RepeatVariation holds the first — over {bars} bars; grids[{count}] {fps}",
count = diag.effective,
bars = constraints.bar_count,
fps = hexes.join(" "),
);
}

fn print_rhythm_diagnostics(
source_rhythms: &[generate::RhythmTemplate],
constraints: &generate::GenerationConstraints,
Expand Down Expand Up @@ -745,6 +862,10 @@ struct GenerateOpts<'a> {
candidates: usize,
/// Skip gesture carving even when the corpus provides stats.
no_gesture: bool,
/// The compiled `--rhythm-*` ask, when a kernel was given.
rhythm: Option<&'a rhythm_pattern::RhythmPatternArgs>,
/// Where to write the expansion artifact, when asked.
emit_rhythm_expansion: Option<&'a Path>,
}

/// Arranges a complementary part B (S13) for the primary track of `input` and
Expand Down Expand Up @@ -1495,6 +1616,7 @@ enum CliError {
Set(rerank::SetError),
Corpus(String),
Complement(complement::ComplementError),
Pattern(rhythm_pattern::PatternDiagnostic),
}

impl fmt::Display for CliError {
Expand All @@ -1511,6 +1633,7 @@ impl fmt::Display for CliError {
Self::Set(e) => write!(f, "candidate set error: {e:?}"),
Self::Corpus(msg) => write!(f, "corpus error: {msg}"),
Self::Complement(e) => write!(f, "complement error: {e:?}"),
Self::Pattern(d) => write!(f, "{d}"),
}
}
}
Expand Down Expand Up @@ -1566,6 +1689,12 @@ impl From<complement::ComplementError> for CliError {
}
}

impl From<rhythm_pattern::PatternDiagnostic> for CliError {
fn from(d: rhythm_pattern::PatternDiagnostic) -> Self {
Self::Pattern(d)
}
}

// ── tests ─────────────────────────────────────────────────────────────────────

/// Red → green for the Codex P2 finding on PR #36: ensemble part selection
Expand Down
Loading
Loading