//! Workflow templates (#192): load, validate, and fill in an API-format //! ComfyUI workflow for one photo. //! //! Nodes are found by `_meta.title`, never by id, so re-exporting after //! editing the graph keeps working. use std::collections::BTreeMap; use std::path::Path; use anyhow::{Context as _, Result, bail}; use serde_json::{Map, Value, json}; use super::config::{LoraConfig, LoraRequest, PromptMode, TemplateConfig}; pub const TITLE_SOURCE: &str = "imageapi:source"; pub const TITLE_SAVE: &str = "imageapi:save"; pub const TITLE_PROMPT: &str = "imageapi:prompt"; pub const TITLE_REFERENCE: &str = "imageapi:reference"; /// `imageapi:lora:`: a LoRA loader the app can switch off (#218). pub const TITLE_LORA_PREFIX: &str = "imageapi:lora:"; /// `imageapi:lora-slot::high|low`: a LoRA loader the app fills with a /// LoRA picked from the catalog (#220); unused slots run at strength 0. pub const TITLE_SLOT_PREFIX: &str = "imageapi:lora-slot:"; /// LoRA loader inputs a switch scales (0 = off). `strength_model` is /// required (LoraLoaderModelOnly); `strength_clip` is scaled when present /// (LoraLoader). const LORA_STRENGTH_INPUTS: &[&str] = &["strength_model", "strength_clip"]; /// Prompt-node inputs, in the order they are tried. const PROMPT_INPUTS: &[&str] = &["text", "prompt", "value"]; /// What a template produces, from its `imageapi:save` node's class. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "lowercase")] pub enum OutputKind { /// Save Image Version: PNG versions. Image, /// Save Video Version: an MP4 (e.g. a clip animated from the photo). Video, } /// The save node class that makes a template's output a video. pub const VIDEO_SAVE_CLASS: &str = "ImageApiSaveVideoVersion"; /// Output nodes that write into ComfyUI's output folder on every API run. const STRAY_OUTPUT_CLASSES: &[&str] = &[ "SaveImage", "SaveAnimatedWEBP", "SaveAnimatedPNG", "SaveWEBM", "SaveVideo", "SaveGLB", ]; #[derive(Debug, Clone)] pub struct Template { pub cfg: TemplateConfig, workflow: Map, source_id: String, save_id: String, /// `(node id, input name)` of the prompt text. prompt: Option<(String, &'static str)>, reference_id: Option, /// Declared LoRA switches that have nodes, in `enhance.toml` order. loras: Vec, /// Pickable-LoRA slots, by slot number. slots: Vec, /// `lora_name`s the workflow loads outside the slots (lightx2v, switch /// LoRAs): never offered in the catalog. fixed_loras: std::collections::HashSet, /// Image or video, from the save node's class. pub output: OutputKind, /// Startup warnings (stray output nodes). pub warnings: Vec, } #[derive(Debug, Clone)] struct LoraSwitch { cfg: LoraConfig, /// Every node titled `imageapi:lora:`, with its exported strengths /// (`(input, value)`), which the switch's scale multiplies. nodes: Vec<(String, Vec<(&'static str, f64)>)>, } /// One pickable-LoRA slot: its high- and low-noise loader nodes, each with /// its exported strengths (a pick's scale multiplies them). #[derive(Debug, Clone)] struct Slot { high: Vec<(String, Vec<(&'static str, f64)>)>, low: Vec<(String, Vec<(&'static str, f64)>)>, } /// A LoRA picked for one slot: its two files and the resolved scale. #[derive(Debug, Clone, PartialEq)] pub struct SlotPick { pub high: String, pub low: String, pub scale: f64, } /// Why a request can't be turned into a ComfyUI prompt. The message is for /// the caller, so it names the problem in the user's terms. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FillError(pub String); impl std::fmt::Display for FillError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } /// A filled-in prompt plus whether the reference node had to be removed /// (so a ComfyUI rejection can be reported as "needs a reference photo"). #[derive(Debug)] pub struct Filled { pub prompt: Value, pub dropped_reference: bool, /// Every switch's resolved scale (0 = off; request, else its default); /// stored on the job so a retry repeats it. Empty without switches. pub loras: BTreeMap, } impl Template { pub fn load(cfg: &TemplateConfig, base_dir: &Path) -> Result