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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ At its core, Renderflow models every transformation as a **directed acyclic grap
- 🎯 **Optimization modes** — Choose Speed, Quality, Balanced, or Pareto-optimal path selection
- 🔄 **Transform pipeline** — Pluggable in-memory content transforms (built-in and custom)
- 🤖 **AI transforms** — Ollama and OpenAI-compatible LLM integration with local caching
- 🧬 **Artifact DNA** — Optional, local-first visual/layout characteristics with strict provenance, hygiene, and similarity guidance
- 🖼️ **Image conversion** — FFmpeg-backed format conversion across 80+ image formats (JPEG, PNG, WebP, AVIF, HEIC, EXR, and more)
- 🎵 **Audio conversion** — FFmpeg-backed format conversion across 40+ audio formats (WAV, FLAC, MP3, AAC, Opus, and more)
- 🎬 **Whole-file video delivery** — Typed HandBrake presets with bounded execution, provenance, and explicit Aniflow temporal boundaries
Expand Down
40 changes: 38 additions & 2 deletions crates/renderflow-core/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use clap::Parser;
use tracing::info;

use crate::cli::{
AiCommands, AiSkillCommands, Cli, Commands, EbookCommands, GraphCommands, LuluCommands,
PluginCommands, PublicationCommands, SpecCommands, ToolCommands, VideoCommands,
AiCommands, AiSkillCommands, Cli, Commands, DnaCommands, EbookCommands, GraphCommands,
LuluCommands, PluginCommands, PublicationCommands, SpecCommands, ToolCommands, VideoCommands,
};
use crate::video::HandBrakeLimits;
use crate::{commands, transforms};
Expand Down Expand Up @@ -137,6 +137,42 @@ pub fn run_cli(cli: Cli) -> Result<()> {
AiCommands::Doctor { ollama_endpoint } => commands::ai::run_doctor(&ollama_endpoint)?,
AiCommands::Cache { path } => commands::ai::run_cache(&path)?,
},
Some(Commands::Dna { subcommand }) => match subcommand {
DnaCommands::Extract {
input,
output,
store,
media_type,
format,
max_source_bytes,
max_observations,
allow_ai,
allow_network,
allow_remote,
protected_reference,
} => commands::dna::run_extract(
&input,
output.as_deref(),
&store,
media_type.as_deref(),
&format,
max_source_bytes,
max_observations,
allow_ai,
allow_network,
allow_remote,
&protected_reference,
)?,
DnaCommands::Validate { input, format } => {
commands::dna::run_validate(&input, &format)?
}
DnaCommands::Compare {
left,
right,
output,
format,
} => commands::dna::run_compare(&left, &right, output.as_deref(), &format)?,
},
Some(Commands::Graph { subcommand }) => match subcommand {
GraphCommands::Plan {
config,
Expand Down
79 changes: 79 additions & 0 deletions crates/renderflow-core/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,20 @@ pub enum Commands {
subcommand: AiCommands,
},

/// Extract, validate, and compare versioned Artifact DNA
#[command(
subcommand_required = true,
arg_required_else_help = true,
after_help = "Examples:\n \
renderflow dna extract --input cover.svg --output cover.dna.json\n \
renderflow dna validate --input cover.dna.json\n \
renderflow dna compare --left cover.dna.json --right divider.dna.json"
)]
Dna {
#[command(subcommand)]
subcommand: DnaCommands,
},

/// Inspect, visualize, and export the transformation execution plan
///
/// These commands expose the canonical execution plan that the planner
Expand Down Expand Up @@ -676,6 +690,71 @@ pub enum AiSkillCommands {
},
}

/// Subcommands for optional, versioned Artifact DNA.
#[derive(Subcommand)]
pub enum DnaCommands {
/// Extract local deterministic DNA from an immutable source artifact
Extract {
/// Source artifact to inspect
#[arg(long, value_name = "FILE")]
input: String,
/// Optional output file; omit to print to standard output
#[arg(long, value_name = "FILE")]
output: Option<String>,
/// Content-addressed artifact-store root
#[arg(long, default_value = ".renderflow/dna-artifacts", value_name = "DIR")]
store: String,
/// Source-reported media type used as an intake signal
#[arg(long, value_name = "TYPE")]
media_type: Option<String>,
/// Serialization format: json (default) or yaml
#[arg(long, default_value = "json", value_name = "FORMAT")]
format: String,
/// Maximum source size permitted for extraction
#[arg(long, default_value_t = 67_108_864, value_name = "BYTES")]
max_source_bytes: u64,
/// Maximum number of observations permitted in the result
#[arg(long, default_value_t = 512, value_name = "COUNT")]
max_observations: usize,
/// Explicitly allow registered AI-assisted extractors
#[arg(long)]
allow_ai: bool,
/// Explicitly allow extractors that require network access
#[arg(long)]
allow_network: bool,
/// Explicitly allow registered remote extractors
#[arg(long, requires_all = ["allow_ai", "allow_network"])]
allow_remote: bool,
/// Protected artist, creator, brand, franchise, or work name to omit
#[arg(long, value_name = "TERM")]
protected_reference: Vec<String>,
},
/// Validate a versioned Artifact DNA JSON document
Validate {
/// Artifact DNA JSON document
#[arg(long, value_name = "FILE")]
input: String,
/// Output format: text (default), json, or yaml
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
},
/// Compare shared, similarity-eligible descriptive dimensions
Compare {
/// Left Artifact DNA JSON document
#[arg(long, value_name = "FILE")]
left: String,
/// Right Artifact DNA JSON document
#[arg(long, value_name = "FILE")]
right: String,
/// Optional report output; omit to print to standard output
#[arg(long, value_name = "FILE")]
output: Option<String>,
/// Serialization format: json (default) or yaml
#[arg(long, default_value = "json", value_name = "FORMAT")]
format: String,
},
}

