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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to this project will be documented in this file.

## [Unreleased]

### Features

- Add a typed, bounded HandBrakeCLI adapter for whole-file video derivatives,
with progress, cancellation, provenance, Flow projection, and an explicit
Aniflow temporal-workflow boundary.

## [0.1.0] - 2026-03-30

### Features
Expand Down Expand Up @@ -45,4 +53,3 @@ All notable changes to this project will be documented in this file.

- Update select_strategy to accept references instead of owned values


70 changes: 68 additions & 2 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ At its core, Renderflow models every transformation as a **directed acyclic grap
- 🤖 **AI transforms** — Ollama and OpenAI-compatible LLM integration with local caching
- 🖼️ **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
- 🧩 **Custom templates** — Per-output Jinja2-compatible templates via [Tera](https://keats.github.io/tera/)
- 🔌 **Plugin system** — Register external transform executors at runtime without modifying core
- 🖼️ **Asset management** — Automatically resolves and validates image paths
Expand Down
1 change: 1 addition & 0 deletions crates/renderflow-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ autobins = false

[dependencies]
clap = { version = "4", features = ["derive"] }
ctrlc = "3.4"

serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
29 changes: 26 additions & 3 deletions crates/renderflow-core/data/adapter-packs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,30 @@ providers:
upstream: https://ffmpeg.org/
rationale: Existing local image/audio adapter; video and subtitle capabilities remain graph-advertised only when an executable edge exists.

- id: adapter.media.handbrake
runtime_tool: tool.handbrake
name: HandBrake bounded whole-file video adapter
families: [audio_video]
maturity: integrated
selection_priority: 20
capabilities: [video.transcode.whole_file]
input_media_types: [video/*]
output_media_types: [video/mp4]
determinism: configuration_dependent
locality: local
fidelity: lossy
execution: *bounded_local
configuration_schema:
preset: allowlisted_enum
timeout_seconds: bounded_integer
capture_limit_bytes: bounded_integer
progress_interval_ms: bounded_integer
maximum_output_bytes: bounded_integer
validation: [validator.core.non_empty, validator.video.mp4_file_type_box]
provenance: [provider_id, provider_version, argv_digest, input_digest, output_digest]
upstream: https://handbrake.fr/
rationale: HandBrakeCLI is integrated for typed whole-file delivery transforms; temporal decomposition, boundary selection, ordering, and reconstruction remain Aniflow-owned.

- id: adapter.pdf.wkhtmltopdf
runtime_tool: tool.wkhtmltopdf
name: wkhtmltopdf compatibility adapter
Expand Down Expand Up @@ -393,7 +417,6 @@ evaluations:
rationale: GUI-centric cross-platform footprint is a poor core dependency; prefer bounded FFmpeg and text-native subtitle adapters.
- candidate: HandBrakeCLI
families: [audio_video]
decision: defer
decision: adapt
upstream: https://handbrake.fr/
rationale: "Keep video transcode ownership coordinated with Aniflow through issue #345."
follow_up: "#345"
rationale: Adopt a narrow allowlisted whole-file preset surface through renderflow.process/v1 while retaining all temporal workflow ownership in Aniflow.
22 changes: 22 additions & 0 deletions crates/renderflow-core/data/tool-registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ tools:
license_notes: "FFmpeg licensing is build-dependent; consult the exact distributed build."
distribution_notes: "Used by Renderflow audio, image, and future video adapters."

- id: tool.handbrake
name: HandBrakeCLI
discovery:
kind: executable
candidates: [HandBrakeCLI]
version_args: [--version]
version:
min_inclusive: "1.6.0"
operating_systems: [linux, macos, windows]
capabilities:
- video.transcode.whole_file
input_media_types:
- video/*
output_media_types:
- video/mp4
determinism: configuration_dependent
locality: local
fidelity: lossy
support_tier: optional
license_notes: "HandBrake is GPL-2.0-only; consult the exact distributed binary and bundled libraries."
distribution_notes: "Optional bounded whole-file delivery adapter; Aniflow owns temporal segmentation and reconstruction."

- id: tool.wkhtmltopdf
name: wkhtmltopdf
discovery:
Expand Down
48 changes: 47 additions & 1 deletion crates/renderflow-core/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use tracing::info;

use crate::cli::{
AiCommands, Cli, Commands, EbookCommands, GraphCommands, LuluCommands, PluginCommands,
PublicationCommands, SpecCommands, ToolCommands,
PublicationCommands, SpecCommands, ToolCommands, VideoCommands,
};
use crate::video::HandBrakeLimits;
use crate::{commands, transforms};

/// Initialize logging for a Renderflow CLI run.
Expand Down Expand Up @@ -217,6 +218,51 @@ pub fn run_cli(cli: Cli) -> Result<()> {
)?,
},
},
Some(Commands::Video { subcommand }) => match subcommand {
VideoCommands::Capabilities { format } => commands::video::run_capabilities(&format)?,
VideoCommands::Plan {
input,
output,
preset,
timeout_seconds,
capture_limit_bytes,
progress_interval_ms,
maximum_output_bytes,
format,
} => commands::video::run_plan(
&input,
&output,
preset.into(),
HandBrakeLimits {
timeout_seconds,
capture_limit_bytes,
progress_interval_ms,
maximum_output_bytes,
},
&format,
)?,
VideoCommands::Transcode {
input,
output,
preset,
timeout_seconds,
capture_limit_bytes,
progress_interval_ms,
maximum_output_bytes,
format,
} => commands::video::run_transcode(
&input,
&output,
preset.into(),
HandBrakeLimits {
timeout_seconds,
capture_limit_bytes,
progress_interval_ms,
maximum_output_bytes,
},
&format,
)?,
},
Some(Commands::Capabilities {
format,
transforms,
Expand Down
80 changes: 79 additions & 1 deletion crates/renderflow-core/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use clap::{Parser, Subcommand};
use clap::{Parser, Subcommand, ValueEnum};

use crate::optimization::OptimizationMode;
use crate::video::HandBrakePreset;

/// Spec-driven document rendering engine
#[derive(Parser)]
Expand Down Expand Up @@ -280,6 +281,13 @@ pub enum Commands {
subcommand: PublicationCommands,
},

/// Plan and execute bounded whole-file video transforms.
#[command(subcommand_required = true, arg_required_else_help = true)]
Video {
#[command(subcommand)]
subcommand: VideoCommands,
},

/// List stable provider capability IDs and their implementations.
Capabilities {
/// Output format: text (default), json, or yaml.
Expand Down Expand Up @@ -314,6 +322,76 @@ pub enum Commands {
},
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum VideoPresetArgument {
#[value(name = "fast-720p30")]
Fast720p30,
#[value(name = "fast-1080p30")]
Fast1080p30,
#[value(name = "creator-1080p60")]
Creator1080p60,
#[value(name = "production-standard")]
ProductionStandard,
}

impl From<VideoPresetArgument> for HandBrakePreset {
fn from(value: VideoPresetArgument) -> Self {
match value {
VideoPresetArgument::Fast720p30 => Self::Fast720p30,
VideoPresetArgument::Fast1080p30 => Self::Fast1080p30,
VideoPresetArgument::Creator1080p60 => Self::Creator1080p60,
VideoPresetArgument::ProductionStandard => Self::ProductionStandard,
}
}
}

#[derive(Subcommand)]
pub enum VideoCommands {
/// Print typed presets, ownership boundaries, and interchange contracts.
Capabilities {
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
},
/// Normalize and hash a whole-file HandBrake request without executing it.
Plan {
#[arg(long, value_name = "FILE")]
input: String,
#[arg(long, value_name = "FILE")]
output: String,
#[arg(long, value_enum, default_value_t = VideoPresetArgument::Fast1080p30)]
preset: VideoPresetArgument,
#[arg(long, default_value_t = 7_200)]
timeout_seconds: u64,
#[arg(long, default_value_t = 262_144)]
capture_limit_bytes: usize,
#[arg(long, default_value_t = 1_000)]
progress_interval_ms: u64,
#[arg(long, default_value_t = 21_474_836_480)]
maximum_output_bytes: u64,
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
},
/// Execute a bounded HandBrakeCLI whole-file transform.
Transcode {
#[arg(long, value_name = "FILE")]
input: String,
#[arg(long, value_name = "FILE")]
output: String,
#[arg(long, value_enum, default_value_t = VideoPresetArgument::Fast1080p30)]
preset: VideoPresetArgument,
#[arg(long, default_value_t = 7_200)]
timeout_seconds: u64,
#[arg(long, default_value_t = 262_144)]
capture_limit_bytes: usize,
#[arg(long, default_value_t = 1_000)]
progress_interval_ms: u64,
#[arg(long, default_value_t = 21_474_836_480)]
maximum_output_bytes: u64,
#[arg(long, default_value = "text", value_name = "FORMAT")]
format: String,
},
}

#[derive(Subcommand)]
pub enum PublicationCommands {
/// Evaluate candidates with the bundled, offline Lulu provider pack.
Expand Down
Loading
Loading