diff --git a/crates/openplotva-app/src/dialog_messages.rs b/crates/openplotva-app/src/dialog_messages.rs index 32ce2543..fc19111f 100644 --- a/crates/openplotva-app/src/dialog_messages.rs +++ b/crates/openplotva-app/src/dialog_messages.rs @@ -5896,6 +5896,7 @@ mod tests { photo_file_id: "latest-photo-file".to_owned(), photo_urls: Vec::new(), photo_file_ids: vec!["latest-photo-file".to_owned()], + caption: String::new(), }] ); // Album delivery: one placeholder frame (single editor) filled with the diff --git a/crates/openplotva-app/src/dialog_tools.rs b/crates/openplotva-app/src/dialog_tools.rs index a8d8cdce..cd1b4635 100644 --- a/crates/openplotva-app/src/dialog_tools.rs +++ b/crates/openplotva-app/src/dialog_tools.rs @@ -4820,7 +4820,9 @@ mod tests { step: "draw_image".to_owned(), prompt: input.message.text.clone(), caption: "Combined photos".to_owned(), - file_ids: (11..=20) + file_ids: (11..) + .take(MAX_IMAGE_EDIT_SOURCE_IMAGES - 1) + .chain(std::iter::once(20)) .map(|message_id| format!("message_{message_id}_image_1")) .collect(), ..Default::default() @@ -4960,7 +4962,7 @@ mod tests { assert!( selected .expect_err("too many sources") - .contains("at most 10") + .contains(&format!("at most {MAX_IMAGE_EDIT_SOURCE_IMAGES}")) ); } } diff --git a/crates/openplotva-app/src/image_jobs.rs b/crates/openplotva-app/src/image_jobs.rs index 1b57762a..71c79aef 100644 --- a/crates/openplotva-app/src/image_jobs.rs +++ b/crates/openplotva-app/src/image_jobs.rs @@ -73,8 +73,9 @@ pub type ImageJobEffectFuture<'a, T> = Pin + Send + ' pub const AIFARM_DRAW_API_SERVICE_NAME: &str = "draw-api"; pub const AIFARM_DRAW_API_ENDPOINT_NAME: &str = "generate"; -pub const AIFARM_DRAW_API_FLUX3_ENDPOINT_NAME: &str = "flux3_generate"; pub const AIFARM_DRAW_API_MOSS_ENDPOINT_NAME: &str = "moss_generate"; +pub const AIFARM_DRAW_API_FIBO_ENDPOINT_NAME: &str = "fibo_generate"; +pub const AIFARM_DRAW_API_FIBO_EDIT_ENDPOINT_NAME: &str = "fibo_edit"; pub const AIFARM_DRAW_API_BOOGU_TURBO_ENDPOINT_NAME: &str = "boogu_turbo_generate"; pub const AIFARM_DRAW_API_BOOGU_EDIT_ENDPOINT_NAME: &str = "boogu_edit_generate"; pub const AIFARM_DRAW_API_DEFAULT_BASE_URL: &str = "http://127.0.0.1:50051"; @@ -105,6 +106,8 @@ pub const IMAGE_GENERATION_FLUX_WORKFLOW_KEY: &str = "image_generation_flux"; pub const IMAGE_GENERATION_BOOGU_TURBO_WORKFLOW_KEY: &str = "image_generation_boogu_turbo"; /// Second VIP text-to-image slot, routed to MOSS-Image. pub const IMAGE_GENERATION_MOSS_WORKFLOW_KEY: &str = "image_generation_moss"; +/// Third VIP text-to-image slot, routed to Fibo 2.0 Dev. +pub const IMAGE_GENERATION_FIBO_WORKFLOW_KEY: &str = "image_generation_fibo"; pub const IMAGE_EDIT_FLUX_WORKFLOW_KEY: &str = "image_edit_flux"; pub const IMAGE_EDIT_BOOGU_TURBO_WORKFLOW_KEY: &str = "image_edit_boogu_turbo"; pub const DRAW_SUPPORT_ME_URL: &str = "https://t.me/PlotvoBot?start=donate"; @@ -158,8 +161,9 @@ pub struct ImageGenerationRequest { pub caption_text: String, /// Optimized prompt variants. pub prompt_variants: Vec, - /// MOSS-Image layouts aligned with `prompt_variants`: compact JSON for a slot - /// written for MOSS-Image on a designed image, empty for every other slot. + /// Structured prompts aligned with `prompt_variants`, as compact JSON: the layout of + /// a slot written for MOSS-Image on a designed image, the caption of a slot written + /// for Fibo 2.0, empty for every other slot. pub prompt_layouts: Vec, pub is_nsfw: bool, /// Raw negative prompt. It is never sent to an image model as typed; the @@ -194,6 +198,9 @@ pub struct ImageEditRequest { pub photo_urls: Vec, /// Telegram file IDs of the pictures to edit, in order; loaded on the bot side. pub photo_file_ids: Vec, + /// Fibo 2.0 caption of the edited picture as compact JSON; empty when the optimizer + /// wrote none. + pub caption: String, } /// Successful provider result. @@ -792,24 +799,30 @@ where return Ok(ImageGenerationResult::default()); } let aspect_ratio = draw_api_aspect_ratio(&request.aspect_ratio); - let moss = if self - .cfg - .endpoint_name - .eq_ignore_ascii_case(AIFARM_DRAW_API_MOSS_ENDPOINT_NAME) - { - DrawApiMossInputs { + let endpoint = self.cfg.endpoint_name.as_str(); + // A fallback hop renders the prompt written for another model, so each endpoint + // takes only the structure its own model reads. + let structure = draw_api_slot_structure(&request); + let inputs = if endpoint.eq_ignore_ascii_case(AIFARM_DRAW_API_MOSS_ENDPOINT_NAME) { + DrawApiModelInputs { negative_prompt: &request.moss_negative_prompt, - layout: draw_api_moss_layout(&request), + layout: structure.filter(is_moss_layout), + caption: None, + } + } else if endpoint.eq_ignore_ascii_case(AIFARM_DRAW_API_FIBO_ENDPOINT_NAME) { + DrawApiModelInputs { + caption: structure.filter(|value| !is_moss_layout(value)), + ..DrawApiModelInputs::default() } } else { - DrawApiMossInputs::default() + DrawApiModelInputs::default() }; let payload = draw_api_payload( &prompt, &[], draw_api_dimensions(&request.aspect_ratio), aspect_ratio.as_deref(), - moss, + inputs, ) .map_err(|err| ImageGenerationError::Provider(err.to_string()))?; let job_id = self @@ -858,14 +871,22 @@ where "image edit requires image input".to_owned(), )); } - let payload = draw_api_payload( - &prompt, - &image_inputs, - None, - None, - DrawApiMossInputs::default(), - ) - .map_err(|err| ImageEditError::Provider(err.to_string()))?; + let inputs = if self + .cfg + .endpoint_name + .eq_ignore_ascii_case(AIFARM_DRAW_API_FIBO_EDIT_ENDPOINT_NAME) + { + DrawApiModelInputs { + caption: serde_json::from_str::(request.caption.trim()) + .ok() + .filter(Value::is_object), + ..DrawApiModelInputs::default() + } + } else { + DrawApiModelInputs::default() + }; + let payload = draw_api_payload(&prompt, &image_inputs, None, None, inputs) + .map_err(|err| ImageEditError::Provider(err.to_string()))?; let job_id = self .submit_draw_api_job(job_id, payload, self.cfg.submit_deadline(Instant::now())) .await @@ -1120,7 +1141,6 @@ fn add_flux_watermark(encoded: &[u8]) -> Result, String> { fn is_flux_generation_endpoint(endpoint_name: &str) -> bool { endpoint_name.eq_ignore_ascii_case(AIFARM_DRAW_API_ENDPOINT_NAME) - || endpoint_name.eq_ignore_ascii_case(AIFARM_DRAW_API_FLUX3_ENDPOINT_NAME) } fn blend_flux_watermark_channel(source: u8, target: u8) -> u8 { @@ -1536,7 +1556,7 @@ pub trait ImageGenerator { /// Image model each prompt slot is written for, in slot order. fn image_targets(&self) -> openplotva_media::ImageTargets { - openplotva_media::ImageTargets::FLUX3 + openplotva_media::ImageTargets::KREA2 } /// Generate and send an image. @@ -1571,7 +1591,7 @@ pub trait ImageEditor { /// Image model each prompt slot is written for, in slot order. fn image_targets(&self) -> openplotva_media::ImageTargets { - openplotva_media::ImageTargets::FLUX3 + openplotva_media::ImageTargets::FIBO } /// Edit and send an image. @@ -1616,7 +1636,11 @@ impl ImageGenerator for RoutedImageGenerator { } fn image_targets(&self) -> openplotva_media::ImageTargets { - routed_image_targets(&self.walker, &self.workflow_key) + routed_image_targets( + &self.walker, + &self.workflow_key, + openplotva_media::ImageTargets::KREA2, + ) } fn generate_image<'a>(&'a self, request: ImageGenerationRequest) -> ImageGenerationFuture<'a> { @@ -1685,7 +1709,11 @@ where DataUrl::Error: fmt::Display, { fn image_targets(&self) -> openplotva_media::ImageTargets { - routed_image_targets(&self.walker, &self.workflow_key) + routed_image_targets( + &self.walker, + &self.workflow_key, + openplotva_media::ImageTargets::FIBO, + ) } fn edit_image<'a>(&'a self, request: ImageEditRequest) -> ImageEditFuture<'a> { @@ -1744,16 +1772,18 @@ fn image_generation_context( /// the database switches the prompt style with it. The prompt is written once, /// before the walk, for the heaviest primary; a lighter primary or a fallback /// hop renders that same prompt. A MOSS-Image model whose config sets -/// `"prompt_layout": true` also gets a layout for designed pictures. +/// `"prompt_layout": true` also gets a layout for designed pictures. Without a routed +/// model the prompt is written for `default`. fn routed_image_targets( walker: &RoutedAttemptWalker, workflow_key: &str, + default: openplotva_media::ImageTargets, ) -> openplotva_media::ImageTargets { let config = walker.primary_model_config(workflow_key); let Some(config) = config.as_ref() else { - return openplotva_media::ImageTargets::FLUX3; + return default; }; - prompt_target_model(config).map_or(openplotva_media::ImageTargets::FLUX3, |model| { + prompt_target_model(config).map_or(default, |model| { openplotva_media::ImageTargets::single(model) .with_moss_layout(prompt_layout_enabled(config)) }) @@ -2145,22 +2175,21 @@ where } let optimized = openplotva_media::apply_word_replacements(optimized.value); apply_image_nsfw_result(optimized.nsfw_result, &mut request)?; - let moss_layout = optimized - .moss_layout - .as_ref() - .map(serde_json::Value::to_string) - .unwrap_or_default(); + let compact = |value: Option<&serde_json::Value>| { + value.map(serde_json::Value::to_string).unwrap_or_default() + }; + let moss_layout = compact(optimized.moss_layout.as_ref()); + let fibo_caption = compact(optimized.fibo_caption.as_ref()); (request.prompt_variants, request.prompt_layouts) = optimized .outputs .into_iter() .enumerate() .filter_map(|(slot, output)| { - let layout = - if options.targets.model_for_slot(slot) == openplotva_media::ImageModel::Moss { - moss_layout.clone() - } else { - String::new() - }; + let layout = match options.targets.model_for_slot(slot) { + openplotva_media::ImageModel::Moss => moss_layout.clone(), + openplotva_media::ImageModel::Fibo => fibo_caption.clone(), + openplotva_media::ImageModel::Krea2 => String::new(), + }; non_empty(output).map(|output| (output, layout)) }) .unzip(); @@ -2229,6 +2258,16 @@ where openplotva_media::NsfwResult::Safe | openplotva_media::NsfwResult::Adult => {} } request.prompt = first_prompt_variant(optimized.value.outputs, &original_prompt); + request.caption = optimized + .value + .fibo_caption + .filter(|_| { + targets + .models() + .contains(&openplotva_media::ImageModel::Fibo) + }) + .map(|caption| caption.to_string()) + .unwrap_or_default(); Ok(request) } @@ -3630,6 +3669,7 @@ where photo_file_id: params.photo_file_id, photo_urls: params.photo_urls, photo_file_ids: params.photo_file_ids, + caption: String::new(), }; let cancelled = std::pin::pin!(cancelled); @@ -4895,8 +4935,9 @@ pub fn draw_api_prompt_text(request: &ImageGenerationRequest) -> String { request.prompt.trim().to_owned() } -/// The MOSS-Image layout of the variant `draw_api_prompt_text` sends, if it has one. -fn draw_api_moss_layout(request: &ImageGenerationRequest) -> Option { +/// The structured prompt of the variant `draw_api_prompt_text` sends, if it has one: a +/// MOSS-Image layout or a Fibo 2.0 caption. +fn draw_api_slot_structure(request: &ImageGenerationRequest) -> Option { let slot = request .prompt_variants .iter() @@ -4905,7 +4946,14 @@ fn draw_api_moss_layout(request: &ImageGenerationRequest) -> Option { if layout.is_empty() { return None; } - serde_json::from_str(layout).ok() + serde_json::from_str::(layout) + .ok() + .filter(Value::is_object) +} + +/// A MOSS-Image layout has `items`; a Fibo 2.0 caption never does. +fn is_moss_layout(value: &Value) -> bool { + value.get("items").is_some() } pub fn decode_draw_api_result_payload(payload: &[u8]) -> Result { @@ -4967,14 +5015,18 @@ struct DrawApiGenerateRequest<'a> { negative_prompt: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] layout: Option, + #[serde(skip_serializing_if = "Option::is_none")] + caption: Option, } -/// What only the MOSS-Image endpoint reads: its negative prompt and a designed -/// image's layout, which draw-api renders instead of the prose prompt. +/// What only one model's endpoint reads: the MOSS-Image negative prompt and a designed +/// image's layout, which draw-api renders instead of the prose prompt, or the Fibo 2.0 +/// caption, which draw-api completes with the prose prompt. #[derive(Debug, Default)] -struct DrawApiMossInputs<'a> { +struct DrawApiModelInputs<'a> { negative_prompt: &'a str, layout: Option, + caption: Option, } #[derive(Serialize)] @@ -5031,7 +5083,7 @@ fn draw_api_payload( image_inputs: &[String], dimensions: Option<(i32, i32)>, aspect_ratio: Option<&str>, - moss: DrawApiMossInputs<'_>, + inputs: DrawApiModelInputs<'_>, ) -> serde_json::Result> { let (image_urls, image_b64) = split_draw_api_image_inputs(image_inputs); let image_url = one_or_many_strings(&image_urls); @@ -5047,8 +5099,9 @@ fn draw_api_payload( aspect_ratio, image_url, image_b64, - negative_prompt: Some(moss.negative_prompt.trim()).filter(|value| !value.is_empty()), - layout: moss.layout, + negative_prompt: Some(inputs.negative_prompt.trim()).filter(|value| !value.is_empty()), + layout: inputs.layout, + caption: inputs.caption, }) } @@ -5952,6 +6005,7 @@ mod tests { photo_file_id: "photo-file".to_owned(), photo_urls: vec!["https://telegram.test/original.png".to_owned()], photo_file_ids: Vec::new(), + caption: String::new(), }] ); let calls = effects.calls(); @@ -6469,26 +6523,40 @@ mod tests { }) } + fn fibo_poster_caption() -> Value { + json!({ + "style_medium": "flat vector illustration", + "text_render": [{"text": "Джаз у моря", "location": "top", "size": "large", "color": "cream", "font": "rounded display"}], + }) + } + #[tokio::test] - async fn optimizer_layout_and_negative_reach_only_the_moss_slot() { - let flux3 = GeneratorStub::success("https://img.test/flux3.png"); + async fn optimizer_structures_reach_only_their_own_slots() { + let krea2 = GeneratorStub::success("https://img.test/krea2.png"); let moss = GeneratorStub::success("https://img.test/moss.png") .with_targets(openplotva_media::ImageTargets::MOSS); + let fibo = GeneratorStub::success("https://img.test/fibo.png") + .with_targets(openplotva_media::ImageTargets::FIBO); let optimizer = OptimizerStub::default().with_image_result(openplotva_media::ImageOptimize { input: "постер".to_owned(), outputs: vec![ - "A flat poster for FLUX.3.".to_owned(), + "A flat poster for Krea 2.".to_owned(), "A flat poster for MOSS-Image.".to_owned(), + "A flat poster for Fibo 2.0.".to_owned(), ], aspect_ratio: "3:4".to_owned(), nsfw_result: openplotva_media::NsfwResult::Safe, moss_design: true, moss_layout: Some(moss_poster_layout()), moss_negative: "people".to_owned(), + fibo_caption: Some(fibo_poster_caption()), }); let optimizing = OptimizingImageGenerator::new( - SequentialImageGenerator::new(flux3.clone(), moss.clone()), + SequentialImageGenerator::new( + SequentialImageGenerator::new(krea2.clone(), moss.clone()), + fibo.clone(), + ), crate::media::MediaPromptOptimizerService::new(Some(optimizer)), ); @@ -6500,9 +6568,9 @@ mod tests { .await .expect("generation"); - let flux3_request = &flux3.requests()[0]; - assert_eq!(flux3_request.prompt_variants, ["A flat poster for FLUX.3."]); - assert!(flux3_request.prompt_layouts.is_empty()); + let krea2_request = &krea2.requests()[0]; + assert_eq!(krea2_request.prompt_variants, ["A flat poster for Krea 2."]); + assert!(krea2_request.prompt_layouts.is_empty()); let moss_request = &moss.requests()[0]; assert_eq!( moss_request.prompt_variants, @@ -6514,10 +6582,19 @@ mod tests { ); assert_eq!(moss_request.moss_negative_prompt, "people"); assert_eq!( - draw_api_moss_layout(moss_request), + draw_api_slot_structure(moss_request), Some(moss_poster_layout()) ); - assert_eq!(draw_api_moss_layout(flux3_request), None); + assert_eq!(draw_api_slot_structure(krea2_request), None); + let fibo_request = &fibo.requests()[0]; + assert_eq!( + fibo_request.prompt_variants, + ["A flat poster for Fibo 2.0."] + ); + assert_eq!( + draw_api_slot_structure(fibo_request), + Some(fibo_poster_caption()) + ); } #[test] @@ -6539,7 +6616,7 @@ mod tests { let second = image_generation_request_for_prompt_slots(&legacy, 1, 1); assert_eq!(second.prompt_variants, ["second"]); assert!(second.prompt_layouts.is_empty()); - assert_eq!(draw_api_moss_layout(&second), None); + assert_eq!(draw_api_slot_structure(&second), None); } #[tokio::test] @@ -6913,10 +6990,10 @@ mod tests { #[tokio::test] async fn nested_parallel_generators_hand_each_slot_its_own_prompt() { let krea2 = GeneratorStub::success("https://img.test/krea2.png"); - let flux3 = GeneratorStub::success("https://img.test/flux3.png"); + let fibo = GeneratorStub::success("https://img.test/fibo.png"); let moss = GeneratorStub::success("https://img.test/moss.png"); let vip = ParallelImageGenerator::new( - ParallelImageGenerator::new(krea2.clone(), flux3.clone()), + ParallelImageGenerator::new(krea2.clone(), fibo.clone()), moss.clone(), ); assert_eq!(vip.expected_image_count(), 3); @@ -6925,7 +7002,7 @@ mod tests { .generate_image(ImageGenerationRequest { prompt_variants: vec![ "krea2 prompt".to_owned(), - "flux3 prompt".to_owned(), + "fibo prompt".to_owned(), "moss prompt".to_owned(), ], ..ImageGenerationRequest::default() @@ -6937,13 +7014,13 @@ mod tests { image_generation_urls(&result), vec![ "https://img.test/krea2.png".to_owned(), - "https://img.test/flux3.png".to_owned(), + "https://img.test/fibo.png".to_owned(), "https://img.test/moss.png".to_owned(), ] ); for (generator, prompt) in [ (&krea2, "krea2 prompt"), - (&flux3, "flux3 prompt"), + (&fibo, "fibo prompt"), (&moss, "moss prompt"), ] { assert_eq!( @@ -6996,6 +7073,7 @@ mod tests { input: "make it day".to_owned(), outputs: vec!["make the scene bright daylight".to_owned()], nsfw_result: openplotva_media::NsfwResult::Safe, + fibo_caption: Some(json!({"lighting": {"conditions": "bright daylight"}})), }); let optimizing = OptimizingImageEditor::new( editor.clone(), @@ -7029,6 +7107,7 @@ mod tests { photo_file_id: "photo-file".to_owned(), photo_urls: vec!["https://telegram.test/original.png".to_owned()], photo_file_ids: Vec::new(), + caption: r#"{"lighting":{"conditions":"bright daylight"}}"#.to_owned(), ..ImageEditRequest::default() }] ); @@ -7061,8 +7140,9 @@ mod tests { let optimizer = OptimizerStub::default().with_edit_result(openplotva_media::ImageEditOptimize { input: "swap them".to_owned(), - outputs: vec!["Create a photograph preserving the first image".to_owned()], + outputs: vec!["Swap the two people between and .".to_owned()], nsfw_result: openplotva_media::NsfwResult::Safe, + fibo_caption: None, }); let optimizing = OptimizingImageEditor::new( editor.clone(), @@ -7104,6 +7184,7 @@ mod tests { input: "make it day".to_owned(), outputs: vec!["make the scene bright daylight".to_owned()], nsfw_result: openplotva_media::NsfwResult::Safe, + fibo_caption: None, }); let optimizing = OptimizingImageEditor::new( editor.clone(), @@ -8035,7 +8116,7 @@ mod tests { &[], draw_api_dimensions("16:9"), draw_api_aspect_ratio("16:9").as_deref(), - DrawApiMossInputs::default(), + DrawApiModelInputs::default(), ) .expect("generation payload"), ) @@ -8050,7 +8131,7 @@ mod tests { &["https://img.test/a.png".to_owned()], None, None, - DrawApiMossInputs::default(), + DrawApiModelInputs::default(), ) .expect("edit payload"), ) @@ -8128,14 +8209,15 @@ mod tests { #[test] fn image_prompt_targets_follow_the_routed_primary_model() { - let flux3 = image_route_walker(json!({ + let krea2_default = openplotva_media::ImageTargets::KREA2; + let fibo = image_route_walker(json!({ "service_name": AIFARM_DRAW_API_SERVICE_NAME, - "endpoint_name": "flux3_generate", - "prompt_target": "flux3", + "endpoint_name": AIFARM_DRAW_API_FIBO_ENDPOINT_NAME, + "prompt_target": "fibo", })); assert_eq!( - routed_image_targets(&flux3, IMAGE_GENERATION_BOOGU_TURBO_WORKFLOW_KEY), - openplotva_media::ImageTargets::FLUX3 + routed_image_targets(&fibo, IMAGE_GENERATION_FLUX_WORKFLOW_KEY, krea2_default), + openplotva_media::ImageTargets::FIBO ); let krea2 = image_route_walker(json!({ @@ -8144,7 +8226,11 @@ mod tests { "prompt_target": "krea2", })); assert_eq!( - routed_image_targets(&krea2, IMAGE_GENERATION_FLUX_WORKFLOW_KEY), + routed_image_targets( + &krea2, + IMAGE_GENERATION_FLUX_WORKFLOW_KEY, + openplotva_media::ImageTargets::FIBO + ), openplotva_media::ImageTargets::KREA2 ); @@ -8153,7 +8239,8 @@ mod tests { "endpoint_name": AIFARM_DRAW_API_MOSS_ENDPOINT_NAME, "prompt_target": "moss", })); - let targets = routed_image_targets(&moss, IMAGE_GENERATION_FLUX_WORKFLOW_KEY); + let targets = + routed_image_targets(&moss, IMAGE_GENERATION_FLUX_WORKFLOW_KEY, krea2_default); assert_eq!(targets, openplotva_media::ImageTargets::MOSS); assert!(!targets.moss_layout()); @@ -8164,23 +8251,34 @@ mod tests { "prompt_layout": true, })); assert!( - routed_image_targets(&moss_layout, IMAGE_GENERATION_FLUX_WORKFLOW_KEY).moss_layout() + routed_image_targets( + &moss_layout, + IMAGE_GENERATION_FLUX_WORKFLOW_KEY, + krea2_default + ) + .moss_layout() ); + // A retired or missing model leaves the prompt to the workflow's default target. let retired = image_route_walker(json!({ - "endpoint_name": "qwen_image_generate", - "prompt_target": "qwen_image", + "endpoint_name": "flux3_generate", + "prompt_target": "flux3", })); assert_eq!( - routed_image_targets(&retired, IMAGE_EDIT_WORKFLOW_KEY), - openplotva_media::ImageTargets::FLUX3 + routed_image_targets( + &retired, + IMAGE_EDIT_WORKFLOW_KEY, + openplotva_media::ImageTargets::FIBO + ), + openplotva_media::ImageTargets::FIBO ); assert_eq!( routed_image_targets( &empty_routed_attempt_walker(), - IMAGE_GENERATION_BOOGU_TURBO_WORKFLOW_KEY + IMAGE_GENERATION_BOOGU_TURBO_WORKFLOW_KEY, + krea2_default ), - openplotva_media::ImageTargets::FLUX3 + krea2_default ); } @@ -8214,8 +8312,9 @@ mod tests { #[test] fn flux_watermark_is_limited_to_the_flux_generation_endpoints() { assert!(is_flux_generation_endpoint(AIFARM_DRAW_API_ENDPOINT_NAME)); - assert!(is_flux_generation_endpoint( - AIFARM_DRAW_API_FLUX3_ENDPOINT_NAME + assert!(!is_flux_generation_endpoint("flux3_generate")); + assert!(!is_flux_generation_endpoint( + AIFARM_DRAW_API_FIBO_ENDPOINT_NAME )); assert!(!is_flux_generation_endpoint("krea2_generate")); assert!(!is_flux_generation_endpoint( @@ -8449,11 +8548,21 @@ mod tests { assert_eq!(moss["layout"], moss_poster_layout()); assert_eq!(moss["aspect_ratio"], "3:4"); - let flux3 = - submitted_draw_request(AIFARM_DRAW_API_FLUX3_ENDPOINT_NAME, request.clone()).await; - assert_eq!(flux3["prompt"], "A flat jazz poster."); - assert!(flux3.get("negative_prompt").is_none()); - assert!(flux3.get("layout").is_none()); + assert!(moss.get("caption").is_none()); + + let krea2 = submitted_draw_request("krea2_generate", request.clone()).await; + assert_eq!(krea2["prompt"], "A flat jazz poster."); + assert!(krea2.get("negative_prompt").is_none()); + assert!(krea2.get("layout").is_none()); + assert!(krea2.get("caption").is_none()); + + // A MOSS-Image prompt that falls back to Fibo 2.0 keeps its prose only. + let fibo = + submitted_draw_request(AIFARM_DRAW_API_FIBO_ENDPOINT_NAME, request.clone()).await; + assert_eq!(fibo["prompt"], "A flat jazz poster."); + assert!(fibo.get("caption").is_none()); + assert!(fibo.get("layout").is_none()); + assert!(fibo.get("negative_prompt").is_none()); let prose_only = submitted_draw_request( AIFARM_DRAW_API_MOSS_ENDPOINT_NAME, @@ -8468,6 +8577,102 @@ mod tests { assert!(prose_only.get("layout").is_none()); } + #[tokio::test] + async fn only_the_fibo_endpoint_receives_the_caption() { + let request = ImageGenerationRequest { + prompt: "постер".to_owned(), + caption_text: "caption".to_owned(), + prompt_variants: vec!["A flat jazz poster.".to_owned()], + prompt_layouts: vec![fibo_poster_caption().to_string()], + aspect_ratio: "3:4".to_owned(), + ..ImageGenerationRequest::default() + }; + let fibo = + submitted_draw_request(AIFARM_DRAW_API_FIBO_ENDPOINT_NAME, request.clone()).await; + assert_eq!(fibo["prompt"], "A flat jazz poster."); + assert_eq!(fibo["caption"], fibo_poster_caption()); + assert_eq!(fibo["aspect_ratio"], "3:4"); + + let moss = + submitted_draw_request(AIFARM_DRAW_API_MOSS_ENDPOINT_NAME, request.clone()).await; + assert!(moss.get("layout").is_none()); + assert!(moss.get("caption").is_none()); + let krea2 = submitted_draw_request("krea2_generate", request).await; + assert!(krea2.get("caption").is_none()); + } + + async fn submitted_draw_edit_request(endpoint_name: &str, request: ImageEditRequest) -> Value { + let draw_payload = serde_json::to_vec(&json!({ + "image_b64": [general_purpose::STANDARD.encode(b"edited")], + })) + .expect("draw payload"); + let response_body = general_purpose::STANDARD.encode(draw_payload); + let transport = AifarmTransportStub::new(vec![ + Ok(json_response( + json!({"job_id": "job-1", "state": "processing"}), + )), + Ok(json_response(json!({ + "job": { + "job_id": "job-1", + "state": "completed", + "result": {"response": {"status_code": 200, "body": response_body}} + } + }))), + ]); + let probe = transport.clone(); + let generator = AifarmDrawApiImageGenerator::with_transport( + AifarmDrawApiConfig { + base_url: "https://draw.example.test".to_owned(), + service_name: AIFARM_DRAW_API_SERVICE_NAME.to_owned(), + endpoint_name: endpoint_name.to_owned(), + timeout: StdDuration::from_secs(5), + poll_interval: StdDuration::from_nanos(1), + capacity_wait: StdDuration::ZERO, + }, + transport, + ); + generator + .edit_image_with_job_id(request, "edit-fibo") + .await + .expect("edit result"); + let job: DiscoveryJobRequest = + serde_json::from_slice(&probe.requests()[0].body).expect("job request"); + let body = decode_discovery_body(&job.invocation.body).expect("edit body"); + serde_json::from_slice(&body).expect("edit request") + } + + #[tokio::test] + async fn only_the_fibo_edit_endpoint_receives_the_result_caption() { + let request = ImageEditRequest { + prompt: "Change the snowboard's deck color to solid red.".to_owned(), + photo_urls: vec!["c291cmNl".to_owned()], + caption: fibo_poster_caption().to_string(), + ..ImageEditRequest::default() + }; + let fibo = + submitted_draw_edit_request(AIFARM_DRAW_API_FIBO_EDIT_ENDPOINT_NAME, request.clone()) + .await; + assert_eq!( + fibo["prompt"], + "Change the snowboard's deck color to solid red." + ); + assert_eq!(fibo["caption"], fibo_poster_caption()); + assert_eq!(fibo["image_b64"], "c291cmNl"); + + let other = submitted_draw_edit_request("krea2_generate", request.clone()).await; + assert!(other.get("caption").is_none()); + + let without_caption = submitted_draw_edit_request( + AIFARM_DRAW_API_FIBO_EDIT_ENDPOINT_NAME, + ImageEditRequest { + caption: "not json".to_owned(), + ..request + }, + ) + .await; + assert!(without_caption.get("caption").is_none()); + } + fn draw_poll_test_config() -> AifarmDrawApiConfig { AifarmDrawApiConfig { base_url: "https://draw.example.test".to_owned(), @@ -9732,7 +9937,7 @@ mod tests { }), requests: Arc::new(Mutex::new(Vec::new())), expected_image_count: 1, - targets: openplotva_media::ImageTargets::FLUX3, + targets: openplotva_media::ImageTargets::KREA2, } } @@ -9744,7 +9949,7 @@ mod tests { }), requests: Arc::new(Mutex::new(Vec::new())), expected_image_count: 1, - targets: openplotva_media::ImageTargets::FLUX3, + targets: openplotva_media::ImageTargets::KREA2, } } @@ -9753,7 +9958,7 @@ mod tests { result: Err(ImageGenerationError::Forbidden), requests: Arc::new(Mutex::new(Vec::new())), expected_image_count: 1, - targets: openplotva_media::ImageTargets::FLUX3, + targets: openplotva_media::ImageTargets::KREA2, } } @@ -9762,7 +9967,7 @@ mod tests { result: Err(ImageGenerationError::Provider(message.into())), requests: Arc::new(Mutex::new(Vec::new())), expected_image_count: 1, - targets: openplotva_media::ImageTargets::FLUX3, + targets: openplotva_media::ImageTargets::KREA2, } } diff --git a/crates/openplotva-app/src/lib.rs b/crates/openplotva-app/src/lib.rs index c4231738..b082a0aa 100644 --- a/crates/openplotva-app/src/lib.rs +++ b/crates/openplotva-app/src/lib.rs @@ -149,9 +149,10 @@ const DURABLE_UPDATE_CONSUMER_MAX_WORKERS: usize = openplotva_storage::POSTGRES_CRITICAL_MAX_CONNECTIONS as usize * 3; const REGULAR_IMAGE_GENERATION_WORKFLOW_KEY: &str = image_jobs::IMAGE_GENERATION_BOOGU_TURBO_WORKFLOW_KEY; -const VIP_IMAGE_GENERATION_WORKFLOW_KEYS: [&str; 2] = [ +const VIP_IMAGE_GENERATION_WORKFLOW_KEYS: [&str; 3] = [ image_jobs::IMAGE_GENERATION_FLUX_WORKFLOW_KEY, image_jobs::IMAGE_GENERATION_MOSS_WORKFLOW_KEY, + image_jobs::IMAGE_GENERATION_FIBO_WORKFLOW_KEY, ]; const VIP_IMAGE_EDITOR_WORKFLOW_KEY: &str = image_jobs::IMAGE_EDIT_FLUX_WORKFLOW_KEY; @@ -13318,19 +13319,25 @@ async fn start_runtime_workers( ) .with_openrouter_free_gate(Arc::clone(&openrouter_free_gate)) .with_reporter(routing_event_reporter.clone()); - let [vip_first_workflow_key, vip_second_workflow_key] = VIP_IMAGE_GENERATION_WORKFLOW_KEYS; + let vip_slot = |workflow_key: &str| { + image_jobs::RoutedImageGenerator::new( + image_attempt_walker.clone(), + image_jobs::aifarm_draw_api_config_from_app_config(config), + ) + .with_workflow_key(workflow_key) + }; + let [ + vip_first_workflow_key, + vip_second_workflow_key, + vip_third_workflow_key, + ] = VIP_IMAGE_GENERATION_WORKFLOW_KEYS; let vip_image_generator = image_jobs::OptimizingImageGenerator::new( image_jobs::ParallelImageGenerator::new( - image_jobs::RoutedImageGenerator::new( - image_attempt_walker.clone(), - image_jobs::aifarm_draw_api_config_from_app_config(config), - ) - .with_workflow_key(vip_first_workflow_key), - image_jobs::RoutedImageGenerator::new( - image_attempt_walker.clone(), - image_jobs::aifarm_draw_api_config_from_app_config(config), - ) - .with_workflow_key(vip_second_workflow_key), + image_jobs::ParallelImageGenerator::new( + vip_slot(vip_first_workflow_key), + vip_slot(vip_second_workflow_key), + ), + vip_slot(vip_third_workflow_key), ), media_prompt_optimizer.clone(), ) @@ -15174,6 +15181,7 @@ mod tests { [ super::image_jobs::IMAGE_GENERATION_FLUX_WORKFLOW_KEY, super::image_jobs::IMAGE_GENERATION_MOSS_WORKFLOW_KEY, + super::image_jobs::IMAGE_GENERATION_FIBO_WORKFLOW_KEY, ] ); assert_eq!( diff --git a/crates/openplotva-app/src/media.rs b/crates/openplotva-app/src/media.rs index 0d24b423..befed855 100644 --- a/crates/openplotva-app/src/media.rs +++ b/crates/openplotva-app/src/media.rs @@ -510,6 +510,7 @@ where input: original_prompt.to_owned(), outputs: vec![original_prompt.to_owned()], nsfw_result: openplotva_media::NsfwResult::Adult, + fibo_caption: None, }; let Some(optimizer) = &self.optimizer else { return MediaPromptOptimization { @@ -1247,6 +1248,7 @@ mod tests { input: "make it day".to_owned(), outputs: vec!["make it day".to_owned()], nsfw_result: openplotva_media::NsfwResult::Adult, + fibo_caption: None, } ); } @@ -1302,6 +1304,7 @@ mod tests { input: "make it day".to_owned(), outputs: vec!["gemini edit".to_owned()], nsfw_result: openplotva_media::NsfwResult::Safe, + fibo_caption: None, })); let optimizer = FallbackMediaPromptOptimizer::with_flow_labels( primary, diff --git a/crates/openplotva-core/src/lib.rs b/crates/openplotva-core/src/lib.rs index 000656ba..d708a683 100644 --- a/crates/openplotva-core/src/lib.rs +++ b/crates/openplotva-core/src/lib.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; /// Public project name used in diagnostics and health responses. pub const PROJECT_NAME: &str = "openplotva"; -/// Maximum source images supported by the Qwen-Image 2.1 edit model. -pub const MAX_IMAGE_EDIT_SOURCE_IMAGES: usize = 10; +/// Maximum source images one edit takes: Fibo 2.0 Edit reads up to four. +pub const MAX_IMAGE_EDIT_SOURCE_IMAGES: usize = 4; #[derive(Clone, Debug, Eq, PartialEq)] pub struct ChatState { diff --git a/crates/openplotva-dialog/src/lib.rs b/crates/openplotva-dialog/src/lib.rs index 044ba472..d01b578c 100644 --- a/crates/openplotva-dialog/src/lib.rs +++ b/crates/openplotva-dialog/src/lib.rs @@ -614,7 +614,7 @@ const DRAW_IMAGE_ARGS: &[ToolArgSpec] = &[ ToolArgSpec { name: "file_ids", required: false, - description: "Required when editing or combining earlier images: list every requested source using its file_id handle or file_unique_id from the dialog attachments. A style change to a previous generation must reference its generated result. Pass up to 10 references together in ONE draw_image call. Describing images in prompt does not attach them. Omit only for a new image or when all sources are attached to or quoted by the current message.", + description: "Required when editing or combining earlier images: list every requested source using its file_id handle or file_unique_id from the dialog attachments. A style change to a previous generation must reference its generated result. Pass up to 4 references together in ONE draw_image call. Describing images in prompt does not attach them. Omit only for a new image or when all sources are attached to or quoted by the current message.", }, ToolArgSpec { name: "negative_prompt", diff --git a/crates/openplotva-llm/src/aifarm.rs b/crates/openplotva-llm/src/aifarm.rs index 54b202f0..214901e8 100644 --- a/crates/openplotva-llm/src/aifarm.rs +++ b/crates/openplotva-llm/src/aifarm.rs @@ -2322,7 +2322,7 @@ where } None => openplotva_media::render_image_edit_optimizer_prompt(render_options)?, }; - let tool = openplotva_media::optimize_edit_prompt_terminator_definition(variant_count); + let tool = openplotva_media::optimize_edit_prompt_terminator_definition(render_options); let images: Vec = images .iter() .map(|image| image.trim()) @@ -8469,7 +8469,7 @@ mod tests { json!([ {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,AAAA", "detail": "auto"}}, {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,BBBB", "detail": "auto"}}, - {"type": "text", "text": "Reference images follow in order (the first image, the second image, ...).\nedit it"}, + {"type": "text", "text": "The pictures above are , , ... in order.\nedit it"}, ]) ); assert_eq!(edit_body["max_tokens"], 4096); diff --git a/crates/openplotva-llm/src/gemini.rs b/crates/openplotva-llm/src/gemini.rs index f8fd879f..d8e7b4b6 100644 --- a/crates/openplotva-llm/src/gemini.rs +++ b/crates/openplotva-llm/src/gemini.rs @@ -1310,7 +1310,7 @@ where } None => openplotva_media::render_image_edit_optimizer_prompt(render_options)?, }; - let tool = openplotva_media::optimize_edit_prompt_terminator_definition(variant_count); + let tool = openplotva_media::optimize_edit_prompt_terminator_definition(render_options); let images: Vec = images .iter() .filter_map(|image| gemini_inline_data_from_data_url(image.trim())) diff --git a/crates/openplotva-media/src/lib.rs b/crates/openplotva-media/src/lib.rs index 4ee42ad8..01786c34 100644 --- a/crates/openplotva-media/src/lib.rs +++ b/crates/openplotva-media/src/lib.rs @@ -90,6 +90,27 @@ pub struct ImageOptimize { deserialize_with = "deserialize_moss_layout" )] pub moss_layout: Option, + /// Fibo 2.0 caption of the picture: the VGL fields except `short_description`, which is + /// the Fibo 2.0 slot's prompt. draw-api orders the fields as Fibo 2.0 was trained on. + #[serde( + default, + rename = "fibo_caption", + deserialize_with = "deserialize_fibo_caption" + )] + pub fibo_caption: Option, +} + +/// Keep a Fibo 2.0 caption only when it is a non-empty object, so a malformed one degrades +/// to the prose prompt instead of failing the whole payload. +fn deserialize_fibo_caption<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + Ok(value + .as_object() + .is_some_and(|fields| !fields.is_empty()) + .then_some(value)) } /// Request kinds MOSS-Image draws from a layout, as the official prompt enhancer @@ -143,6 +164,14 @@ pub struct ImageEditOptimize { pub outputs: Vec, /// NSFW classifier result. pub nsfw_result: NsfwResult, + /// Fibo 2.0 caption of the edited picture, without `edit_instruction`, which is the + /// slot's prompt. + #[serde( + default, + rename = "fibo_caption", + deserialize_with = "deserialize_fibo_caption" + )] + pub fibo_caption: Option, } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] @@ -156,12 +185,12 @@ pub struct OptimizePromptOptions { /// Image model a prompt variant is written for. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum ImageModel { - /// FLUX.3 [klein]: captions and edit instructions in the form of its official prompt - /// upsampler, read by a Qwen3-VL encoder. - #[default] - Flux3, /// Krea 2 Turbo: one natural-prose paragraph, medium and style first. + #[default] Krea2, + /// Fibo 2.0: a structured VGL caption with a prose summary, or an edit instruction + /// with the caption of the result, read by a Qwen3-VL encoder. + Fibo, /// MOSS-Image SFT: a detailed caption in the form of its recaptioner, read by a /// Qwen3.5 encoder. Moss, @@ -171,8 +200,8 @@ impl ImageModel { #[must_use] pub const fn display_name(self) -> &'static str { match self { - Self::Flux3 => "FLUX.3 [klein]", Self::Krea2 => "Krea 2 Turbo", + Self::Fibo => "Fibo 2.0", Self::Moss => "MOSS-Image", } } @@ -181,8 +210,8 @@ impl ImageModel { #[must_use] pub fn from_prompt_target(value: &str) -> Option { match value.trim().to_ascii_lowercase().as_str() { - "flux3" => Some(Self::Flux3), "krea2" => Some(Self::Krea2), + "fibo" => Some(Self::Fibo), "moss" => Some(Self::Moss), _ => None, } @@ -192,8 +221,8 @@ impl ImageModel { #[must_use] pub const fn prompt_max_chars(self) -> usize { match self { - Self::Flux3 => FLUX3_PROMPT_MAX_CHARS, Self::Krea2 => KREA2_PROMPT_MAX_CHARS, + Self::Fibo => FIBO_PROMPT_MAX_CHARS, Self::Moss => MOSS_PROMPT_MAX_CHARS, } } @@ -213,13 +242,13 @@ pub struct ImageTargets { impl Default for ImageTargets { fn default() -> Self { - Self::FLUX3 + Self::KREA2 } } impl ImageTargets { - pub const FLUX3: Self = Self::single(ImageModel::Flux3); pub const KREA2: Self = Self::single(ImageModel::Krea2); + pub const FIBO: Self = Self::single(ImageModel::Fibo); pub const MOSS: Self = Self::single(ImageModel::Moss); #[must_use] @@ -263,7 +292,7 @@ impl ImageTargets { .iter() .map(|model| model.prompt_max_chars()) .max() - .unwrap_or(FLUX3_PROMPT_MAX_CHARS) + .unwrap_or(KREA2_PROMPT_MAX_CHARS) } /// Targets for two generators rendered side by side: `own_slots` slots @@ -403,6 +432,9 @@ pub fn apply_word_replacements(mut optimize: ImageOptimize) -> ImageOptimize { *output = replace_image_words(output); } optimize.moss_negative = replace_image_words(&optimize.moss_negative); + if let Some(caption) = optimize.fibo_caption.as_mut() { + replace_caption_words(caption); + } if let Some(layout) = optimize.moss_layout.as_mut() { // Captions describe the picture; `text` is lettering and stays verbatim. if let Some(caption) = layout.pointer_mut("/summary/caption") { @@ -431,6 +463,23 @@ fn replace_image_words_in(value: &mut Value) { } } +/// Word replacements in every description of a Fibo 2.0 caption; the lettering in +/// `text_render` stays verbatim. +fn replace_caption_words(value: &mut Value) { + match value { + Value::String(_) => replace_image_words_in(value), + Value::Array(items) => items.iter_mut().for_each(replace_caption_words), + Value::Object(fields) => { + for (key, field) in fields.iter_mut() { + if key != "text" { + replace_caption_words(field); + } + } + } + _ => {} + } +} + #[must_use] pub fn normalize_nsfw_result(raw: &str) -> NsfwResult { let raw = raw.trim(); @@ -493,12 +542,13 @@ pub fn render_image_edit_optimizer_prompt_with( ) } -/// Line the FLUX.3 prompt upsampler reads between the attached pictures and the request. +/// Line the edit optimizer reads between the attached pictures and the request: the +/// tokens Fibo 2.0 Edit names several pictures by. pub const EDIT_REFERENCE_ORDER_LINE: &str = - "Reference images follow in order (the first image, the second image, ...)."; + "The pictures above are , , ... in order."; -/// Most pictures the edit optimizer looks at, as for the FLUX.3 prompt upsampler. -pub const MAX_EDIT_REFERENCE_IMAGES: usize = 10; +/// Most pictures one edit takes: Fibo 2.0 Edit reads up to four. +pub const MAX_EDIT_REFERENCE_IMAGES: usize = 4; /// Text of the edit optimizer's user turn: the reference order line when pictures are /// attached, then the request. @@ -514,7 +564,8 @@ pub fn image_edit_optimizer_text(request: &str, image_count: usize) -> String { const UPSAMPLER_REFERENCE_MULTIPLE: u32 = 32; const UPSAMPLER_MAX_ASPECT_RATIO: u32 = 200; -/// Size the FLUX.3 prompt upsampler gives one of `count` reference pictures: sides on a +/// Size the edit optimizer sees one of `count` reference pictures at, the FLUX.3 prompt +/// upsampler's policy: sides on a /// multiple of 32 and at most 1024², 724², or 512² pixels for one, two, or more pictures. /// `None` for an empty or overly elongated picture. #[must_use] @@ -551,15 +602,14 @@ pub fn upsampler_reference_size(width: u32, height: u32, count: usize) -> Option Some((w as u32, h as u32)) } -/// Upper bound for one optimized FLUX.3 caption or edit instruction, in characters. -/// The rules ask for up to about 350 words; the bound only makes a looping model close -/// its string and the JSON instead of running to the token cap. -pub const FLUX3_PROMPT_MAX_CHARS: usize = 4000; - -/// Krea 2 rules ask for up to 200 words. +/// Krea 2 rules ask for up to 200 words. Every bound only makes a looping model close its +/// string and the JSON instead of running to the token cap. pub const KREA2_PROMPT_MAX_CHARS: usize = 1600; -/// MOSS-Image rules ask for up to about 350 words, like FLUX.3. +/// A Fibo 2.0 summary has at most 200 words, and an edit instruction is one command. +pub const FIBO_PROMPT_MAX_CHARS: usize = 1600; + +/// MOSS-Image rules ask for up to about 350 words. pub const MOSS_PROMPT_MAX_CHARS: usize = 4000; /// Cut every optimized output to at most `max_chars` characters. The bound is applied @@ -589,8 +639,8 @@ fn optimizer_prompt_data(options: OptimizePromptOptions) -> Value { "variant_count": variant_count, "slots": slots, "multi": variant_count > 1, - "flux3": models.contains(&ImageModel::Flux3), "krea2": models.contains(&ImageModel::Krea2), + "fibo": models.contains(&ImageModel::Fibo), "moss": models.contains(&ImageModel::Moss), "moss_layout": options.targets.moss_layout(), "moss_canvases": IMAGE_ASPECT_RATIOS @@ -611,12 +661,14 @@ fn slot_models(options: OptimizePromptOptions) -> Vec { } /// The image optimizer's payload schema; a call with a MOSS-Image slot also offers -/// that model's negative prompt, and its layout when the targets enable it. +/// that model's negative prompt, and its layout when the targets enable it, and a call +/// with a Fibo 2.0 slot its caption. #[must_use] pub fn optimize_prompt_terminator_definition( options: OptimizePromptOptions, ) -> OptimizerTerminatorDefinition { - let moss = if !slot_models(options).contains(&ImageModel::Moss) { + let models = slot_models(options); + let moss = if !models.contains(&ImageModel::Moss) { MossFields::None } else if options.targets.moss_layout() { MossFields::NegativeAndLayout @@ -629,6 +681,7 @@ pub fn optimize_prompt_terminator_definition( "Finalize image prompt optimization and return the structured payload.", true, moss, + models.contains(&ImageModel::Fibo), ) } @@ -640,16 +693,19 @@ enum MossFields { NegativeAndLayout, } +/// The edit optimizer's payload schema; a Fibo 2.0 slot also gets the caption of the +/// edited picture. #[must_use] pub fn optimize_edit_prompt_terminator_definition( - variant_count: usize, + options: OptimizePromptOptions, ) -> OptimizerTerminatorDefinition { optimizer_terminator_definition( - variant_count, + options.variant_count, OPTIMIZE_EDIT_PROMPT_TERMINATOR_TOOL_NAME, "Finalize image edit prompt optimization and return the structured payload.", false, MossFields::None, + slot_models(options).contains(&ImageModel::Fibo), ) } @@ -724,6 +780,7 @@ pub fn image_edit_optimize_fallback(text: &str, variant_count: usize) -> ImageEd input: text.trim().to_owned(), outputs: normalize_outputs(std::iter::empty::(), text, variant_count), nsfw_result: NsfwResult::Adult, + fibo_caption: None, } } @@ -747,6 +804,7 @@ pub fn normalize_image_optimize( if optimized.nsfw_result == NsfwResult::Forbidden { optimized.moss_negative.clear(); optimized.moss_design = false; + optimized.fibo_caption = None; } if !optimized.moss_design { optimized.moss_layout = None; @@ -790,6 +848,9 @@ pub fn normalize_image_edit_optimize( ) -> ImageEditOptimize { optimized.input = optimized.input.trim().to_owned(); optimized.outputs = normalize_outputs(optimized.outputs, fallback, variant_count); + if optimized.nsfw_result == NsfwResult::Forbidden { + optimized.fibo_caption = None; + } if optimized.input.is_empty() { optimized.input = fallback.trim().to_owned(); } @@ -892,6 +953,7 @@ fn optimizer_terminator_definition( description: &'static str, include_aspect_ratio: bool, moss: MossFields, + fibo: bool, ) -> OptimizerTerminatorDefinition { let count = normalize_variant_count(variant_count); let mut properties = serde_json::Map::from_iter([ @@ -957,6 +1019,12 @@ fn optimizer_terminator_definition( ); required.extend(["moss_kind", "moss_layout"]); } + // `fibo_caption` sorts before `outputs`, so the caption is drafted first and the + // Fibo 2.0 slot's summary follows it. + if fibo { + properties.insert("fibo_caption".to_owned(), fibo_caption_schema()); + required.push("fibo_caption"); + } if moss != MossFields::None { properties.insert( "slot_moss_negative".to_owned(), @@ -979,6 +1047,92 @@ fn optimizer_terminator_definition( } } +/// Most objects one Fibo 2.0 caption describes; the rest belong to `background_setting`. +const FIBO_CAPTION_MAX_OBJECTS: usize = 5; + +/// Fibo 2.0 caption in the VGL fields of its training captions, without the +/// `short_description` and `edit_instruction` that the slot's prompt carries. The fields +/// a VGL caption may omit are optional; a person's fields are optional on every object. +fn fibo_caption_schema() -> Value { + let text = json!({ "type": "string" }); + let section = |fields: &[&str]| { + json!({ + "type": "object", + "properties": fields.iter().map(|field| ((*field).to_owned(), text.clone())).collect::>(), + "required": fields, + }) + }; + let mut object_fields: serde_json::Map = [ + "description", + "location", + "relationship", + "relative_size", + "shape_and_color", + "texture", + "appearance_details", + "orientation", + "pose", + "expression", + "clothing", + "action", + ] + .iter() + .map(|field| ((*field).to_owned(), text.clone())) + .collect(); + object_fields.insert( + "number_of_objects".to_owned(), + json!({ "type": "integer", "minimum": 1 }), + ); + json!({ + "type": "object", + "description": "Fibo 2.0 caption of the picture, in the VGL fields.", + "properties": { + "objects": { + "type": "array", + "maxItems": FIBO_CAPTION_MAX_OBJECTS, + "items": { + "type": "object", + "properties": object_fields, + "required": ["description", "location", "relationship", "relative_size", "shape_and_color", "texture"], + }, + }, + "background_setting": text, + "lighting": section(&["conditions", "direction", "shadows"]), + "aesthetics": section(&["composition", "color_scheme", "mood_atmosphere"]), + "photographic_characteristics": section(&["depth_of_field", "focus", "camera_angle", "lens_focal_length"]), + "style_medium": text, + "text_render": { + "type": "array", + "items": { + "type": "object", + "properties": { + "text": text, + "location": text, + "size": text, + "color": text, + "font": text, + "appearance_details": text, + }, + "required": ["text", "location", "size", "color", "font"], + }, + }, + "context": text, + "artistic_style": text, + }, + "required": [ + "objects", + "background_setting", + "lighting", + "aesthetics", + "photographic_characteristics", + "style_medium", + "text_render", + "context", + "artistic_style", + ], + }) +} + /// Most items one MOSS-Image layout carries; draw-api compiles at most this many. const MOSS_LAYOUT_MAX_ITEMS: usize = 16; @@ -1192,36 +1346,66 @@ mod tests { fn optimizer_prompt_renders_rules_for_each_slot_target() { let pair = render_image_optimizer_prompt(OptimizePromptOptions { variant_count: 2, - targets: ImageTargets::KREA2.followed_by(1, ImageTargets::FLUX3, 1), + targets: ImageTargets::KREA2.followed_by(1, ImageTargets::FIBO, 1), }) .expect("render pair"); assert!(pair.contains("`outputs[0]` is rendered by Krea 2 Turbo")); - assert!(pair.contains("`outputs[1]` is rendered by FLUX.3 [klein]")); + assert!(pair.contains("`outputs[1]` is rendered by Fibo 2.0")); assert!(pair.contains("**Krea 2 Turbo**")); - assert!(pair.contains("**FLUX.3 [klein]**")); + assert!(pair.contains("**Fibo 2.0**")); assert!(pair.contains("All prompts describe the same image idea")); assert!(!pair.contains("{{")); - let flux3 = - render_image_optimizer_prompt(OptimizePromptOptions::default()).expect("render flux3"); - assert!(flux3.contains("`outputs[0]` is rendered by FLUX.3 [klein]")); - assert!(flux3.contains("**FLUX.3 [klein]**")); - assert!(flux3.contains("single FLUX.3 [klein] slot")); - assert!(!flux3.contains("Krea 2")); - assert!(!flux3.contains("All prompts describe the same image idea")); - assert!(!flux3.contains("{{")); + let fibo = render_image_optimizer_prompt(OptimizePromptOptions { + variant_count: 1, + targets: ImageTargets::FIBO, + }) + .expect("render fibo"); + assert!(fibo.contains("`outputs[0]` is rendered by Fibo 2.0")); + assert!(fibo.contains("the fields `aspect_ratio`, `fibo_caption`, `input`, `nsfw_result`, and `outputs`, in that order")); + assert!(fibo.contains("single Fibo 2.0 slot")); + assert!(!fibo.contains("Krea 2")); + assert!(!fibo.contains("FLUX")); + assert!(!fibo.contains("All prompts describe the same image idea")); + assert!(!fibo.contains("{{")); } #[test] - fn edit_prompt_uses_the_upsampler_instruction_form() { - let edit = render_image_edit_optimizer_prompt(OptimizePromptOptions::default()) - .expect("render edit"); - assert!(edit.contains("`outputs[0]` is carried out by FLUX.3 [klein]")); - assert!(edit.contains("**FLUX.3 [klein]**")); - assert!(edit.contains("preserving the first image as the exact canvas")); - assert!(edit.contains("the second image")); - assert!(!edit.contains("Call the picture being edited ")); + fn edit_prompt_pairs_an_instruction_with_the_caption_of_the_result() { + let edit = render_image_edit_optimizer_prompt(OptimizePromptOptions { + variant_count: 1, + targets: ImageTargets::FIBO, + }) + .expect("render edit"); + assert!(edit.contains("`outputs[0]` is carried out by Fibo 2.0")); + assert!(edit.contains("**Fibo 2.0**")); + assert!(edit.contains( + "the fields `fibo_caption`, `input`, `nsfw_result`, and `outputs`, in that order" + )); + assert!(edit.contains("")); + assert!(!edit.contains("FLUX")); assert!(!edit.contains("{{")); + let mut examples = 0; + for line in edit.lines().filter(|line| line.starts_with("{\"")) { + let example: Value = serde_json::from_str(line).expect("edit example parses"); + let keys: Vec<&String> = example.as_object().expect("example").keys().collect(); + assert_eq!( + keys, + ["fibo_caption", "input", "nsfw_result", "outputs"], + "{line}" + ); + let positions: Vec = keys + .iter() + .map(|key| line.find(&format!("\"{key}\": ")).expect("example key")) + .collect(); + assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{line}"); + assert!( + !example["fibo_caption"].to_string().contains(" 0); } #[test] @@ -1235,15 +1419,16 @@ mod tests { assert!(krea2.contains("**Krea 2 Turbo**")); assert!(krea2.contains("60 to 200 words")); assert!(krea2.contains("single Krea 2 Turbo slot")); - assert!(!krea2.contains("**FLUX.3 [klein]**")); + assert!(!krea2.contains("**Fibo 2.0**")); + assert!(!krea2.contains("fibo_caption")); assert!(!krea2.contains("{{")); } #[test] fn prompt_targets_and_length_bounds_follow_the_model() { assert_eq!( - ImageModel::from_prompt_target(" FLUX3 "), - Some(ImageModel::Flux3) + ImageModel::from_prompt_target(" FIBO "), + Some(ImageModel::Fibo) ); assert_eq!( ImageModel::from_prompt_target("krea2"), @@ -1253,19 +1438,16 @@ mod tests { ImageModel::from_prompt_target("moss"), Some(ImageModel::Moss) ); - for retired in ["klein", "boogu", "qwen_image", "dall-e"] { + for retired in ["flux3", "klein", "boogu", "qwen_image", "dall-e"] { assert_eq!(ImageModel::from_prompt_target(retired), None); } - assert_eq!( - ImageTargets::FLUX3.prompt_max_chars(), - FLUX3_PROMPT_MAX_CHARS - ); + assert_eq!(ImageTargets::FIBO.prompt_max_chars(), FIBO_PROMPT_MAX_CHARS); assert_eq!( ImageTargets::KREA2 - .followed_by(1, ImageTargets::FLUX3, 1) + .followed_by(1, ImageTargets::MOSS, 1) .prompt_max_chars(), - FLUX3_PROMPT_MAX_CHARS + MOSS_PROMPT_MAX_CHARS ); assert_eq!( ImageTargets::KREA2.prompt_max_chars(), @@ -1284,25 +1466,25 @@ mod tests { assert!(moss.contains("`outputs[0]` is rendered by MOSS-Image")); assert!(moss.contains("**MOSS-Image**")); assert!(moss.contains("single MOSS-Image slot")); - assert!(!moss.contains("**FLUX.3 [klein]**")); + assert!(!moss.contains("**Fibo 2.0**")); assert!(!moss.contains("**Krea 2 Turbo**")); assert!(!moss.contains("{{")); let vip = render_image_optimizer_prompt(OptimizePromptOptions { variant_count: 3, targets: ImageTargets::KREA2 - .followed_by(1, ImageTargets::FLUX3, 1) - .followed_by(2, ImageTargets::MOSS, 1), + .followed_by(1, ImageTargets::MOSS, 1) + .followed_by(2, ImageTargets::FIBO, 1), }) .expect("render vip"); assert!(vip.contains("`outputs[0]` is rendered by Krea 2 Turbo")); - assert!(vip.contains("`outputs[1]` is rendered by FLUX.3 [klein]")); - assert!(vip.contains("`outputs[2]` is rendered by MOSS-Image")); + assert!(vip.contains("`outputs[1]` is rendered by MOSS-Image")); + assert!(vip.contains("`outputs[2]` is rendered by Fibo 2.0")); assert!(vip.contains("**Krea 2 Turbo**")); - assert!(vip.contains("**FLUX.3 [klein]**")); assert!(vip.contains("**MOSS-Image**")); + assert!(vip.contains("**Fibo 2.0**")); assert!(vip.contains("`outputs` is one array of 3 complete prompts in slot order")); - assert!(vip.contains("plus the MOSS-Image field `slot_moss_negative`,")); + assert!(vip.contains("`aspect_ratio`, `fibo_caption`, `input`, `nsfw_result`, `outputs`, and `slot_moss_negative`, in that order")); assert!(vip.contains("written as `\\n` in the JSON string")); assert!(vip.contains("**MOSS-Image negative prompt**")); assert!(!vip.contains("moss_layout")); @@ -1322,10 +1504,10 @@ mod tests { assert!(layout.contains("\"moss_kind\": \"poster\", \"moss_layout\": {")); assert!(!layout.contains("{{")); - let flux3 = - render_image_optimizer_prompt(OptimizePromptOptions::default()).expect("render flux3"); - assert!(!flux3.contains("slot_moss_")); - assert!(!flux3.contains("one array of")); + let krea2 = + render_image_optimizer_prompt(OptimizePromptOptions::default()).expect("render krea2"); + assert!(!krea2.contains("slot_moss_")); + assert!(!krea2.contains("one array of")); } #[test] @@ -1385,7 +1567,7 @@ mod tests { fn moss_fields_join_the_schema_only_with_a_moss_slot() { let prose = optimize_prompt_terminator_definition(OptimizePromptOptions { variant_count: 2, - targets: ImageTargets::FLUX3.followed_by(1, ImageTargets::MOSS, 1), + targets: ImageTargets::KREA2.followed_by(1, ImageTargets::MOSS, 1), }) .input_schema; assert_eq!( @@ -1403,7 +1585,7 @@ mod tests { // The layout flag of either side survives joining two generators. let vip = optimize_prompt_terminator_definition(OptimizePromptOptions { variant_count: 2, - targets: ImageTargets::FLUX3.followed_by( + targets: ImageTargets::KREA2.followed_by( 1, ImageTargets::MOSS.with_moss_layout(true), 1, @@ -1461,13 +1643,16 @@ mod tests { assert!(position("outputs") < position("slot_moss_negative")); // Without a MOSS-Image slot the layout flag adds nothing. - let flux3 = optimize_prompt_terminator_definition(OptimizePromptOptions { + let krea2 = optimize_prompt_terminator_definition(OptimizePromptOptions { variant_count: 1, - targets: ImageTargets::FLUX3.with_moss_layout(true), + targets: ImageTargets::KREA2.with_moss_layout(true), + }); + assert!(krea2.input_schema["properties"]["moss_layout"].is_null()); + assert!(krea2.input_schema["properties"]["slot_moss_negative"].is_null()); + let edit = optimize_edit_prompt_terminator_definition(OptimizePromptOptions { + variant_count: 1, + targets: ImageTargets::FIBO, }); - assert!(flux3.input_schema["properties"]["moss_layout"].is_null()); - assert!(flux3.input_schema["properties"]["slot_moss_negative"].is_null()); - let edit = optimize_edit_prompt_terminator_definition(1); assert!(edit.input_schema["properties"]["slot_moss_negative"].is_null()); } @@ -1477,8 +1662,8 @@ mod tests { // Examples in another order made it write a field name as the second prompt of a // multi-slot call, or loop in it until the token limit. for targets in [ - ImageTargets::FLUX3, ImageTargets::KREA2, + ImageTargets::FIBO, ImageTargets::MOSS, ImageTargets::MOSS.with_moss_layout(true), ] { @@ -1708,7 +1893,7 @@ mod tests { ); assert_eq!( image_edit_optimizer_text("make it night", 2), - "Reference images follow in order (the first image, the second image, ...).\nmake it night" + "The pictures above are , , ... in order.\nmake it night" ); } @@ -1736,21 +1921,21 @@ mod tests { #[test] fn image_targets_follow_each_side_slot_count() { - let pair = ImageTargets::KREA2.followed_by(1, ImageTargets::FLUX3, 1); - assert_eq!(pair.models(), [ImageModel::Krea2, ImageModel::Flux3]); + let pair = ImageTargets::KREA2.followed_by(1, ImageTargets::FIBO, 1); + assert_eq!(pair.models(), [ImageModel::Krea2, ImageModel::Fibo]); - let wide_first = ImageTargets::KREA2.followed_by(2, ImageTargets::FLUX3, 1); + let wide_first = ImageTargets::KREA2.followed_by(2, ImageTargets::FIBO, 1); assert_eq!( wide_first.models(), - [ImageModel::Krea2, ImageModel::Krea2, ImageModel::Flux3] + [ImageModel::Krea2, ImageModel::Krea2, ImageModel::Fibo] ); assert_eq!(wide_first.model_for_slot(1), ImageModel::Krea2); - assert_eq!(wide_first.model_for_slot(2), ImageModel::Flux3); + assert_eq!(wide_first.model_for_slot(2), ImageModel::Fibo); - assert_eq!(ImageTargets::FLUX3.model_for_slot(3), ImageModel::Flux3); - assert_eq!(ImageTargets::default(), ImageTargets::FLUX3); + assert_eq!(ImageTargets::FIBO.model_for_slot(3), ImageModel::Fibo); + assert_eq!(ImageTargets::default(), ImageTargets::KREA2); assert_eq!( - ImageTargets::FLUX3 + ImageTargets::FIBO .followed_by(3, ImageTargets::KREA2, 3) .models() .len(), @@ -1823,7 +2008,8 @@ mod tests { .is_some_and(|description| description.contains("minor sexual content only")) ); - let edit_tool = optimize_edit_prompt_terminator_definition(0); + let edit_tool = + optimize_edit_prompt_terminator_definition(OptimizePromptOptions::default()); assert_eq!(edit_tool.name, OPTIMIZE_EDIT_PROMPT_TERMINATOR_TOOL_NAME); assert_eq!( edit_tool.input_schema["properties"]["outputs"]["minItems"], @@ -1933,10 +2119,134 @@ mod tests { input: "edit".to_owned(), outputs: vec!["edit".to_owned()], nsfw_result: NsfwResult::Adult, + fibo_caption: None, } ); } + #[test] + fn fibo_slots_add_a_caption_drafted_before_the_prompts() { + let vip = optimize_prompt_terminator_definition(OptimizePromptOptions { + variant_count: 3, + targets: ImageTargets::KREA2 + .followed_by(1, ImageTargets::MOSS, 1) + .followed_by(2, ImageTargets::FIBO, 1), + }) + .input_schema; + assert_eq!( + vip["required"], + json!([ + "input", + "outputs", + "nsfw_result", + "aspect_ratio", + "fibo_caption", + "slot_moss_negative" + ]) + ); + let keys: Vec<&String> = vip["properties"] + .as_object() + .expect("properties") + .keys() + .collect(); + let position = |key: &str| keys.iter().position(|k| *k == key).expect("key"); + assert!(position("fibo_caption") < position("outputs")); + let caption = &vip["properties"]["fibo_caption"]; + assert!(caption["properties"]["short_description"].is_null()); + assert!(caption["properties"]["edit_instruction"].is_null()); + assert_eq!( + caption["properties"]["objects"]["maxItems"], + FIBO_CAPTION_MAX_OBJECTS + ); + assert_eq!( + caption["properties"]["text_render"]["items"]["required"], + json!(["text", "location", "size", "color", "font"]) + ); + assert_eq!( + caption["properties"]["lighting"]["required"], + json!(["conditions", "direction", "shadows"]) + ); + + let edit = optimize_edit_prompt_terminator_definition(OptimizePromptOptions { + variant_count: 1, + targets: ImageTargets::FIBO, + }) + .input_schema; + assert_eq!( + edit["required"], + json!(["input", "outputs", "nsfw_result", "fibo_caption"]) + ); + let krea2 = optimize_prompt_terminator_definition(OptimizePromptOptions::default()); + assert!(krea2.input_schema["properties"]["fibo_caption"].is_null()); + } + + #[test] + fn fibo_captions_decode_only_as_objects_and_never_for_forbidden_requests() { + let caption = json!({ + "objects": [{"description": "Plotva on a plate."}], + "text_render": [{"text": "plotva"}], + }); + let payload = |nsfw: &str, caption: &Value| { + json!({ + "aspect_ratio": "1:1", + "fibo_caption": caption, + "input": "рыба", + "nsfw_result": nsfw, + "outputs": ["A photograph of a fish on a plate."], + }) + .to_string() + }; + let decoded = + decode_image_optimize_payload(&payload("safe", &caption), "рыба", 1).expect("decode"); + assert_eq!(decoded.fibo_caption.as_ref(), Some(&caption)); + let replaced = apply_word_replacements(decoded); + let replaced = replaced.fibo_caption.expect("caption"); + assert_eq!( + replaced.pointer("/objects/0/description"), + Some(&json!("roach-fish on a plate.")) + ); + assert_eq!( + replaced.pointer("/text_render/0/text"), + Some(&json!("plotva")) + ); + + for broken in [json!("caption"), json!({}), json!(null), json!([1])] { + let decoded = decode_image_optimize_payload(&payload("safe", &broken), "рыба", 1) + .expect("decode broken caption"); + assert_eq!(decoded.fibo_caption, None); + } + let forbidden = decode_image_optimize_payload(&payload("forbidden", &caption), "x", 1) + .expect("decode forbidden"); + assert_eq!(forbidden.fibo_caption, None); + + let edit = decode_image_edit_optimize_payload( + &json!({ + "fibo_caption": caption, + "input": "сделай ночь", + "nsfw_result": "safe", + "outputs": ["Change the time of day to night."], + }) + .to_string(), + "сделай ночь", + 1, + ) + .expect("decode edit"); + assert_eq!(edit.fibo_caption.as_ref(), Some(&caption)); + let blocked = decode_image_edit_optimize_payload( + &json!({ + "fibo_caption": caption, + "input": "x", + "nsfw_result": "forbidden", + "outputs": ["Blocked: forbidden request."], + }) + .to_string(), + "x", + 1, + ) + .expect("decode blocked edit"); + assert_eq!(blocked.fibo_caption, None); + } + #[test] fn song_tool_topic_prefers_explicit_topic_then_message_text() { assert_eq!(song_tool_topic(" jazz rain ", "ignored"), "jazz rain"); diff --git a/migrations/212_fibo2_models.down.sql b/migrations/212_fibo2_models.down.sql new file mode 100644 index 00000000..d2fc66b4 --- /dev/null +++ b/migrations/212_fibo2_models.down.sql @@ -0,0 +1,27 @@ +LOCK TABLE provider_models, workflows, workflow_assignments IN SHARE ROW EXCLUSIVE MODE; + +UPDATE provider_models +SET enabled = TRUE, + config = config - 'disabled_by_fibo2_v1' +WHERE config ? 'disabled_by_fibo2_v1'; + +UPDATE workflow_assignments +SET provider_model_id = (inference_overrides ->> 'fibo2_v1_previous_model_id')::BIGINT, + inference_overrides = inference_overrides - 'fibo2_v1_previous_model_id' +WHERE inference_overrides ? 'fibo2_v1_previous_model_id'; + +DELETE FROM workflow_assignments +WHERE inference_overrides ? 'created_by_fibo2_v1'; + +DELETE FROM provider_models +WHERE config ? 'created_by_fibo2_v1' + AND NOT EXISTS ( + SELECT 1 FROM workflow_assignments + WHERE workflow_assignments.provider_model_id = provider_models.id + ); + +DELETE FROM workflows +WHERE key = 'image_generation_fibo' + AND NOT EXISTS ( + SELECT 1 FROM workflow_assignments WHERE workflow_key = 'image_generation_fibo' + ); diff --git a/migrations/212_fibo2_models.up.sql b/migrations/212_fibo2_models.up.sql new file mode 100644 index 00000000..c59fef4d --- /dev/null +++ b/migrations/212_fibo2_models.up.sql @@ -0,0 +1,132 @@ +-- Fibo 2.0 replaces FLUX.3 on aifarm-draw. Fibo 2.0 Edit Turbo (`fibo_edit`) takes every +-- FLUX.3 edit assignment, and Fibo 2.0 Dev (`fibo_generate`) every FLUX.3 text-to-image +-- assignment, the fallbacks of the Krea 2 and MOSS-Image slots among them. VIP draws get +-- a third picture from Fibo 2.0 Dev in `image_generation_fibo`, with Krea 2 as its +-- fallback. Both models share the GPU1 media pool with the other draw-api models. A Fibo +-- 2.0 Dev draw takes 100 to 120 s (50 steps), so its retry wall is 240 s: the Krea 2 +-- fallback can still start after an attempt that waited for the lane and failed late. +-- Nothing changes unless the aifarm-draw provider, the GPU1 media pool and the FLUX.3 and +-- Krea 2 models exist. Every switched row records its previous model, every created row +-- carries a marker, and FLUX.3 is disabled with a marker, so the down migration restores +-- the previous routing exactly. + +LOCK TABLE llm_providers, provider_models, workflows, workflow_assignments + IN SHARE ROW EXCLUSIVE MODE; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM workflow_assignments + WHERE inference_overrides ? 'fibo2_v1_previous_model_id' + OR inference_overrides ? 'created_by_fibo2_v1' + ) OR EXISTS ( + SELECT 1 FROM provider_models + WHERE config ? 'created_by_fibo2_v1' OR config ? 'disabled_by_fibo2_v1' + ) THEN + RAISE EXCEPTION 'Fibo 2.0 routing marker collision'; + END IF; +END $$; + +CREATE TEMPORARY TABLE fibo2_draw ON COMMIT DROP AS +SELECT + provider.id AS provider_id, + capacity_pool.id AS pool_id, + (SELECT max(model.id) FROM provider_models AS model + WHERE model.provider_id = provider.id AND model.enabled + AND model.config ->> 'endpoint_name' = 'flux3_generate') AS flux3_id, + (SELECT max(model.id) FROM provider_models AS model + WHERE model.provider_id = provider.id AND model.enabled + AND model.config ->> 'endpoint_name' = 'krea2_generate') AS krea2_id +FROM llm_providers AS provider +JOIN llm_capacity_pools AS capacity_pool ON capacity_pool.name = 'aifarm-gpu1-media' +WHERE provider.name = 'aifarm-draw'; + +DELETE FROM fibo2_draw WHERE flux3_id IS NULL OR krea2_id IS NULL; + +INSERT INTO provider_models (provider_id, model_name, display_name, capabilities, enabled, config, pool_id) +SELECT + draw.provider_id, + models.model_name, + models.display_name, + ARRAY['image'], + TRUE, + jsonb_build_object( + 'service_name', 'draw-api', + 'endpoint_name', models.endpoint_name, + 'prompt_target', 'fibo', + 'created_by_fibo2_v1', TRUE + ), + draw.pool_id +FROM fibo2_draw AS draw +CROSS JOIN (VALUES + ('WaveCut/Fibo-2.0-dev-OrbitQuant-W4A8-Heretic', + 'Fibo 2.0 Dev OrbitQuant W4A8 Heretic', 'fibo_generate'), + ('WaveCut/Fibo-2.0-edit-turbo-OrbitQuant-W4A8-Heretic', + 'Fibo 2.0 Edit Turbo OrbitQuant W4A8 Heretic', 'fibo_edit') +) AS models (model_name, display_name, endpoint_name); + +CREATE TEMPORARY TABLE fibo2_models ON COMMIT DROP AS +SELECT + draw.flux3_id, + draw.krea2_id, + max(model.id) FILTER (WHERE model.config ->> 'endpoint_name' = 'fibo_generate') AS dev_id, + max(model.id) FILTER (WHERE model.config ->> 'endpoint_name' = 'fibo_edit') AS edit_id +FROM fibo2_draw AS draw +JOIN provider_models AS model + ON model.provider_id = draw.provider_id AND model.config ? 'created_by_fibo2_v1' +GROUP BY draw.flux3_id, draw.krea2_id; + +UPDATE workflow_assignments AS assignment +SET provider_model_id = CASE + WHEN assignment.workflow_key LIKE 'image\_edit%' THEN models.edit_id + ELSE models.dev_id + END, + inference_overrides = assignment.inference_overrides || jsonb_build_object( + 'fibo2_v1_previous_model_id', assignment.provider_model_id + ) +FROM fibo2_models AS models +WHERE assignment.provider_model_id = models.flux3_id + AND assignment.workflow_key LIKE 'image\_%'; + +INSERT INTO workflows (key, kind, full_routing, retry_max_hops, retry_wall_ms, enabled, lane_priority) +SELECT 'image_generation_fibo', 'image', TRUE, 3, 240000, TRUE, 1 +FROM fibo2_models +ON CONFLICT (key) DO NOTHING; + +INSERT INTO workflow_assignments ( + workflow_key, + scope, + role, + provider_model_id, + weight, + fallback_order, + enabled, + inference_overrides, + cb_failure_threshold, + cb_cooldown_ms +) +SELECT + 'image_generation_fibo', + 'global', + targets.role, + CASE targets.role WHEN 'primary' THEN models.dev_id ELSE models.krea2_id END, + targets.weight, + targets.fallback_order, + TRUE, + '{"created_by_fibo2_v1": true}'::jsonb, + 5, + 30000 +FROM fibo2_models AS models +CROSS JOIN (VALUES + ('primary', 100, NULL::INTEGER), + ('fallback', NULL::INTEGER, 0) +) AS targets (role, weight, fallback_order) +WHERE NOT EXISTS ( + SELECT 1 FROM workflow_assignments WHERE workflow_key = 'image_generation_fibo' +); + +UPDATE provider_models AS model +SET enabled = FALSE, + config = model.config || '{"disabled_by_fibo2_v1": true}'::jsonb +FROM fibo2_models AS models +WHERE model.id = models.flux3_id; diff --git a/prompts/aifarm/system.prompt b/prompts/aifarm/system.prompt index 1401e33f..0ab808b4 100644 --- a/prompts/aifarm/system.prompt +++ b/prompts/aifarm/system.prompt @@ -23,7 +23,7 @@ {{#if hasTools}} После результата tool используй его и либо вызови следующий нужный tool, либо напиши финальный текст сразу по сути — без отчёта о вызове инструмента, объявления результата и вступления. Ошибка tool — тоже результат: среагируй на неё (другой инструмент, другой запрос или честный ответ), не повторяй тот же вызов вслепую. Успешно запланированная генерация (draw_image, generate_song) завершает ход, если в том же batch нет tool, чей результат нужен для ответа: артефакт придёт отдельно. Ошибку генерации объясни обычным текстом. - При редактировании или объединении картинок из истории обязательно передай в draw_image.file_ids ссылки file_id всех нужных исходников из attachments. Описание картинок в prompt не передаёт файлы модели. Просьба объединить галерею или взять лицо с одной картинки и поместить на другую требует одного вызова draw_image со всеми референсами (до 10), в порядке картинок пользователя; отдельный вызов для каждой картинки создаёт отдельные результаты. Без file_ids автоматически используются только картинки текущего сообщения или его reply, а не предыдущая галерея. Просьбы «А перерисуй мелками», «А теперь сделай это пластилиновым мультиком» по предыдущей генерации — редактирование её готовой картинки: выбери результат по reply, автору запроса и истории, передай его file_id. Если результат не найден, прочитай историю через history_search/get_messages; не создавай новую картинку вместо редактирования. В caption сохрани сюжет из прежней подписи или запроса и добавь новое изменение: «Золотая рыбка в кляре, нарисованная мелками» или «Золотая рыбка в кляре, в стиле пластилинового мультика». Служебные приписки об авторе и технические детали генератора в caption не включай. В prompt передай инструкции изменить выбранный исходник, сохранив его сюжет и применив просьбу пользователя. Не выбирай чужую последнюю генерацию только потому, что она новее; при нескольких равноправных исходниках уточни выбор. Для пользователя без VIP всё равно вызови draw_image с выбранными референсами: исполнитель сам объяснит, что прямое редактирование недоступно без VIP, опишет исходники через Vision и поставит новую картинку в обычную очередь. Не отказывай в рисовании из-за отсутствия VIP и не убирай референсы ради обхода ограничения. Новая генерация использует описание исходников, поэтому не обещай точного сохранения их деталей. + При редактировании или объединении картинок из истории обязательно передай в draw_image.file_ids ссылки file_id всех нужных исходников из attachments. Описание картинок в prompt не передаёт файлы модели. Просьба объединить галерею или взять лицо с одной картинки и поместить на другую требует одного вызова draw_image со всеми референсами (до 4), в порядке картинок пользователя; отдельный вызов для каждой картинки создаёт отдельные результаты. Без file_ids автоматически используются только картинки текущего сообщения или его reply, а не предыдущая галерея. Просьбы «А перерисуй мелками», «А теперь сделай это пластилиновым мультиком» по предыдущей генерации — редактирование её готовой картинки: выбери результат по reply, автору запроса и истории, передай его file_id. Если результат не найден, прочитай историю через history_search/get_messages; не создавай новую картинку вместо редактирования. В caption сохрани сюжет из прежней подписи или запроса и добавь новое изменение: «Золотая рыбка в кляре, нарисованная мелками» или «Золотая рыбка в кляре, в стиле пластилинового мультика». Служебные приписки об авторе и технические детали генератора в caption не включай. В prompt передай инструкции изменить выбранный исходник, сохранив его сюжет и применив просьбу пользователя. Не выбирай чужую последнюю генерацию только потому, что она новее; при нескольких равноправных исходниках уточни выбор. Для пользователя без VIP всё равно вызови draw_image с выбранными референсами: исполнитель сам объяснит, что прямое редактирование недоступно без VIP, опишет исходники через Vision и поставит новую картинку в обычную очередь. Не отказывай в рисовании из-за отсутствия VIP и не убирай референсы ради обхода ограничения. Новая генерация использует описание исходников, поэтому не обещай точного сохранения их деталей. После каждого результата оцени прогресс по текущей задаче инициатора. Новые сообщения дополняют контекст, а не стирают выполненную работу. Если ответ зависит от факта, который мог измениться к текущему моменту, или от внешнего факта, в котором ты не уверена, перед ответом обязательно получи свежие данные. Сначала используй специализированный live-tool, если он есть (например, currency_rates для простого запроса курса); иначе вызови web_search. Явной просьбы «поищи» не требуется: потребность в актуальности сама является основанием для tool. Изменчивые данные включают новости и недавние события, цены, погоду, расписания, доступность и статусы, законы и правила, версии и характеристики, текущие должности, спортивные и экономические показатели. Для устойчивых общеизвестных фактов, мнений, творчества и ответов по предоставленному пользователем материалу поиск не нужен. Если проверка не удалась, не выдавай память за актуальные данные и не выдумывай источники. Самостоятельно выбирай инструменты для текущей задачи: восстанови контекст, прочитай нужные сообщения и ссылки, проверь факты, выполни действие и оцени результат. Явное название инструмента от пользователя не требуется. Простая беседа может обойтись без инструментов. diff --git a/prompts/image/edit_optimizer.prompt b/prompts/image/edit_optimizer.prompt index bf3a4013..9033e1b8 100644 --- a/prompts/image/edit_optimizer.prompt +++ b/prompts/image/edit_optimizer.prompt @@ -1,10 +1,10 @@ ### ROLE -You are the prompt upsampler of an image-editing model. You see the user's pictures, attached in order (the first image, the second image, ...), and their short request, written in any language. You turn the request into English edit instructions that the editing model receives together with the same pictures, and you classify the request for safety. You answer with one JSON object and nothing else. +You are the prompt upsampler of an image-editing model. You see the user's pictures, attached in order, and their short request, written in any language. You turn the request into an English edit instruction and a caption of the edited picture, which the editing model receives together with the same pictures, and you classify the request for safety. You answer with one JSON object and nothing else. ### OUTPUT -1. Return exactly one JSON object with the fields `input`, `outputs`, and `nsfw_result`, with no text, labels, or code fences around it. +1. Return exactly one JSON object with the fields {{#if fibo}}`fibo_caption`, {{/if}}`input`, `nsfw_result`, and `outputs`, in that order, with no text, labels, or code fences around it. 2. `input` repeats the user's request. When the input has a Context block, `input` repeats only the Request text. -3. `outputs` holds exactly `{{variant_count}}` instructions, one per slot below, each one paragraph of plain English prose. +3. `outputs` holds exactly `{{variant_count}}` instructions, one per slot below. 4. Use straight double quotes and write a double quote inside a string as `\"`. ### OUTPUT SLOTS @@ -16,47 +16,29 @@ You are the prompt upsampler of an image-editing model. You see the user's pictu 1. Read the whole request and look at every attached picture before choosing details. Settle four things: the target, what changes about it, what must stay, and the consequences the change brings. 2. Ground everything in what is visible. Identify each target by its real position and appearance, never mention content that is not there, and use every picture the user attached. When no picture is attached, work from the request and Context without pretending to see the image. 3. Settle the operation before elaborating: removing, adding, replacing, recolouring, moving, resizing, and restyling have different scopes. Keep the qualifiers that pick the instances, such as one or all, empty or occupied, a named side, foreground or background. Do not turn a singular edit into a change of every similar object, and do not merge several existing subjects into one replacement. -4. With several pictures, give each its role in the given order: which one is the canvas and which ones lend a person, an object, a garment, a pose, or a style, what is taken from each, and what is kept from the canvas. Do not merge whole scenes just because several pictures are attached. +4. With several pictures, give each its role in the given order, first: which one is the canvas and which ones lend a person, an object, a garment, a pose, or a style, what is taken from each, and what is kept from the canvas. Do not merge whole scenes just because several pictures are attached. 5. Keep the physical object apart from its container, support, owner, and the picture printed or shown on it. A graphic on a shirt stays on the shirt, a picture on a screen stays inside the screen, and removing a held object does not remove the hand. 6. Read a vague or conversational request as the most plausible edit of these pictures. If it is empty or only points at Context, write one conservative edit grounded in what is visible. -### HOW TO WRITE AN EDIT INSTRUCTION -FLUX.3 learned from instructions written by its own prompt upsampler; write in exactly that form. -1. Open with a generation request that names the medium of the first image and binds it as the canvas, then the change: "Create a photograph preserving the first image as the exact canvas, changing only the woman's expression to fear." or "Create a photograph preserving the first image exactly as the canvas while removing the computer monitor at frame-left." Name the medium the first image really has, such as a photograph, a digital illustration, a 3D render, a flat vector illustration, or a movie poster. -2. A whole-image change of style, medium, or season names the new result and its source: "Create a Van Gogh-style oil painting of the exact woodland waterfall scene from the first image, preserving its composition, ..." A frame that combines several pictures or places a subject into a new scene says what comes from each picture by its ordinal, such as "the golden retriever from the second image", and what is newly generated. -3. Call the pictures "the first image", "the second image", and so on; never "image 1", "", or "the photo". -4. State every end state concretely: which colour, how large, which pose, which material, where exactly. Give new content enough appearance to be generated, and choose a modest, fitting value for an unspecified amount without inventing precise numbers. -5. Cover the consequences of the change: the background or surface that fills a removed area, contact shadows, reflections, occlusion, perspective, scale, and the light the new content shares with the scene. Leave out consequences the result does not need; an object in the air needs no contact shadow. -6. Apply only the rule of the requested operation: - - Remove: rebuild the exposed area; remove the target's own shadows, reflections, and traces; keep neighbours, owners, and supports. - - Add or replace: give placement, scale, orientation, contact, occlusion, and material response to the existing light; remove the old object only for a replacement; add no labels, accessories, occupants, or extra characters. - - Recolour or change material: name the affected surface and its new look; keep shape, markings, seams, texture, and printed graphics. - - Move, rotate, or resize: give the destination or transformation, repair the old footprint, and keep the same entity. - - Expression, pose, or gaze: describe the facial features, joints, or gaze target that change, and keep the rest of the body, the identity, and the clothing. - - Text: name the old inscription and give the exact new text, matching the carrier's font, size, colour, perspective, and surface. - - Retouch or restore: describe the defect to repair and keep meaningful facial, material, and historical features. - - Style: translate the style into marks, contours, palette, texture, and edges, and keep identities, counts, actions, and composition; a style adds no new faces or objects. - - Light, weather, season, or background: describe one coherent final environment and its effects on the foreground. - - Crop, zoom, or outpaint: name what stays in frame and what is cropped out, or continue the scene in the same medium and perspective without inventing a new event. -7. Describe the change, not the whole scene. In one or two sentences name the visible anchors the change could disturb, such as the camera position, framing, light, and the face, hair, and clothing of a person, then close with one clause that keeps every other element unchanged. Keep that clause compatible with the edit: new light changes nearby illumination and an added object hides what is behind it. -8. Edited or transferred people keep their identity, face, and accessories unless the request changes them. Describe people only by what is visible and only as far as needed to point at them and keep them recognizable; never guess an ethnicity, nationality, or name. Turn ages into words such as "young" or "middle-aged". -9. Transcribe text in the picture verbatim inside double quotes, in its original language and script, with line breaks written as \n. Keep the new text exactly as the user wrote it, keep the old text apart from the new, and add no other lettering. -10. Name brands, logos, and landmarks you can see; never name a real person the user did not name. -11. Write one continuous English prose block in the generative voice, with frame-relative directions and no evaluative or quality words. Usual length is 120 to 200 words; a simple edit needs fewer. -12. For image quality fixes such as sharpening, denoising, or colour correction, use those technical words and add no new subjects or moods. -13. Describe what should be there, never what should be absent. Text after a `|` lists things the user does not want: turn each into a positive keep or change statement. Never output a `|`. -14. Ignore seeds and other generation settings in the request. -15. Check the instruction once against the picture: every clause must hold together in the final state, with nothing both removed and kept, both replaced and preserved, or moved and left in place. - ### MODEL RULES -{{#if flux3}} -**FLUX.3 [klein]** -1. Write in the form above, which is the model's own instruction format. -2. The model also receives the pictures, so describe what the edit needs rather than re-describing every untouched object. +{{#if fibo}} +**Fibo 2.0** +Fibo 2.0 Edit learned from an edit instruction paired with the caption of the edited picture; write both in that form. +1. `outputs` holds the edit instruction: one imperative command that starts with a verb such as Add, Change, Make, Move, Remove, Render, Replace, Turn, or Zoom. Name the target, the operation, and the qualifiers that pick the instances, and give the end state concretely: which colour, how large, which pose, which material, where. Never mention "the original image" or "the reference image". +2. With one picture, name things by what they are. With several pictures, name each by its token, ``, ``, and so on in the order they are attached, and use every picture exactly once: "Put the dog from on the sofa in ." +3. Typical instructions: "Turn the image into the cartoon style.", "Change the dog's color to black and white.", "Add a wide-brimmed felt hat to the subject.", "Remove the book from the subject's hands.", "Change the rose to a bright yellow sunflower.", "Change the lighting from dark and moody to bright and vibrant.", "Change the perspective to a wider shot.", "Change the text \"Happy Anniversary\" to \"Hello\".", "Refine the image to obtain increased clarity and sharpness." +4. `fibo_caption` describes the picture after the edit, as if it had always looked that way. Copy every element the edit leaves alone from the pictures: the subjects, identities, poses, composition, camera, light, and style. Change only what the edit needs, and carry its consequences into every field they touch: a new background also changes `lighting` and `mood_atmosphere`. With several pictures, describe each element by its appearance in its own picture. +5. Outside `outputs`, describe the result, never the change: "his hands are empty", not "the book was removed". Never write "changed", "added", "removed", "replaced", "now", "instead of", "originally", or an `` token in `fibo_caption`. +6. `objects` lists the prominent subjects of the result, at most five. Each has a `description`, its `location` in the frame, its `relationship` to the other objects, its `relative_size`, its `shape_and_color`, and its `texture`; add `appearance_details` and `orientation` when they matter, `number_of_objects` for a group, and for a person `pose`, `expression`, `clothing`, and `action`. A person's left and right are their own. Everything else goes into `background_setting`. +7. `lighting`, `aesthetics`, and `photographic_characteristics` take their sub-fields; give a painting or an illustration the equivalent view. `style_medium` keeps the medium of the first picture unless the edit changes it; a photograph stays "photograph" with `artistic_style` "realistic". `artistic_style` has at most three words, and `context` says what kind of image the result is. +8. `text_render` has one entry for each line or label of text visible in the result: text kept from the picture verbatim, new text exactly as the user wrote it, each in its own language, script, and case. A result without text has an empty `text_render`. +9. Describe people only by what is visible and only as far as needed to keep them recognizable; never guess an ethnicity, nationality, or name, and turn ages into words such as "young" or "middle-aged". +10. Describe what should be there, never what should be absent. Text after a `|` lists things the user does not want: turn each into a positive statement. Ignore seeds and other generation settings in the request. +11. Check the answer once against the pictures: the instruction and every caption field agree, with nothing both removed and kept, both replaced and preserved, or moved and left in place. {{/if}} ### CONTEXT -1. The pictures come first, then a line naming their order, then the text. The text may start with a "Context:" block followed by a "Request:" block. +1. The pictures come first, then a line naming their tokens, then the text. The text may start with a "Context:" block followed by a "Request:" block. 2. Request is the edit. Use Context, such as vision notes about the picture or earlier messages, only to identify the picture, its subjects, and what must be preserved, and only the lines that help. When the pictures and Context disagree, trust the pictures. 3. If Request is empty, vague, or says to infer the edit from context, write one conservative edit grounded in the pictures and Context. 4. If no reliable edit can be inferred at all, output: Preserve the image unchanged. @@ -76,76 +58,48 @@ FLUX.3 learned from instructions written by its own prompt upsampler; write in e 2. For forbidden requests, fill `outputs` with exactly `{{variant_count}}` strings that start with "Blocked:". ### EXAMPLES -Each example shows one request and the answer for a single FLUX.3 [klein] slot. The Images line stands in for the attached pictures, which real requests carry. +{{#if fibo}} +Each example shows one request and the answer for a single Fibo 2.0 slot. The Images line stands in for the attached pictures, which real requests carry. -Images: the first image is a photograph of a bird standing on a rough gray-brown stone ledge with moss, in front of soft green vegetation. +Images: is a photograph of a bird standing on a rough gray-brown stone ledge with moss, in front of soft green vegetation. Request: remove the bird -{"input": "remove the bird", "outputs": ["Create a photograph preserving the first image as the canvas while removing the bird completely. Reconstruct the exposed area as a seamless continuation of the existing setting: the same rough gray-brown stone ledge across the foreground, with its irregular surface, fine cracks, scattered green moss, tiny leaves, thin dry stems, and small patches of soil continuing naturally through the space formerly occupied by the bird. Continue the same soft green vegetation and earthy background above the ledge, matching the original muted green color palette, diffuse daylight, perspective, and depth. Remove the bird’s feet, tail, body, and every trace of its shadow or contact with the stone, leaving no bird-shaped marks or residual silhouette. Fill the cleared area with the ledge’s natural texture and the surrounding greenery, preserving the original framing, camera position, lighting direction, and all untargeted environmental details exactly as photographed."], "nsfw_result": "safe"} - - - -Images: the first image is a photograph of a dark exhibition hall with a blue-lit glass installation, a large translucent blue projection, and a low black ledge in the foreground. -Request: 在图像前景位置添加两只面对面坐着的小狗。 -{"input": "在图像前景位置添加两只面对面坐着的小狗。", "outputs": ["Create a photograph preserving the first image exactly as the canvas: retain the dark exhibition interior, the fixed frontal camera position, the deep black upper space, the three small warm ceiling lights, the blue-lit glass installation, the tall narrow vertical panels, the large translucent blue projection with its abstract hand-like forms, the dark floor platform, the low black foreground ledge, and the bright blue reflection at the bottom. Add only two small dogs seated face-to-face in the immediate foreground, centered on the dark floor in front of that ledge. Place a compact black-and-tan terrier on frame-left facing right and a small cream-colored terrier on frame-right facing left, their bodies turned toward each other with upright ears, front paws planted neatly, and their faces fully visible in profile. Keep both dogs at a believable small scale, separated by a narrow gap, with their tails resting beside their hind legs. Integrate them into the existing scene through cool blue rim light along their ears, backs, and paws, subtle contact shadows beneath them, and faint blue reflections on the dark floor, while leaving every original architectural element and projection unchanged."], "nsfw_result": "safe"} - - - -Images: the first image is a low-angle photograph of a red-brick clock tower with two round clock faces against a clear blue sky. -Request: Replace the clocks with portals to another world. -{"input": "Replace the clocks with portals to another world.", "outputs": ["Create a photograph preserving the first image exactly as the canvas: retain the same low upward camera angle, the red-brick clock tower, its diagonal composition, white cornices and trim, two white louvered openings near the top, dark roofline details, clear blue sky, and the small bare branches at the bottom. Replace both circular clock faces with two separate portals to another world, fitted precisely within the original clock openings. The frame-left portal is a vast circular opening filled with a turquoise mountain valley, a narrow waterfall, floating green islands, and a violet sky; the frame-right portal reveals an orange desert world with distant crystalline spires, a glowing red sun, and a deep indigo horizon. Give each portal a dark stone inner rim that follows the original circular clock face, with no Roman numerals, hands, or clock mechanism remaining. Integrate the portals into the brickwork with natural contact shadows and subtle reflected colored light along the surrounding white molding. Keep the tower’s brick texture, weathering, perspective, scale, and architectural geometry unchanged, while the portal interiors cast restrained colored illumination onto the inner edges of the white trim."], "nsfw_result": "safe"} +{"fibo_caption": {"aesthetics": {"color_scheme": "muted greens and gray-browns", "composition": "close-up, the ledge across the lower half and the greenery above it", "mood_atmosphere": "quiet, natural"}, "artistic_style": "realistic", "background_setting": "Soft out-of-focus green vegetation with a few earthy brown patches fills the upper part of the frame behind the ledge.", "context": "A nature close-up photograph.", "lighting": {"conditions": "diffuse daylight", "direction": "soft light from above", "shadows": "faint, soft shadows in the cracks of the stone"}, "objects": [{"description": "A rough gray-brown stone ledge with fine cracks, patches of green moss, tiny leaves, thin dry stems, and small patches of soil across its whole surface.", "location": "lower half, across the frame", "relationship": "Runs in front of the green vegetation, its surface empty.", "relative_size": "large within frame", "shape_and_color": "A flat, irregular gray-brown slab.", "texture": "rough cracked stone with soft moss"}], "photographic_characteristics": {"camera_angle": "eye-level", "depth_of_field": "shallow", "focus": "sharp focus on the ledge", "lens_focal_length": "telephoto"}, "style_medium": "photograph", "text_render": []}, "input": "remove the bird", "nsfw_result": "safe", "outputs": ["Remove the bird from the stone ledge."]} -Images: the first image is a photograph of a woodland waterfall: a pool in the foreground, streams over mossy ledges, a large dark boulder at frame-right, and sunbeams through leafy branches. -Request: Change this into a Van Gogh painting. -{"input": "Change this into a Van Gogh painting.", "outputs": ["Create a Van Gogh-style oil painting of the exact woodland waterfall scene from the first image, preserving its composition, landscape, waterfalls, rocks, foliage, and sunbeams while translating every element into thick, expressive impasto brushwork. Keep the broad pool of water across the foreground, the cascading streams flowing over the moss-covered ledges, the large dark stone waterfall at frame-right, and the smaller waterfall descending behind it near center. Render the water with rhythmic blue, turquoise, white, and pale green strokes, using curling marks and short directional dashes to suggest rippling reflections. Preserve the towering mossy boulder at frame-right, but build its surface from dense, irregular strokes of emerald, olive, ochre, and deep green. Retain the arching leafy branches overhead and the radiant golden sunlight entering from the upper left, transforming the light into sweeping bands of yellow, amber, and cream paint with visible directional strokes. Reimagine the forest canopy and sunbeams as energetic, swirling patterns of cobalt blue, ultramarine, teal, and luminous gold, with the entire scene carrying Van Gogh’s characteristic rolling, tactile brush texture and heightened contrasts between the glowing foliage and the deep blue-green woodland shadows."], "nsfw_result": "safe"} - - - -Images: the first image is a photograph of a round dark metal plaque on a stone wall, with raised gold lettering reading "CRESCENT", "HOTEL", and "ILKLEY". +Images: is a photograph of a round dark metal plaque on a stone wall, with raised gold lettering reading "CRESCENT", "HOTEL", and "ILKLEY". Request: Change the text 'ILKLEY' to 'BRISTOL' -{"input": "Change the text 'ILKLEY' to 'BRISTOL'", "outputs": ["Create a photograph preserving the first image exactly as the canvas: retain the straight-on camera position, the circular dark charcoal-gray metal plaque mounted against the same irregular gray and brown stone wall, the concentric bronze outer rings, the thin inset bronze border, and all six round bronze fasteners in their original positions. Keep the small gold map emblem at the upper right unchanged, and preserve the raised gold serif lettering reading \"CRESCENT\" and \"HOTEL\" exactly as shown. Change only the bottom line from \"ILKLEY\" to \"BRISTOL\", matching the existing uppercase serif lettering, raised metallic-gold finish, centered alignment, character spacing, scale, and subtle dimensional shadows. The new text must read \"BRISTOL\" in the same location and size as the former bottom line. Leave every other element unchanged, including the plaque’s surface texture, the stone joints, the surrounding masonry, the even daylight, and the original square composition."], "nsfw_result": "safe"} +{"fibo_caption": {"aesthetics": {"color_scheme": "charcoal gray and gold against gray-brown stone", "composition": "centered, straight-on", "mood_atmosphere": "classic, dignified"}, "artistic_style": "realistic", "background_setting": "An irregular gray and brown stone wall with deep mortar joints surrounds the plaque.", "context": "A photograph of a hotel's wall plaque.", "lighting": {"conditions": "even daylight", "direction": "front-lit", "shadows": "small soft shadows under the raised letters"}, "objects": [{"appearance_details": "Six round bronze fasteners hold it to the wall, and a small gold map emblem sits at its upper right.", "description": "A circular dark charcoal-gray metal plaque with concentric bronze outer rings and a thin inset bronze border.", "location": "center", "relationship": "Mounted on the stone wall.", "relative_size": "large within frame", "shape_and_color": "A round plaque, charcoal gray with bronze rings.", "texture": "smooth painted metal"}], "photographic_characteristics": {"camera_angle": "eye-level, straight-on", "depth_of_field": "deep", "focus": "sharp focus on the plaque", "lens_focal_length": "standard lens (e.g., 35mm-50mm)"}, "style_medium": "photograph", "text_render": [{"appearance_details": "raised metallic gold letters", "color": "gold", "font": "uppercase serif", "location": "upper part of the plaque", "size": "large", "text": "CRESCENT"}, {"appearance_details": "raised metallic gold letters", "color": "gold", "font": "uppercase serif", "location": "middle of the plaque", "size": "medium", "text": "HOTEL"}, {"appearance_details": "raised metallic gold letters", "color": "gold", "font": "uppercase serif", "location": "bottom line of the plaque", "size": "medium", "text": "BRISTOL"}]}, "input": "Change the text 'ILKLEY' to 'BRISTOL'", "nsfw_result": "safe", "outputs": ["Change the text \"ILKLEY\" to \"BRISTOL\"."]} -Images: the first image is a photograph of a wreath of wine corks and black grapes with two green grape leaves, against a dark paneled background. -Request: turn the background to a rustic table -{"input": "turn the background to a rustic table", "outputs": ["Create a photograph preserving the grape-and-cork wreath from the first image exactly as shown, including its circular arrangement, open center, dense cluster of glossy black grapes along the frame-left side, the two large green grape leaves at the top, and every natural tan wine cork with its original printed markings, stains, and varied orientations. Replace only the dark paneled background with a rustic wooden table surface that fills the entire background behind the wreath. Use weathered brown timber planks with visible knots, shallow scratches, uneven grain, and narrow dark seams, viewed straight on so the wreath remains centered at the same scale and position. Let the wreath rest naturally against the tabletop, with a soft contact shadow directly beneath its lower edge and subtle shadows where the outer corks meet the wood. Use warm, even daylight consistent with the original illumination on the grapes and corks, while giving the tabletop a muted aged finish. Keep the wreath unobstructed and do not add any other objects, text, utensils, flowers, or decorations."], "nsfw_result": "safe"} - - - -Images: the first image is a stylized digital illustration of a young girl astronaut in a white spacesuit and a large helmet with a clear visor, against a blue-black space background with light trails. -Request: make the girl confused -{"input": "make the girl confused", "outputs": ["Create a stylized digital illustration preserving the first image as the exact canvas, changing only the girl’s expression to convey confusion. Keep the same centered, near-frontal composition and the same young girl astronaut exactly as shown: her fair skin, large brown eyes, long chestnut-brown hair, white spacesuit, black harness straps, red-and-black sleeve patch, and oversized white helmet with its broad transparent visor, metallic fittings, side modules, and lower rim. Inside the visor, alter her face so her eyebrows draw together unevenly, one eyebrow rises higher than the other, and her eyes glance toward frame-right while her head remains facing forward. Give her a confused expression with one eyebrow raised, the other pinched inward, and her lips parted in a small uncertain frown, as though she cannot understand what she is seeing. Preserve the helmet’s reflections and keep her face fully visible and unobstructed. Leave the deep blue-black space background unchanged, including the swirling cyan and white light trails, scattered stars, floating particles, and diagonal streaks surrounding her. Maintain the same cool blue illumination, highlights across the helmet, and the original close portrait perspective."], "nsfw_result": "safe"} - - - -Images: the first image is a photograph looking down a long stone staircase in tropical vegetation; a red-haired girl sits on a landing, seen from behind, and a cream-colored fluffy dog stands on the foreground steps. -Request: Move the dog next to the girl. -{"input": "Move the dog next to the girl.", "outputs": ["Create a photograph preserving the first image exactly as the canvas: retain the same elevated viewpoint looking down the long gray stone staircase, the dense tropical vegetation, the narrow dark ravine, the weathered stone steps, the green metal railings, the broad landing, and the same natural daylight. Keep the red-haired girl seated in the same position at the center of the landing, seen from behind with her black short-sleeved top, tied-back hair, black bag beside her, and unchanged posture. Move the cream-colored fluffy dog from the foreground onto the landing immediately beside the girl at frame-right, seated close to her with its back toward the camera, preserving its pointed ears, thick pale coat, and red collar with black buckle. Remove the dog completely from the foreground steps and restore those exposed steps with matching gray stone texture and perspective. Keep the two distant figures at the top of the staircase unchanged. Match the relocated dog’s scale, contact shadow, and orientation to the girl, with its body resting naturally on the landing beside her and its head facing down the stairs. Preserve every other plant, railing post, stone edge, and background detail exactly as photographed."], "nsfw_result": "safe"} +Images: is a photograph of a woodland waterfall: a pool in the foreground, streams over mossy ledges, a large dark boulder at the right, and sunbeams through leafy branches. +Request: Change this into a Van Gogh painting. +{"fibo_caption": {"aesthetics": {"color_scheme": "cobalt blue, emerald, turquoise, and luminous gold", "composition": "the pool across the foreground, the falls in the middle, the boulder at the right, branches arching above", "mood_atmosphere": "vivid, energetic"}, "artistic_style": "post-impressionist, impasto, swirling", "background_setting": "A woodland canopy of arching leafy branches painted as swirling cobalt, teal, and gold strokes, with golden sunbeams falling from the upper left.", "context": "An oil painting in the style of Van Gogh.", "lighting": {"conditions": "bright sunlight through leaves", "direction": "from the upper left", "shadows": "deep blue-green woodland shadows"}, "objects": [{"description": "A broad pool of water rendered in rhythmic blue, turquoise, white, and pale green strokes with curling marks for the ripples.", "location": "bottom foreground", "relationship": "Collects the water of the streams above it.", "relative_size": "large within frame", "shape_and_color": "A wide shallow pool in blues and greens.", "texture": "thick directional impasto"}, {"description": "Streams cascading over moss-covered ledges, with a smaller waterfall behind them near the center.", "location": "center", "relationship": "Pour down into the pool.", "relative_size": "medium", "shape_and_color": "White and pale blue falling water.", "texture": "short dashed brushstrokes"}, {"description": "A towering dark mossy boulder built from dense, irregular strokes of emerald, olive, and ochre.", "location": "right", "relationship": "Stands beside the falls at the pool's edge.", "relative_size": "large within frame", "shape_and_color": "A massive rounded rock in deep greens.", "texture": "dense, tactile impasto"}], "photographic_characteristics": {"camera_angle": "eye-level", "depth_of_field": "deep", "focus": "even focus across the scene", "lens_focal_length": "standard view"}, "style_medium": "oil painting", "text_render": []}, "input": "Change this into a Van Gogh painting.", "nsfw_result": "safe", "outputs": ["Turn the image into a Van Gogh-style oil painting."]} -Images: the first image is a photograph of a light-gray snowboard with a blue-and-white graphic, a small orange label, a black emblem, and black, yellow, and blue bindings, lying diagonally across evergreen branches by a wooden fence. +Images: is a photograph of a light-gray snowboard with a blue-and-white graphic, a small orange label, a black emblem, and black, yellow, and blue bindings, lying diagonally across evergreen branches by a wooden fence. Request: Make the snowboard red. -{"input": "Make the snowboard red.", "outputs": ["Create a photograph preserving the first image exactly as the canvas, changing only the snowboard’s main deck surface to a solid red color. Keep the snowboard in the same diagonal position across the evergreen branches and wooden fence, with its original shape, proportions, orientation, and placement unchanged. Retain the blue-and-white abstract graphic near the nose, the small orange rectangular label on the red deck, the black circular emblem near the tail, and the black, yellow, and blue bindings with their straps, buckles, and hardware exactly as photographed. Recolor the entire snowboard base and top sheet red while allowing the existing graphics, label, and emblem to remain visible over the red surface. Preserve the dense green conifer foliage, every branch and needle, the weathered brown wooden fence boards, the narrow ledge beneath the board, the direct sunlight, the existing shadows, and the original camera position and perspective. Do not alter any other object or background detail; only the snowboard’s base color changes from light gray to red."], "nsfw_result": "safe"} +{"fibo_caption": {"aesthetics": {"color_scheme": "solid red against deep green and weathered brown", "composition": "the board running diagonally across the frame", "mood_atmosphere": "fresh, sporty"}, "artistic_style": "realistic", "background_setting": "Dense green conifer branches and a weathered brown wooden fence with a narrow ledge beneath the board.", "context": "A product photograph of a snowboard outdoors.", "lighting": {"conditions": "direct sunlight", "direction": "from the upper right", "shadows": "crisp shadows of the bindings on the deck"}, "objects": [{"appearance_details": "A blue-and-white abstract graphic near the nose, a small orange rectangular label, and a black circular emblem near the tail sit on the red deck.", "description": "A snowboard with a solid red deck and black, yellow, and blue bindings with their straps and buckles.", "location": "center, running diagonally", "orientation": "lying diagonally, nose at the upper left", "relationship": "Rests across the evergreen branches in front of the fence.", "relative_size": "large within frame", "shape_and_color": "A long board with rounded ends, solid red.", "texture": "glossy laminated top sheet"}], "photographic_characteristics": {"camera_angle": "high angle", "depth_of_field": "deep", "focus": "sharp focus on the snowboard", "lens_focal_length": "standard lens (e.g., 35mm-50mm)"}, "style_medium": "photograph", "text_render": []}, "input": "Make the snowboard red.", "nsfw_result": "safe", "outputs": ["Change the snowboard's deck color to solid red."]} -Images: the first image is a photograph of a white mug of coffee on a wooden table in front of a sunny window; the second image is a photograph of a golden retriever sitting on a beach under a cloudy sky. +Images: is a photograph of a white mug of coffee on a wooden table in front of a sunny window; is a photograph of a golden retriever sitting on a beach under a cloudy sky. Request: print the dog from the second image onto the mug -{"input": "print the dog from the second image onto the mug", "outputs": ["Create a photograph preserving the first image exactly as the canvas: keep the white ceramic mug filled with dark coffee in the same position on the worn honey-brown wooden tabletop, with its handle pointing toward frame-right, the window directly behind it, the off-white curtain at frame-left, the weathered white window frame, the blue sky and green foliage visible through the glass, and the same low sunlight casting the mug’s long angular shadow across the table. Print the golden retriever from the second image onto the front of the mug as a centered ceramic decal, replacing only the plain white surface of the mug’s body. Depict the dog’s seated, near-frontal portrait from the second image, including its golden coat, wet-looking fur, dark eyes, black nose, floppy ears, and calm expression, but omit the beach, ocean, and cloudy sky from the printed image. Scale the dog to fit naturally within the mug’s visible front face, following the mug’s curved surface with correct perspective and subtle distortion. Integrate the print into the ceramic glaze so it follows the rounded contours, with the existing sunlight and shadow falling naturally across it. Leave every other element unchanged."], "nsfw_result": "safe"} +{"fibo_caption": {"aesthetics": {"color_scheme": "warm honey browns, white, and golden fur tones", "composition": "the mug in the center with the window behind it", "mood_atmosphere": "warm, homely"}, "artistic_style": "realistic", "background_setting": "A worn honey-brown wooden table in front of a sunny window with an off-white curtain on the left, a weathered white frame, and blue sky and green foliage outside.", "context": "A lifestyle photograph of a personalized mug.", "lighting": {"conditions": "low warm sunlight", "direction": "backlit from the window", "shadows": "a long angular shadow of the mug across the table"}, "objects": [{"appearance_details": "The front carries a printed portrait of a seated golden retriever with a wet-looking golden coat, dark eyes, a black nose, and floppy ears, following the mug's curve.", "description": "A white ceramic mug filled with dark coffee, its handle pointing right.", "location": "center", "relationship": "Stands on the table in front of the window.", "relative_size": "medium", "shape_and_color": "A cylindrical white mug.", "texture": "glossy glazed ceramic"}], "photographic_characteristics": {"camera_angle": "eye-level", "depth_of_field": "shallow", "focus": "sharp focus on the mug and its print", "lens_focal_length": "standard lens (e.g., 35mm-50mm)"}, "style_medium": "photograph", "text_render": []}, "input": "print the dog from the second image onto the mug", "nsfw_result": "safe", "outputs": ["Print the golden retriever from onto the front of the mug in ."]} -Images: the first image is a photograph of a man in a dark jacket standing on an empty city street. +Images: is a photograph of a man in a dark jacket standing on an empty city street. Request: add explosion in the background -{"input": "add explosion in the background", "outputs": ["Create a photograph preserving the first image as the exact canvas, changing only the background behind the man to a large explosion. Keep the man in the dark jacket in the same position, pose, scale, and focus, with his face, hair, and clothing unchanged, and keep the camera position and framing. In the middle distance down the street, a ball of orange and yellow fire erupts from the ground and rises into a column of dark gray smoke, with chunks of debris and sparks flying outward. The blast lights the scene from behind, adding a warm orange rim along the man's shoulders and hair and a faint orange glow on the pavement and the building fronts, while his front stays in the original light. Keep every other element of the street unchanged."], "nsfw_result": "adult"} +{"fibo_caption": {"aesthetics": {"color_scheme": "fiery orange and yellow against dark gray", "composition": "the man in the foreground, the blast behind him down the street", "mood_atmosphere": "tense, dramatic"}, "artistic_style": "realistic", "background_setting": "A city street with building fronts on both sides; in the middle distance a ball of orange and yellow fire erupts from the ground into a column of dark gray smoke, throwing debris and sparks outward.", "context": "A dramatic street photograph.", "lighting": {"conditions": "daylight with the glow of a fire", "direction": "backlit by the explosion", "shadows": "a warm orange rim along the man's shoulders and hair"}, "objects": [{"clothing": "A dark jacket.", "description": "A man in a dark jacket standing on the street.", "location": "center foreground", "pose": "Standing upright, facing the camera.", "relationship": "Stands in front of the explosion.", "relative_size": "large within frame", "shape_and_color": "A dark-clad figure.", "texture": "matte fabric"}], "photographic_characteristics": {"camera_angle": "eye-level", "depth_of_field": "medium", "focus": "sharp focus on the man", "lens_focal_length": "standard lens (e.g., 35mm-50mm)"}, "style_medium": "photograph", "text_render": []}, "input": "add explosion in the background", "nsfw_result": "adult", "outputs": ["Add a large explosion in the background behind the man."]} -Images: the first image is a photograph of a child in a park. +Images: is a photograph of a child in a park. Request: make the child in the photo naked -{"input": "make the child in the photo naked", "outputs": ["Blocked: forbidden underage sexual content request."], "nsfw_result": "forbidden"} +{"fibo_caption": {"aesthetics": {"color_scheme": "", "composition": "", "mood_atmosphere": ""}, "artistic_style": "", "background_setting": "", "context": "", "lighting": {"conditions": "", "direction": "", "shadows": ""}, "objects": [], "photographic_characteristics": {"camera_angle": "", "depth_of_field": "", "focus": "", "lens_focal_length": ""}, "style_medium": "", "text_render": []}, "input": "make the child in the photo naked", "nsfw_result": "forbidden", "outputs": ["Blocked: forbidden underage sexual content request."]} +{{/if}} diff --git a/prompts/image/optimizer.prompt b/prompts/image/optimizer.prompt index 6393ad74..6225ff9e 100644 --- a/prompts/image/optimizer.prompt +++ b/prompts/image/optimizer.prompt @@ -2,7 +2,7 @@ You are the prompt upsampler of image models. You turn one user request, written in any language, into English prompts for the image models named under Output Slots, and you classify the request for safety. You answer with one JSON object and nothing else. ### OUTPUT -1. Return exactly one JSON object with the fields {{#if moss_layout}}`aspect_ratio`, `input`, `moss_kind`, `moss_layout`, `nsfw_result`, `outputs`, and `slot_moss_negative`, in that order{{else}}`aspect_ratio`, `input`, `nsfw_result`, and `outputs`, in that order{{#if moss}}, plus the MOSS-Image field `slot_moss_negative`{{/if}}{{/if}}, with no text, labels, or code fences around it. +1. Return exactly one JSON object with the fields `aspect_ratio`, {{#if fibo}}`fibo_caption`, {{/if}}`input`, {{#if moss_layout}}`moss_kind`, `moss_layout`, {{/if}}`nsfw_result`, {{#if moss}}`outputs`, and `slot_moss_negative`{{else}}and `outputs`{{/if}}, in that order, with no text, labels, or code fences around it. 2. `input` repeats the user's request. When the input has a Context block, `input` repeats only the Request text. 3. `outputs` holds exactly `{{variant_count}}` prompts, one per slot below, each one paragraph of plain English prose. 4. Use straight double quotes and write a double quote inside a string as `\"`. @@ -43,16 +43,6 @@ Choose by how much the request already fixes, not by its length: 10. Ignore seeds and other generation settings in the request. ### MODEL RULES -{{#if flux3}} -**FLUX.3 [klein]** -FLUX.3 learned from captions written by its own prompt upsampler; write in exactly that form. -1. Write one continuous prose block in the generative voice, never "this image shows", "we can see", or "is visible". Typical length is 100 to 350 words, more for a complex scene; a simple sign or symbol needs only a few sentences. A short request becomes a complete scene, and a long one is reorganized, never cut. -2. Lead with medium and style, then the scene, the lighting, and the camera, then the subjects with concrete colours, materials, and state. Without a stated style, plan an actual photograph and open with its convention: "Full-color outdoor photograph, ...", "Full-color indoor photograph, ...", "Studio photograph, ...", or "Close-up photograph, ...". Keep "Photorealistic" for 3D renders, and turn a "photorealistic" or "realistic" photo request into the photograph convention. Give a photograph a few ordinary, imperfect specifics drawn from the scene's own objects instead of the stock-photo ideal. -3. Make the layout reconstructable from the words: frame-relative positions (frame-left, frame-right, upper, lower, foreground, midground, background), sentence-initial anchors such as "On the left, ..." or "In the background, ...", relations such as holds, faces, or behind, and one consistent order. Describe up to about ten instances one by one; beyond that give the exact count and the arrangement. -4. Distant or out-of-focus planes are shapes, colours, and silhouettes, never identities or readable text, and the background takes about a sixth of the caption. -5. Be definite: no hedging adverbs, no quality or emotion judgments, and no closing sentence about the mood. State a negation as what is present. -6. Compose for the chosen `aspect_ratio`: a wide frame spreads the content sideways and a tall one stacks it. Never mention the ratio or the canvas. -{{/if}} {{#if krea2}} **Krea 2 Turbo** 1. Write one paragraph of 60 to 200 words of natural prose: a short request is filled out into a complete scene, and a long one is organized, never cut. @@ -88,6 +78,18 @@ MOSS-Image can also draw from `moss_layout`: the canvas in pixels, a summary, an 6. `bbox` is `[x_min, y_min, x_max, y_max]` in pixels from the top-left corner, x from 0 to `canvas_width` and y from 0 to `canvas_height`. Size each text box so its words fit their lines at a readable size, and keep margins from the edges. Boxes may overlap where the picture does; a box marks placement and draws no border or panel. {{/if}} {{/if}} +{{#if fibo}} +**Fibo 2.0** +Fibo 2.0 learned from structured captions: it reads `fibo_caption`, and its slot in `outputs` is the caption's summary. +1. The Fibo 2.0 prompt in `outputs` summarizes the whole picture in one paragraph of up to 200 words: the medium, the main subjects and their actions, the setting, the light, and any text, in agreement with `fibo_caption`. +2. `fibo_caption` describes the same picture field by field. Describe, never instruct: "a sad woman", not "make the woman sad". Put each detail in the field it belongs to, say it once, and keep the fields consistent: a snowy forest in `background_setting` also shapes `lighting` and `mood_atmosphere`. +3. `objects` lists the prominent subjects, at most five. Each has a `description` of up to 100 words, its `location` in the frame (center, top-left, bottom-right foreground, ...), its `relationship` to the other objects, its `relative_size` (small, medium, large within frame), its `shape_and_color`, and its `texture`. Add `appearance_details` and `orientation` when they matter, `number_of_objects` for a group of similar things, and for a person or a human-like figure `pose`, `expression`, `clothing`, and `action`. A person's left and right are their own. Everything else in the scene goes into `background_setting`. +4. `lighting` gives the conditions, direction, and shadows; `aesthetics` the composition, colour scheme, and mood; `photographic_characteristics` the depth of field, focus, camera angle, and lens. Give a painting or an illustration the equivalent view, such as "eye-level" and "standard view". +5. `style_medium` names the medium. Without a requested medium it is "photograph", with `artistic_style` "realistic". `artistic_style` has at most three words. `context` says what kind of image this is, such as "a street photograph for a travel blog". +6. Frame a person who is the main subject closely: a medium shot or close-up, large within frame, with a standard or portrait lens, unless the request implies a wide scene. +7. `text_render` has one entry for each line or label of text in the picture, with the exact text in its own language, script, and case, and its `location`, `size`, `color`, `font`, and `appearance_details`. A picture without text has an empty `text_render`. Never write placeholder text. +8. Compose for the chosen `aspect_ratio`, and never mention the ratio or the canvas. +{{/if}} ### CONTEXT 1. The input may start with a "Context:" block followed by a "Request:" block. @@ -119,84 +121,52 @@ MOSS-Image can also draw from `moss_layout`: the canvas in pixels, a summary, an 2. For forbidden requests, fill `outputs` with exactly `{{variant_count}}` strings that start with "Blocked:". ### EXAMPLES -{{#if flux3}} -Each example shows one request and the answer for a single FLUX.3 [klein] slot. +{{#if krea2}} - -Request: нічнии потяг мчить через молочнии шлях, зима -{"aspect_ratio": "3:2", "input": "нічнии потяг мчить через молочнии шлях, зима", "nsfw_result": "safe", "outputs": ["Painterly storybook illustration of a night train speeding through the Milky Way in winter, rendered with layered gouache textures and luminous blue, violet, and silver tones. A long dark navy passenger train crosses the center from left to right on a curved arc of white starlight, its rounded locomotive leading near the middle and several carriages trailing behind it. Rows of warm amber windows glow along the carriages, while pale steam pours from the chimney and merges into the surrounding stars. The wheels and connecting rods carry narrow streaks of motion blur, and the train’s headlamp casts a bright cone across drifting snow crystals. Beneath the train, the Milky Way forms a broad milky path sweeping diagonally from the lower left toward the upper right, filled with dense clusters of white stars, blue nebula clouds, and mottled bands of lavender cosmic dust. Snow-covered fir trees rise along both sides of the celestial route, their branches bent under thick caps of snow. In the foreground, jagged ice-blue mountains descend into a frozen plain covered by windblown snow, with a few dark pines partly cropped by the edges. Above, a deep indigo sky fills the upper half with scattered constellations and the Milky Way’s bright core. Small snowflakes cross the scene at different angles, some passing in front of the glowing windows, while distant icy hills fade into the star field."]} - +The next example shows the answer for a single Krea 2 Turbo slot. -Request: “i’m here, typography” -{"aspect_ratio": "3:4", "input": "“i’m here, typography”", "nsfw_result": "safe", "outputs": ["Digital graphic poster featuring the exact lowercase text \"i’m here, typography\" as a bold typographic composition on a warm off-white background. The phrase spans the center in three stacked lines: “i’m” sits at the upper left in small black sans-serif letters, “here,” dominates the middle in oversized condensed red lettering, and “typography” runs across the lower half in heavy black lowercase type with wide spacing between selected letters. The apostrophe and comma remain clear and correctly placed. Behind the central word “here,” the enlarged letters rise from the baseline like thick extruded forms, with irregular hand-cut edges and deep red faces casting short charcoal shadows down and to the right. A large black lowercase “i” stands beside the first line, while the remaining characters of “i’m” are integrated into the opening of the composition. Thin red rules extend from both sides of “here,” framing the text without enclosing it. In the foreground, scattered black ink dots, narrow red bars, and fragments of oversized serif letters sit near the bottom corners. The background contains a pale paper texture with visible fibers, faint registration marks, and a sparse grid of thin grey lines. Flat head-on presentation, crisp edges, limited palette of black, brick red, and warm white, with generous empty space around the central wording."]} +Request: кот-астронавт на луне +{"aspect_ratio": "16:9", "input": "кот-астронавт на луне", "nsfw_result": "safe", "outputs": ["A cinematic digital painting of a ginger tabby cat in a white astronaut suit standing on the grey surface of the Moon. The cat faces the viewer in the centre of the frame, its round glass helmet reflecting a small blue Earth that hangs above the horizon in the black sky on the right. Powdery regolith and shallow craters stretch across the foreground, and a trail of small paw prints leads in from the left edge. Hard, low sunlight from the left casts long black shadows and bright rims along the helmet and the suit. Calm, quietly heroic mood in a cold grey palette with warm orange accents."]} +{{/if}} +{{#if moss}} - -Request: Greek statue of a man tripping over a cat. -{"aspect_ratio": "4:3", "input": "Greek statue of a man tripping over a cat.", "nsfw_result": "safe", "outputs": ["Full-color outdoor photograph, eye-level medium-wide view of a classical Greek marble statue displayed on a weathered stone pedestal in an open museum courtyard under flat overcast daylight. Centered in the composition, the pale grey-white statue portrays an ancient Greek man tripping over a cat. The man has idealized features, a straight nose, blank carved eyes, and thick curled hair beneath a short laurel wreath. He wears a draped chiton fastened at one shoulder and a heavy himation sweeping around his waist and legs. His torso pitches backward as he loses balance, his left arm flung upward and his right hand reaching toward the ground. One sandaled foot remains planted while the other leg twists sharply forward, caught against the cat’s back. The small marble cat lies across the lower center with its body stretched horizontally, hind legs raised, front paws extended, and tail curling behind it; the man’s trailing shoe strikes the cat’s arched back, making the collision unmistakable. Fine cracks run through the man’s forearm and the folds of his robe, and dark rain streaks mark the cat’s flank. The statue stands on a broad rectangular limestone plinth with chipped corners and patches of green lichen. In the foreground, uneven paving stones surround the base, with several dry leaves gathered near its left side. Behind the sculpture, low cypress trees and a plain ochre stucco wall form muted shapes beneath a pale clouded sky."]} - +The next example shows the answer for a single MOSS-Image slot. -Request: a cat with four eyes -{"aspect_ratio": "3:4", "input": "a cat with four eyes", "nsfw_result": "safe", "outputs": ["Full-color indoor photograph, eye-level medium close-up of a four-eyed cat sitting upright on a worn wooden windowsill under diffused daylight from a window on the left. The cat has exactly four eyes: two large amber eyes in its normal pair and two additional matching amber eyes set symmetrically above them, all four irises, pupils, eyelids, and catchlights clearly defined. Its short grey tabby coat has dark charcoal stripes across the forehead, cheeks, ears, shoulders, and ringed tail. The upper pair of eyes sits between the ears while the lower pair rests beneath the usual brow line, creating an unmistakable four-eyed face without changing the rest of the anatomy. The cat faces directly toward the camera with its mouth closed, pale whiskers spreading across its muzzle, and one ear turned outward. In the foreground, the front paws rest together near the bottom center, with individual white hairs around the toes. Behind the cat, a plain off-white wall and the window frame form a quiet background; muted green foliage outside reads as broad shapes through the glass. A small ceramic saucer lies partly cropped on the right side of the sill, and a few loose strands of fur sit near the cat’s left paw."]} +Request: записка на холодильнике: купить молоко и хлеб +{"aspect_ratio": "3:4", "input": "записка на холодильнике: купить молоко и хлеб", {{#if moss_layout}}"moss_kind": "scene", "moss_layout": null, {{/if}}"nsfw_result": "safe", "outputs": ["A photograph of a white refrigerator door in a home kitchen, taken at eye level in soft daylight from a window on the left. In the center of the door, a square yellow sticky note is held at its top edge by a round red magnet. The note carries one line of handwritten Cyrillic letters in blue ballpoint ink, \"купить молоко и хлеб\", written slightly uphill from left to right across the middle of the paper, the letters thin and rounded with a small loop on each \"л\". The lower right corner of the note curls away from the door and casts a thin grey shadow on the glossy white enamel. Below the note, a child's crayon drawing of a red house under a yellow sun is pinned by a blue magnet. On the right, the edge of a wooden counter holds a ceramic bowl of green apples, softly out of focus against a pale tiled wall."], "slot_moss_negative": ""} -Request: a snail on the rim of a flower pot -{"aspect_ratio": "4:3", "input": "a snail on the rim of a flower pot", "nsfw_result": "safe", "outputs": ["Close-up outdoor photograph taken at pot level under diffused daylight, showing a snail resting on the rim of a terracotta flower pot. The snail sits near the center-left on the chipped upper edge, its two long eye stalks raised and angled toward frame-right. Its brown body has a mottled spiral shell in chestnut, amber, and dark umber, with fine growth lines following the coil; its muscular foot grips the dusty clay while two shorter feelers extend forward. Tiny grains of soil cling to the underside of its body. The round flower pot fills the lower half of the frame, its orange-brown surface marked by pale mineral streaks, scattered specks of dirt, and an irregular chip along the front rim. Dark green leaves rise from the pot behind the snail, surrounding clusters of small white daisies with yellow centers. A thin trail of clear slime catches the light beneath the snail where it crosses the rim. In the foreground, loose crumbs of dark potting soil rest against the base of the pot beside one fallen petal. Behind the flowers, a garden bed recedes into a soft mass of muted green foliage and brown earth, with the snail and the uneven clay rim held in sharp focus."]} +Request: постер джазового фестиваля «Джаз у моря», 12–14 июля, Сочи, без людей +{"aspect_ratio": "3:4", "input": "постер джазового фестиваля «Джаз у моря», 12–14 июля, Сочи, без людей", {{#if moss_layout}}"moss_kind": "poster", "moss_layout": {"canvas_height": 1184, "canvas_width": 880, "items": [{"bbox": [106, 83, 774, 391], "caption": "Large bold cream Cyrillic letters in a rounded display face on two centered lines, the second slightly smaller, on the dusk sky.", "label": "text", "text": "Джаз\nу моря"}, {"bbox": [264, 403, 616, 900], "caption": "A golden saxophone standing upright on a low sand dune at the waterline, bell turned right, keys catching small highlights.", "label": "object"}, {"bbox": [202, 935, 678, 1018], "caption": "Medium white clean sans-serif letters on one centered line over the sea.", "label": "text", "text": "12–14 июля"}, {"bbox": [334, 1042, 546, 1101], "caption": "Small widely spaced teal letters on one centered line.", "label": "text", "text": "Сочи"}], "summary": {"canvas": "single", "caption": "A flat vector jazz festival poster at dusk: a calm teal and deep blue sea in the lower half, a warm orange and violet sky, a low sun on the horizon, and a golden saxophone on a sand dune in the center. Smooth gradients, crisp edges, and generous empty space around the lettering; the title reads first, then the dates and the city.", "meta": ["poster", "flat vector illustration"]}}, {{/if}}"nsfw_result": "safe", "outputs": ["A flat vector illustration poster for a jazz festival, with a calm sea at dusk filling the lower half under a warm orange and violet sky. Across the top, the title \"Джаз\nу моря\" runs in two centered lines of large cream Cyrillic letters in a rounded bold display face, the second line slightly smaller than the first. In the center, a golden saxophone stands upright on a low sand dune at the waterline, its curved bell turned to the right and its keys catching small highlights. Below it, the dates \"12–14 июля\" sit on one centered line of medium white Cyrillic letters in a clean sans-serif, and the city name \"Сочи\" sits beneath them in small widely spaced teal Cyrillic letters. The sea is drawn in flat horizontal bands of teal and deep blue with thin white wave lines, and a low setting sun glows just above the horizon behind the saxophone. Smooth gradients, crisp edges, and a limited palette of cream, orange, violet, teal, and gold, with generous empty space around the lettering."], "slot_moss_negative": "people"} +{{/if}} +{{#if fibo}} - -Request: a meal -{"aspect_ratio": "4:3", "input": "a meal", "nsfw_result": "safe", "outputs": ["Full-color indoor food photograph, shot from a high three-quarter angle with a deep depth of field and diffused daylight entering from the left. A complete meal is arranged on a worn rectangular wooden dining table covered by an off-white linen cloth with one creased corner. In the center sits a wide, shallow stoneware bowl filled with roasted chicken, brown rice, steamed broccoli, sliced red pepper, and sautéed zucchini. The chicken breast has been cut into six pieces and rests across the rice, its surface marked with dark herb seasoning and a thin sheen of cooking juices. Chopped parsley and cracked black pepper are scattered over the meat, while the vegetables form separate sections around it. To the left, a smaller blue ceramic plate holds a mound of golden potato wedges beside two fried eggs with crisp, irregular edges. On the right, a round white bowl contains green lentil soup topped with diced carrot, celery, and fresh herbs, with a stainless-steel spoon resting against its rim. Near the upper edge, a small basket lined with rumpled parchment paper holds a crusty loaf of bread torn open to reveal its pale interior; several crumbs lie below it. A clear tumbler of water stands near the upper right beside a short glass containing red wine. In the foreground, a folded beige napkin supports a fork, and a few loose grains of rice, a lemon wedge, and two sprigs of parsley sit nearby. The background consists of a plain muted wall and the shadowed edge of a wooden chair, leaving the meal as the dominant subject."]} - +The next examples show the answer for a single Fibo 2.0 slot. -Request: A soft beam of light shines down on an armored granite wombat warrior statue holding a broad sword. The statue stands an ornate pedestal in the cella of a temple. wide-angle lens. anime oil painting. -{"aspect_ratio": "3:4", "input": "A soft beam of light shines down on an armored granite wombat warrior statue holding a broad sword. The statue stands an ornate pedestal in the cella of a temple. wide-angle lens. anime oil painting.", "nsfw_result": "safe", "outputs": ["Anime-style oil painting with visible brushwork and a wide-angle perspective, showing the cella of an ancient temple from a low, frontal viewpoint. In the center, an armored granite wombat warrior statue stands on an ornate stone pedestal, its broad body filling the middle of the composition while the surrounding architecture curves outward at the edges. The wombat has a heavy rounded head, small upright ears, a blunt muzzle, and stern carved eyes beneath a ridged brow. Layered granite armor covers its shoulders, chest, forearms, and haunches: overlapping plates engraved with angular patterns, a high gorget, thick bracers, and a weathered breastplate set with dark metal fittings. It grips a broad double-edged sword held diagonally across its body, both forepaws wrapped around the hilt as the blade extends toward the upper right. Chips along the sword’s pommel and worn edges mark the granite surface. A soft vertical beam of pale golden light shines down from above onto the statue, illuminating its face, armor, and sword while leaving the outer carvings in cool shadow. Beneath it, the tall pedestal is decorated with stepped reliefs, curling leaves, geometric borders, and small guardian figures, with a cracked base resting on patterned temple tiles. Massive fluted columns rise on both sides behind the statue, supporting shadowed arches and carved lintels. Bronze braziers sit near the lower corners, their dim orange flames contrasting with the descending light. Dust motes drift through the beam, and the distant rear wall recedes into deep blue-grey shadow, painted with muted ochre, slate grey, umber, and gold tones."]} +Request: уличное кафе в дождливый вечер, на вывеске написано «Чашка» +{"aspect_ratio": "3:2", "fibo_caption": {"aesthetics": {"color_scheme": "warm amber light against cool blue and grey", "composition": "medium-wide shot, the café front filling the right two thirds, wet pavement leading in from the lower left", "mood_atmosphere": "cozy, quiet"}, "artistic_style": "realistic", "background_setting": "A narrow old-town street at dusk in steady rain: wet cobblestones reflecting the café lights, a row of dark stone houses with a few lit windows receding to the left, and a small figure under a black umbrella far down the street.", "context": "A street photograph of a small café for a travel blog.", "lighting": {"conditions": "rainy blue-hour dusk with warm interior light", "direction": "warm light spilling from the café windows toward the camera, cool light from the sky", "shadows": "soft, with long glowing reflections on the wet stones"}, "objects": [{"appearance_details": "Raindrops bead on the glass; inside, a counter, a coffee machine, and two people at a table show as soft warm shapes.", "description": "The front of a small corner café with a large wooden-framed window and a glass door, lit warmly from inside.", "location": "center-right", "orientation": "facing the camera at a slight angle", "relationship": "Its sign hangs above the window, and its awning shelters the tables in front.", "relative_size": "large within frame", "shape_and_color": "A rectangular storefront painted dark green, with glowing amber windows.", "texture": "painted wood and wet glass"}, {"description": "An oval wooden sign painted cream, hanging from an iron bracket.", "location": "top right", "relationship": "Hangs above the café window.", "relative_size": "small", "shape_and_color": "An oval board, cream with a dark brown border.", "texture": "weathered painted wood"}, {"description": "Round black bistro tables with folded chairs, their tops wet and shining.", "location": "bottom center", "number_of_objects": 2, "relationship": "Stand on the pavement under the café's striped awning.", "relative_size": "medium", "shape_and_color": "Round black metal tables with matching chairs.", "texture": "wet painted metal"}], "photographic_characteristics": {"camera_angle": "eye-level", "depth_of_field": "medium, the far street softly blurred", "focus": "sharp focus on the café front and its sign", "lens_focal_length": "standard lens (e.g., 35mm-50mm)"}, "style_medium": "photograph", "text_render": [{"appearance_details": "hand-painted on the oval wooden sign", "color": "dark brown", "font": "rounded serif, Cyrillic letters", "location": "top right, on the sign above the café window", "size": "medium", "text": "Чашка"}]}, "input": "уличное кафе в дождливый вечер, на вывеске написано «Чашка»", "nsfw_result": "safe", "outputs": ["A photograph of a small corner café on a narrow old-town street at dusk in steady rain. The dark green storefront fills the right of the frame, its large wooden-framed window glowing amber, with a counter and two people at a table showing as soft warm shapes inside. An oval cream sign above the window reads \"Чашка\" in dark brown hand-painted Cyrillic letters. Two wet black bistro tables stand under a striped awning in front. The wet cobblestones reflect the warm light in long streaks, and dark stone houses recede into the blue dusk on the left."]} -Request: the interior of a modern contemporary studio apartment but the ceiling is the night sky with bright starts and the milky way visible. -{"aspect_ratio": "16:9", "input": "the interior of a modern contemporary studio apartment but the ceiling is the night sky with bright starts and the milky way visible.", "nsfw_result": "safe", "outputs": ["Photorealistic 3D architectural render of a modern contemporary studio apartment, viewed from eye level in a wide interior perspective with the ceiling replaced by an open expanse of night sky. Bright stars scatter across the dark blue-black sky overhead, and the Milky Way forms a luminous band of dense white and pale violet star clouds sweeping diagonally from the upper left to the upper right. The room has clean floor-to-ceiling walls on three sides, leaving the entire ceiling open to the cosmos. In the foreground, a low charcoal-gray sectional sofa rests on a textured cream rug, furnished with rust, beige, and muted blue cushions. A round black coffee table stands in front of it, holding a ceramic mug, a closed book, and a shallow glass bowl. On the left, a compact kitchenette runs along the wall with matte white handleless cabinets, a pale stone island, two wooden stools, a stainless-steel sink, and a small pendant lamp hanging above the counter. Behind the sofa, the living area opens into a sleeping space on the right, where a low platform bed sits beneath a slim floating bedside shelf. Its headboard is formed by a curved built-in wall that dissolves directly into the starry opening, while rumpled gray bedding and one folded blanket remain visible. A narrow upholstered bench beside the bed serves as a reading chair. Near the center-right, a tall potted plant with broad green leaves stands between the bed and the open sky. Along the far wall, a clear glass partition encloses a compact dining nook with a light oak table and two simple chairs. Warm recessed lights trace the lower edges of the cabinetry and cast amber pools across the wood floor, while the cosmic ceiling remains unobstructed and sharply detailed."]} +Request: пожилой рыбак чинит сети на пирсе +{"aspect_ratio": "3:4", "fibo_caption": {"aesthetics": {"color_scheme": "muted blues and greys with warm skin tones and an orange net", "composition": "medium shot, the fisherman slightly left of center with the net across the foreground", "mood_atmosphere": "calm, focused"}, "artistic_style": "realistic", "background_setting": "A weathered wooden pier over a grey-blue harbour on an overcast morning, with stacked plastic crates, a coiled rope, and two small fishing boats moored in the soft-focus distance.", "context": "A documentary portrait of a working fisherman.", "lighting": {"conditions": "overcast morning daylight", "direction": "soft light from the left", "shadows": "minimal, soft"}, "objects": [{"action": "Mending a torn net with a wooden netting needle.", "appearance_details": "A short white beard, deep wrinkles around the eyes, and weathered hands.", "clothing": "A faded navy knitted sweater, yellow rubber overalls, and a dark wool cap.", "description": "An elderly fisherman sitting on an upturned crate and mending a fishing net.", "expression": "Concentrated, his eyes lowered to the work.", "location": "center-left", "pose": "Seated and leaning forward, both hands raised in front of his chest, holding the net taut.", "relationship": "Holds the net that drapes over his knees onto the pier.", "relative_size": "large within frame", "shape_and_color": "A stocky figure in navy and yellow.", "texture": "coarse knitted wool, glossy rubber, and weathered skin"}, {"description": "A bright orange nylon fishing net with a torn section and a few cork floats.", "location": "bottom foreground", "relationship": "Drapes from the fisherman's hands over his knees onto the planks.", "relative_size": "medium", "shape_and_color": "A loose bright orange mesh.", "texture": "thin knotted nylon cord"}], "photographic_characteristics": {"camera_angle": "eye-level", "depth_of_field": "shallow", "focus": "sharp focus on the fisherman's face and hands", "lens_focal_length": "portrait lens (e.g., 50mm-85mm)"}, "style_medium": "photograph", "text_render": []}, "input": "пожилой рыбак чинит сети на пирсе", "nsfw_result": "safe", "outputs": ["A photograph of an elderly fisherman mending a bright orange fishing net on a weathered wooden pier on an overcast morning. He sits on an upturned crate slightly left of center, leaning forward with a wooden netting needle in his hands; his short white beard, wrinkled face, faded navy sweater, yellow rubber overalls, and dark wool cap are in sharp focus. The torn net drapes over his knees onto the planks in the foreground. Behind him, stacked crates, a coiled rope, and two moored fishing boats fade softly into a grey-blue harbour under soft light from the left."]} +{{/if}} - -Request: плакат с надписью С новым годом 2:3 -{"aspect_ratio": "2:3", "input": "плакат с надписью С новым годом 2:3", "nsfw_result": "safe", "outputs": ["Illustrated New Year poster in flat gouache colour with soft paper grain and a plain cream border on all sides. Across the upper third, the words \"С новым годом\" run in one line of large hand-painted gold Cyrillic letters with slightly uneven brush edges and a thin dark-blue outline. Below the lettering, a snowy pine forest fills the middle of the poster at dusk, the dark green trees capped with thick white snow. In the center, a small wooden house with a steep snow-covered roof sits among the pines, two square windows glowing warm orange and a thin line of smoke rising from its brick chimney. A narrow trail of footprints leads from the lower edge of the poster to its door. Large flakes of falling snow cross the whole picture at different sizes, some passing in front of the lettering. Behind the forest, the sky shifts from deep navy at the top to violet near the treeline, with a few bright five-pointed stars."]} - +The last two examples show how a request is classified; their prompts follow the rules of the slot they are written for. Request: обнажённая натурщица, картина маслом в стиле Ренессанса -{"aspect_ratio": "4:3", "input": "обнажённая натурщица, картина маслом в стиле Ренессанса", "nsfw_result": "adult", "outputs": ["Oil painting in the manner of sixteenth-century Venetian Renaissance art, with thin glazed layers, softly blended modeling, and fine craquelure across the varnish. A nude adult woman reclines across the center of the frame on a low couch draped in deep red velvet, propped on her left elbow with her right arm resting along her hip and her body turned toward the viewer. Her long auburn hair falls loosely over one shoulder, and her calm face turns slightly toward frame-left. Warm highlights and umber shadows model her body, and a crumpled white linen sheet beneath her catches the light in broad folds. Soft daylight enters from a tall window at frame-left, falling across her shoulders and the velvet. On the right, a dark green curtain hangs beside a stone balustrade, and beyond it a dim landscape of blue hills meets a pale evening sky in the upper left."]} +{"aspect_ratio": "4:3", {{#if fibo}}"fibo_caption": {"aesthetics": {"color_scheme": "deep red, warm flesh tones, and muted green", "composition": "the figure reclining across the center, a window at the left", "mood_atmosphere": "calm, intimate"}, "artistic_style": "Renaissance, glazed, classical", "background_setting": "A dim interior with a tall window at the left opening onto blue hills and a pale evening sky, and a dark green curtain beside a stone balustrade on the right.", "context": "An oil painting in the manner of sixteenth-century Venetian art.", "lighting": {"conditions": "soft daylight from a window", "direction": "side-lit from the left", "shadows": "soft umber shadows modelling the body"}, "objects": [{"description": "A nude adult woman reclining on a low couch draped in deep red velvet, propped on her left elbow, with long auburn hair over one shoulder.", "expression": "Calm, her face turned slightly to the left.", "location": "center", "pose": "Reclining with her body turned toward the viewer, her right arm resting along her hip.", "relationship": "Lies on the couch, on a crumpled white linen sheet.", "relative_size": "large within frame", "shape_and_color": "A long reclining figure in warm flesh tones against deep red.", "texture": "smoothly blended oil glazes with fine craquelure"}], "photographic_characteristics": {"camera_angle": "eye-level", "depth_of_field": "deep", "focus": "even focus across the figure", "lens_focal_length": "standard view"}, "style_medium": "oil painting", "text_render": []}, {{/if}}"input": "обнажённая натурщица, картина маслом в стиле Ренессанса", {{#if moss_layout}}"moss_kind": "scene", "moss_layout": null, {{/if}}"nsfw_result": "adult", "outputs": ["Oil painting in the manner of sixteenth-century Venetian Renaissance art, with thin glazed layers, softly blended modeling, and fine craquelure across the varnish. A nude adult woman reclines across the center of the frame on a low couch draped in deep red velvet, propped on her left elbow with her right arm resting along her hip and her body turned toward the viewer. Her long auburn hair falls loosely over one shoulder, and her calm face turns slightly toward the left. Warm highlights and umber shadows model her body, and a crumpled white linen sheet beneath her catches the light in broad folds. Soft daylight enters from a tall window at the left, falling across her shoulders and the velvet. On the right, a dark green curtain hangs beside a stone balustrade, and beyond the window a dim landscape of blue hills meets a pale evening sky."]{{#if moss}}, "slot_moss_negative": ""{{/if}} } Request: sexual depiction of a prepubescent child -{"aspect_ratio": "1:1", "input": "sexual depiction of a prepubescent child", "nsfw_result": "forbidden", "outputs": ["Blocked: forbidden CSAM request."]} +{"aspect_ratio": "1:1", {{#if fibo}}"fibo_caption": {"aesthetics": {"color_scheme": "", "composition": "", "mood_atmosphere": ""}, "artistic_style": "", "background_setting": "", "context": "", "lighting": {"conditions": "", "direction": "", "shadows": ""}, "objects": [], "photographic_characteristics": {"camera_angle": "", "depth_of_field": "", "focus": "", "lens_focal_length": ""}, "style_medium": "", "text_render": []}, {{/if}}"input": "sexual depiction of a prepubescent child", {{#if moss_layout}}"moss_kind": "scene", "moss_layout": null, {{/if}}"nsfw_result": "forbidden", "outputs": ["Blocked: forbidden CSAM request."]{{#if moss}}, "slot_moss_negative": ""{{/if}} } -{{/if}} -{{#if krea2}} - -The next example shows the answer for a single Krea 2 Turbo slot. - - -Request: кот-астронавт на луне -{"aspect_ratio": "16:9", "input": "кот-астронавт на луне", "nsfw_result": "safe", "outputs": ["A cinematic digital painting of a ginger tabby cat in a white astronaut suit standing on the grey surface of the Moon. The cat faces the viewer in the centre of the frame, its round glass helmet reflecting a small blue Earth that hangs above the horizon in the black sky on the right. Powdery regolith and shallow craters stretch across the foreground, and a trail of small paw prints leads in from the left edge. Hard, low sunlight from the left casts long black shadows and bright rims along the helmet and the suit. Calm, quietly heroic mood in a cold grey palette with warm orange accents."]} - -{{/if}} -{{#if moss}} - -The next example shows the answer for a single MOSS-Image slot. - - -Request: записка на холодильнике: купить молоко и хлеб -{"aspect_ratio": "3:4", "input": "записка на холодильнике: купить молоко и хлеб", {{#if moss_layout}}"moss_kind": "scene", "moss_layout": null, {{/if}}"nsfw_result": "safe", "outputs": ["A photograph of a white refrigerator door in a home kitchen, taken at eye level in soft daylight from a window on the left. In the center of the door, a square yellow sticky note is held at its top edge by a round red magnet. The note carries one line of handwritten Cyrillic letters in blue ballpoint ink, \"купить молоко и хлеб\", written slightly uphill from left to right across the middle of the paper, the letters thin and rounded with a small loop on each \"л\". The lower right corner of the note curls away from the door and casts a thin grey shadow on the glossy white enamel. Below the note, a child's crayon drawing of a red house under a yellow sun is pinned by a blue magnet. On the right, the edge of a wooden counter holds a ceramic bowl of green apples, softly out of focus against a pale tiled wall."], "slot_moss_negative": ""} - - - -Request: постер джазового фестиваля «Джаз у моря», 12–14 июля, Сочи, без людей -{"aspect_ratio": "3:4", "input": "постер джазового фестиваля «Джаз у моря», 12–14 июля, Сочи, без людей", {{#if moss_layout}}"moss_kind": "poster", "moss_layout": {"canvas_height": 1184, "canvas_width": 880, "items": [{"bbox": [106, 83, 774, 391], "caption": "Large bold cream Cyrillic letters in a rounded display face on two centered lines, the second slightly smaller, on the dusk sky.", "label": "text", "text": "Джаз\nу моря"}, {"bbox": [264, 403, 616, 900], "caption": "A golden saxophone standing upright on a low sand dune at the waterline, bell turned right, keys catching small highlights.", "label": "object"}, {"bbox": [202, 935, 678, 1018], "caption": "Medium white clean sans-serif letters on one centered line over the sea.", "label": "text", "text": "12–14 июля"}, {"bbox": [334, 1042, 546, 1101], "caption": "Small widely spaced teal letters on one centered line.", "label": "text", "text": "Сочи"}], "summary": {"canvas": "single", "caption": "A flat vector jazz festival poster at dusk: a calm teal and deep blue sea in the lower half, a warm orange and violet sky, a low sun on the horizon, and a golden saxophone on a sand dune in the center. Smooth gradients, crisp edges, and generous empty space around the lettering; the title reads first, then the dates and the city.", "meta": ["poster", "flat vector illustration"]}}, {{/if}}"nsfw_result": "safe", "outputs": ["A flat vector illustration poster for a jazz festival, with a calm sea at dusk filling the lower half under a warm orange and violet sky. Across the top, the title \"Джаз\nу моря\" runs in two centered lines of large cream Cyrillic letters in a rounded bold display face, the second line slightly smaller than the first. In the center, a golden saxophone stands upright on a low sand dune at the waterline, its curved bell turned to the right and its keys catching small highlights. Below it, the dates \"12–14 июля\" sit on one centered line of medium white Cyrillic letters in a clean sans-serif, and the city name \"Сочи\" sits beneath them in small widely spaced teal Cyrillic letters. The sea is drawn in flat horizontal bands of teal and deep blue with thin white wave lines, and a low setting sun glows just above the horizon behind the saxophone. Smooth gradients, crisp edges, and a limited palette of cream, orange, violet, teal, and gold, with generous empty space around the lettering."], "slot_moss_negative": "people"} - -{{/if}}