/// Subcommands for `renderflow graph`.
#[derive(Subcommand)]
pub enum GraphCommands {
Expand Down
127 changes: 127 additions & 0 deletions crates/renderflow-core/src/commands/dna.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//! Handlers for optional Artifact DNA extraction, validation, and comparison.

use std::path::Path;

use anyhow::{Context, Result};
use serde::Serialize;

use crate::artifact::ArtifactStore;
use crate::dna::{
ArtifactDna, ArtifactDnaComparison, ArtifactDnaEngine, DnaExtractionPolicy,
DnaExtractionStatus, DnaProtectedReferenceRule,
};
use crate::intake::{IntakeEngine, IntakeRequest};

#[allow(clippy::too_many_arguments)]
pub fn run_extract(
input: &str,
output: Option<&str>,
store_path: &str,
media_type: Option<&str>,
format: &str,
max_source_bytes: u64,
max_observations: usize,
allow_ai: bool,
allow_network: bool,
allow_remote: bool,
protected_references: &[String],
) -> Result<()> {
let store = ArtifactStore::new(store_path)?;
let mut request = IntakeRequest::from_path(input);
if let Some(media_type) = media_type {
request = request.with_media_type(media_type);
}
let intake = IntakeEngine::new().intake(&request, &store)?;
let mut policy = DnaExtractionPolicy::explicit_local();
policy.max_source_bytes = max_source_bytes;
policy.max_observations = max_observations;
policy.allow_ai = allow_ai;
policy.allow_network = allow_network;
policy.allow_remote = allow_remote;
policy.protected_references = protected_references
.iter()
.map(|term| DnaProtectedReferenceRule {
term: term.clone(),
descriptive_replacement: None,
})
.collect();
let outcome = ArtifactDnaEngine::with_builtins().extract(&intake.source, &store, &policy)?;
let Some(dna) = outcome.dna else {
anyhow::bail!(
"Artifact DNA extraction {:?}: {}",
outcome.status,
outcome.diagnostics.join("; ")
);
};
let serialized = serialize(&dna, format)?;
if let Some(output) = output {
std::fs::write(output, &serialized)
.with_context(|| format!("failed to write Artifact DNA '{}'", output))?;
let artifact_id = outcome
.artifact
.as_ref()
.map(|artifact| artifact.id().to_string())
.unwrap_or_else(|| "unavailable".to_string());
println!(
"Artifact DNA {:?}: {} (stored as {})",
outcome.status,
Path::new(output).display(),
artifact_id
);
} else {
print!("{serialized}");
}
if outcome.status == DnaExtractionStatus::Partial {
for diagnostic in outcome.diagnostics {
eprintln!("warning: {diagnostic}");
}
}
Ok(())
}

pub fn run_validate(input: &str, format: &str) -> Result<()> {
let dna = ArtifactDna::load(input)?;
dna.validate()?;
match format {
"text" => println!(
"valid {}: {} observations across {} modalities",
dna.schema_version,
dna.observations.len(),
dna.modalities.len()
),
"json" | "yaml" => {
let report = serde_json::json!({
"schema_version": dna.schema_version,
"valid": true,
"observations": dna.observations.len(),
"modalities": dna.modalities,
});
print!("{}", serialize(&report, format)?);
}
_ => anyhow::bail!("unknown output format '{format}'; expected text, json, or yaml"),
}
Ok(())
}

pub fn run_compare(left: &str, right: &str, output: Option<&str>, format: &str) -> Result<()> {
let left = ArtifactDna::load(left)?;
let right = ArtifactDna::load(right)?;
let comparison = ArtifactDnaComparison::compare(&left, &right)?;
let serialized = serialize(&comparison, format)?;
if let Some(output) = output {
std::fs::write(output, &serialized)
.with_context(|| format!("failed to write DNA comparison '{}'", output))?;
println!("Artifact DNA comparison written to {output}");
} else {
print!("{serialized}");
}
Ok(())
}

fn serialize(value: &impl Serialize, format: &str) -> Result<String> {
match format {
"json" => Ok(format!("{}\n", serde_json::to_string_pretty(value)?)),
"yaml" => Ok(serde_yaml_ng::to_string(value)?),
_ => anyhow::bail!("unknown output format '{format}'; expected json or yaml"),
}
}
1 change: 1 addition & 0 deletions crates/renderflow-core/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod ai;
pub mod audit;
pub mod build;
pub mod dna;
pub mod ebook;
pub mod graph;
pub mod inspect;
Expand Down
Loading
Loading