diff --git a/crates/openplotva-app/src/dialog_messages.rs b/crates/openplotva-app/src/dialog_messages.rs index 286030ec..2f7ba4ba 100644 --- a/crates/openplotva-app/src/dialog_messages.rs +++ b/crates/openplotva-app/src/dialog_messages.rs @@ -4597,6 +4597,7 @@ mod tests { negative_prompt: String::new(), aspect_ratio: String::new(), seed: String::new(), + ..crate::image_jobs::ImageGenerationRequest::default() }] ); // Album delivery: the classic placeholder photo goes out first, then the diff --git a/crates/openplotva-app/src/image_jobs.rs b/crates/openplotva-app/src/image_jobs.rs index d835f412..5066c497 100644 --- a/crates/openplotva-app/src/image_jobs.rs +++ b/crates/openplotva-app/src/image_jobs.rs @@ -157,11 +157,17 @@ 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. + pub prompt_layouts: Vec, pub is_nsfw: bool, - /// Raw negative prompt. No current image backend takes one (they run without - /// classifier-free guidance), so it is never sent to the image model; the + /// Raw negative prompt. It is never sent to an image model as typed; the /// optimizer sees it as exclusions to express positively. pub negative_prompt: String, + /// Negative prompt for MOSS-Image, the one backend that runs classifier-free + /// guidance: the optimizer's request-specific exclusions, or the `|` text of a + /// verbatim `!draw` prompt. + pub moss_negative_prompt: String, /// Raw aspect ratio. pub aspect_ratio: String, /// Raw seed. @@ -783,11 +789,24 @@ 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 { + negative_prompt: &request.moss_negative_prompt, + layout: draw_api_moss_layout(&request), + } + } else { + DrawApiMossInputs::default() + }; let payload = draw_api_payload( &prompt, &[], draw_api_dimensions(&request.aspect_ratio), aspect_ratio.as_deref(), + moss, ) .map_err(|err| ImageGenerationError::Provider(err.to_string()))?; let job_id = self @@ -836,8 +855,14 @@ where "image edit requires image input".to_owned(), )); } - let payload = draw_api_payload(&prompt, &image_inputs, None, None) - .map_err(|err| ImageEditError::Provider(err.to_string()))?; + let payload = draw_api_payload( + &prompt, + &image_inputs, + None, + None, + DrawApiMossInputs::default(), + ) + .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 @@ -1715,19 +1740,27 @@ fn image_generation_context( /// model's `prompt_target` config names the model, so switching the model in /// 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. +/// hop renders that same prompt. A MOSS-Image model whose config sets +/// `"prompt_layout": true` also gets a layout for designed pictures. fn routed_image_targets( walker: &RoutedAttemptWalker, workflow_key: &str, ) -> openplotva_media::ImageTargets { - walker - .primary_model_config(workflow_key) - .as_ref() - .and_then(prompt_target_model) - .map_or( - openplotva_media::ImageTargets::FLUX3, - openplotva_media::ImageTargets::single, - ) + let config = walker.primary_model_config(workflow_key); + let Some(config) = config.as_ref() else { + return openplotva_media::ImageTargets::FLUX3; + }; + prompt_target_model(config).map_or(openplotva_media::ImageTargets::FLUX3, |model| { + openplotva_media::ImageTargets::single(model) + .with_moss_layout(prompt_layout_enabled(config)) + }) +} + +fn prompt_layout_enabled(config: &serde_json::Value) -> bool { + config + .get("prompt_layout") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false) } fn prompt_target_model(config: &serde_json::Value) -> Option { @@ -2072,7 +2105,15 @@ where tracing::debug!(%error, "image prompt classification failed; treating as adult"); } apply_image_nsfw_result(classified.value.nsfw_result, &mut request)?; + request.moss_negative_prompt = request + .prompt_variants + .iter() + .find_map(|variant| { + non_empty(openplotva_media::part_image_prompt(variant).negative_prompt) + }) + .unwrap_or_else(|| request.negative_prompt.trim().to_owned()); request.prompt_variants = preset; + request.prompt_layouts = Vec::new(); return Ok(request); } let request_text = build_draw_prompt_text( @@ -2101,11 +2142,27 @@ where } let optimized = openplotva_media::apply_word_replacements(optimized.value); apply_image_nsfw_result(optimized.nsfw_result, &mut request)?; - request.prompt_variants = optimized + let moss_layout = optimized + .moss_layout + .as_ref() + .map(serde_json::Value::to_string) + .unwrap_or_default(); + (request.prompt_variants, request.prompt_layouts) = optimized .outputs .into_iter() - .filter_map(non_empty) - .collect(); + .enumerate() + .filter_map(|(slot, output)| { + let layout = + if options.targets.model_for_slot(slot) == openplotva_media::ImageModel::Moss { + moss_layout.clone() + } else { + String::new() + }; + non_empty(output).map(|output| (output, layout)) + }) + .unzip(); + clear_empty_layouts(&mut request.prompt_layouts); + request.moss_negative_prompt = optimized.moss_negative; if request.aspect_ratio.trim().is_empty() { request.aspect_ratio = optimized.aspect_ratio.trim().to_owned(); } @@ -2208,16 +2265,33 @@ fn image_generation_request_for_prompt_slots( count: usize, ) -> ImageGenerationRequest { let mut request = request.clone(); - request.prompt_variants = request + let layouts = request + .prompt_layouts + .iter() + .map(String::as_str) + .chain(std::iter::repeat("")); + (request.prompt_variants, request.prompt_layouts) = request .prompt_variants .iter() + .zip(layouts) .skip(start) .take(count.max(1)) - .filter_map(|variant| non_empty(variant.clone())) - .collect(); + .filter_map(|(variant, layout)| { + non_empty(variant.clone()).map(|variant| (variant, layout.to_owned())) + }) + .unzip(); + clear_empty_layouts(&mut request.prompt_layouts); request } +/// A request carries layouts only while some slot has one, so requests without a +/// designed MOSS-Image slot stay as they were. +fn clear_empty_layouts(layouts: &mut Vec) { + if layouts.iter().all(String::is_empty) { + layouts.clear(); + } +} + fn append_image_generation_result( combined: &mut ImageGenerationResult, result: ImageGenerationResult, @@ -3309,6 +3383,7 @@ where negative_prompt: params.negative_prompt, aspect_ratio: params.aspect_ratio, seed: params.seed, + ..ImageGenerationRequest::default() }; // Fill the album progressively: each streamed provider image lands in the @@ -4816,6 +4891,19 @@ 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 { + let slot = request + .prompt_variants + .iter() + .position(|value| !value.trim().is_empty())?; + let layout = request.prompt_layouts.get(slot)?.trim(); + if layout.is_empty() { + return None; + } + serde_json::from_str(layout).ok() +} + pub fn decode_draw_api_result_payload(payload: &[u8]) -> Result { let raw = serde_json::from_slice::(payload) .map_err(|err| format!("failed to decode response: {err}"))?; @@ -4871,6 +4959,18 @@ struct DrawApiGenerateRequest<'a> { image_url: Option>, #[serde(skip_serializing_if = "Option::is_none")] image_b64: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + negative_prompt: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + layout: 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. +#[derive(Debug, Default)] +struct DrawApiMossInputs<'a> { + negative_prompt: &'a str, + layout: Option, } #[derive(Serialize)] @@ -4927,6 +5027,7 @@ fn draw_api_payload( image_inputs: &[String], dimensions: Option<(i32, i32)>, aspect_ratio: Option<&str>, + moss: DrawApiMossInputs<'_>, ) -> serde_json::Result> { let (image_urls, image_b64) = split_draw_api_image_inputs(image_inputs); let image_url = one_or_many_strings(&image_urls); @@ -4942,6 +5043,8 @@ 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, }) } @@ -5427,6 +5530,7 @@ mod tests { negative_prompt: "blur".to_owned(), aspect_ratio: "16:9".to_owned(), seed: "42".to_owned(), + ..ImageGenerationRequest::default() }] ); let calls = effects.calls(); @@ -6004,6 +6108,7 @@ mod tests { outputs: vec!["plotva swims near a roach".to_owned()], aspect_ratio: "16:9".to_owned(), nsfw_result: openplotva_media::NsfwResult::Safe, + ..openplotva_media::ImageOptimize::default() }); let optimizing = OptimizingImageGenerator::new( generator.clone(), @@ -6057,6 +6162,7 @@ mod tests { outputs: vec!["a rewritten cat".to_owned()], aspect_ratio: "1:1".to_owned(), nsfw_result: openplotva_media::NsfwResult::Safe, + ..openplotva_media::ImageOptimize::default() }); let optimizing = OptimizingImageGenerator::new( generator.clone(), @@ -6149,6 +6255,7 @@ mod tests { outputs: vec!["a ginger cat on a windowsill".to_owned()], aspect_ratio: "1:1".to_owned(), nsfw_result: openplotva_media::NsfwResult::Safe, + ..openplotva_media::ImageOptimize::default() }); let optimizing = OptimizingImageGenerator::new( generator.clone(), @@ -6188,6 +6295,7 @@ mod tests { outputs: vec!["cat portrait variant".to_owned()], aspect_ratio: String::new(), nsfw_result: openplotva_media::NsfwResult::Safe, + ..openplotva_media::ImageOptimize::default() }); let optimizing = OptimizingImageGenerator::new( generator.clone(), @@ -6222,6 +6330,7 @@ mod tests { outputs: vec!["cat variant".to_owned()], aspect_ratio: "2:3".to_owned(), nsfw_result: openplotva_media::NsfwResult::Safe, + ..openplotva_media::ImageOptimize::default() }); let optimizing = OptimizingImageGenerator::new( generator.clone(), @@ -6250,6 +6359,7 @@ mod tests { outputs: vec!["cat variant".to_owned()], aspect_ratio: "2:3".to_owned(), nsfw_result: openplotva_media::NsfwResult::Safe, + ..openplotva_media::ImageOptimize::default() }); let optimizing = OptimizingImageGenerator::new( generator.clone(), @@ -6280,6 +6390,7 @@ mod tests { ], aspect_ratio: "16:9".to_owned(), nsfw_result: openplotva_media::NsfwResult::Safe, + ..openplotva_media::ImageOptimize::default() }); let optimizing = OptimizingImageGenerator::new( generator.clone(), @@ -6343,6 +6454,110 @@ mod tests { ); } + fn moss_poster_layout() -> Value { + json!({ + "items": [{"bbox": [70, 120, 330, 880], "caption": "Bold cream letters.", "label": "text", "text": "Джаз\nу моря"}], + "summary": {"caption": "A flat vector jazz festival poster."}, + }) + } + + #[tokio::test] + async fn optimizer_layout_and_negative_reach_only_the_moss_slot() { + let flux3 = GeneratorStub::success("https://img.test/flux3.png"); + let moss = GeneratorStub::success("https://img.test/moss.png") + .with_targets(openplotva_media::ImageTargets::MOSS); + 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 MOSS-Image.".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(), + }); + let optimizing = OptimizingImageGenerator::new( + SequentialImageGenerator::new(flux3.clone(), moss.clone()), + crate::media::MediaPromptOptimizerService::new(Some(optimizer)), + ); + + optimizing + .generate_image(ImageGenerationRequest { + prompt: "постер".to_owned(), + ..ImageGenerationRequest::default() + }) + .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 moss_request = &moss.requests()[0]; + assert_eq!( + moss_request.prompt_variants, + ["A flat poster for MOSS-Image."] + ); + assert_eq!( + moss_request.prompt_layouts, + [moss_poster_layout().to_string()] + ); + assert_eq!(moss_request.moss_negative_prompt, "people"); + assert_eq!( + draw_api_moss_layout(moss_request), + Some(moss_poster_layout()) + ); + assert_eq!(draw_api_moss_layout(flux3_request), None); + } + + #[test] + fn prompt_slots_keep_each_layout_with_its_variant() { + let request = ImageGenerationRequest { + prompt_variants: vec!["first".to_owned(), " ".to_owned(), "third".to_owned()], + prompt_layouts: vec![String::new(), String::new(), "{\"items\":[]}".to_owned()], + ..ImageGenerationRequest::default() + }; + let tail = image_generation_request_for_prompt_slots(&request, 1, 2); + assert_eq!(tail.prompt_variants, ["third"]); + assert_eq!(tail.prompt_layouts, ["{\"items\":[]}"]); + + // Requests built before layouts existed carry none. + let legacy = ImageGenerationRequest { + prompt_variants: vec!["first".to_owned(), "second".to_owned()], + ..ImageGenerationRequest::default() + }; + 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); + } + + #[tokio::test] + async fn verbatim_draw_sends_the_typed_negative_to_moss() { + let generator = GeneratorStub::success("https://img.test/moss.png") + .with_targets(openplotva_media::ImageTargets::MOSS); + let optimizing = OptimizingImageGenerator::new( + generator.clone(), + crate::media::MediaPromptOptimizerService::new(Some(OptimizerStub::default())), + ); + + optimizing + .generate_image(ImageGenerationRequest { + prompt: "lake".to_owned(), + prompt_variants: vec!["mountain lake | boats, people".to_owned()], + ..ImageGenerationRequest::default() + }) + .await + .expect("generation"); + + let request = &generator.requests()[0]; + assert_eq!(request.prompt_variants, ["mountain lake"]); + assert_eq!(request.moss_negative_prompt, "boats, people"); + assert!(request.prompt_layouts.is_empty()); + } + #[tokio::test] async fn boogu_generation_submits_gradio_payload_and_reads_sse_url() { let transport = BooguTransportStub::new(vec![ @@ -7806,6 +8021,7 @@ mod tests { &[], draw_api_dimensions("16:9"), draw_api_aspect_ratio("16:9").as_deref(), + DrawApiMossInputs::default(), ) .expect("generation payload"), ) @@ -7815,8 +8031,14 @@ mod tests { assert_eq!(generation["height"], 704); let edit: serde_json::Value = serde_json::from_slice( - &draw_api_payload("night", &["https://img.test/a.png".to_owned()], None, None) - .expect("edit payload"), + &draw_api_payload( + "night", + &["https://img.test/a.png".to_owned()], + None, + None, + DrawApiMossInputs::default(), + ) + .expect("edit payload"), ) .expect("edit json"); assert!(edit.get("aspect_ratio").is_none()); @@ -7912,6 +8134,25 @@ mod tests { openplotva_media::ImageTargets::KREA2 ); + let moss = image_route_walker(json!({ + "service_name": AIFARM_DRAW_API_SERVICE_NAME, + "endpoint_name": AIFARM_DRAW_API_MOSS_ENDPOINT_NAME, + "prompt_target": "moss", + })); + let targets = routed_image_targets(&moss, IMAGE_GENERATION_FLUX_WORKFLOW_KEY); + assert_eq!(targets, openplotva_media::ImageTargets::MOSS); + assert!(!targets.moss_layout()); + + let moss_layout = image_route_walker(json!({ + "service_name": AIFARM_DRAW_API_SERVICE_NAME, + "endpoint_name": AIFARM_DRAW_API_MOSS_ENDPOINT_NAME, + "prompt_target": "moss", + "prompt_layout": true, + })); + assert!( + routed_image_targets(&moss_layout, IMAGE_GENERATION_FLUX_WORKFLOW_KEY).moss_layout() + ); + let retired = image_route_walker(json!({ "endpoint_name": "qwen_image_generate", "prompt_target": "qwen_image", @@ -8134,6 +8375,85 @@ mod tests { assert_eq!(requests[1].url, "https://draw.example.test/v1/jobs/job-1"); } + async fn submitted_draw_request(endpoint_name: &str, request: ImageGenerationRequest) -> Value { + let source_image = encode_solid_png(4, 4, image::Rgba([10, 20, 30, 255])); + let draw_payload = serde_json::to_vec(&json!({ + "image_b64": [general_purpose::STANDARD.encode(source_image)], + })) + .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 + .generate_image_with_job_id(request, "draw-moss") + .await + .expect("draw result"); + let job: DiscoveryJobRequest = + serde_json::from_slice(&probe.requests()[0].body).expect("job request"); + let body = decode_discovery_body(&job.invocation.body).expect("draw body"); + serde_json::from_slice(&body).expect("draw request") + } + + #[tokio::test] + async fn only_the_moss_endpoint_receives_the_layout_and_negative_prompt() { + let request = ImageGenerationRequest { + prompt: "постер".to_owned(), + caption_text: "caption".to_owned(), + prompt_variants: vec!["A flat jazz poster.".to_owned()], + prompt_layouts: vec![moss_poster_layout().to_string()], + moss_negative_prompt: " people ".to_owned(), + aspect_ratio: "3:4".to_owned(), + ..ImageGenerationRequest::default() + }; + + let moss = + submitted_draw_request(AIFARM_DRAW_API_MOSS_ENDPOINT_NAME, request.clone()).await; + assert_eq!(moss["prompt"], "A flat jazz poster."); + assert_eq!(moss["negative_prompt"], "people"); + 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()); + + let prose_only = submitted_draw_request( + AIFARM_DRAW_API_MOSS_ENDPOINT_NAME, + ImageGenerationRequest { + prompt_layouts: Vec::new(), + moss_negative_prompt: String::new(), + ..request + }, + ) + .await; + assert!(prose_only.get("negative_prompt").is_none()); + assert!(prose_only.get("layout").is_none()); + } + fn draw_poll_test_config() -> AifarmDrawApiConfig { AifarmDrawApiConfig { base_url: "https://draw.example.test".to_owned(), @@ -9385,6 +9705,7 @@ mod tests { result: Result, requests: Arc>>, expected_image_count: usize, + targets: openplotva_media::ImageTargets, } impl GeneratorStub { @@ -9396,6 +9717,7 @@ mod tests { }), requests: Arc::new(Mutex::new(Vec::new())), expected_image_count: 1, + targets: openplotva_media::ImageTargets::FLUX3, } } @@ -9407,6 +9729,7 @@ mod tests { }), requests: Arc::new(Mutex::new(Vec::new())), expected_image_count: 1, + targets: openplotva_media::ImageTargets::FLUX3, } } @@ -9415,6 +9738,7 @@ mod tests { result: Err(ImageGenerationError::Forbidden), requests: Arc::new(Mutex::new(Vec::new())), expected_image_count: 1, + targets: openplotva_media::ImageTargets::FLUX3, } } @@ -9423,6 +9747,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, } } @@ -9431,6 +9756,11 @@ mod tests { self } + fn with_targets(mut self, targets: openplotva_media::ImageTargets) -> Self { + self.targets = targets; + self + } + fn requests(&self) -> Vec { self.requests.lock().expect("requests").clone() } @@ -9441,6 +9771,10 @@ mod tests { self.expected_image_count } + fn image_targets(&self) -> openplotva_media::ImageTargets { + self.targets + } + fn generate_image<'a>( &'a self, request: ImageGenerationRequest, diff --git a/crates/openplotva-app/src/media.rs b/crates/openplotva-app/src/media.rs index d9f3d6c9..0d24b423 100644 --- a/crates/openplotva-app/src/media.rs +++ b/crates/openplotva-app/src/media.rs @@ -470,6 +470,7 @@ where outputs: vec![fallback_prompt.to_owned()], aspect_ratio: aspect_ratio.to_owned(), nsfw_result: openplotva_media::NsfwResult::Adult, + ..ImageOptimize::default() }; let Some(optimizer) = &self.optimizer else { return MediaPromptOptimization { @@ -1225,6 +1226,7 @@ mod tests { outputs: vec!["cat".to_owned()], aspect_ratio: "1:1".to_owned(), nsfw_result: openplotva_media::NsfwResult::Adult, + ..ImageOptimize::default() } ); } @@ -1264,6 +1266,7 @@ mod tests { outputs: vec!["gemini cat".to_owned()], aspect_ratio: "1:1".to_owned(), nsfw_result: openplotva_media::NsfwResult::Safe, + ..ImageOptimize::default() })); let optimizer = FallbackMediaPromptOptimizer::new( primary, diff --git a/crates/openplotva-llm/src/aifarm.rs b/crates/openplotva-llm/src/aifarm.rs index 23aa0d46..448bfe08 100644 --- a/crates/openplotva-llm/src/aifarm.rs +++ b/crates/openplotva-llm/src/aifarm.rs @@ -2271,7 +2271,7 @@ where } None => openplotva_media::render_image_optimizer_prompt(render_options)?, }; - let tool = openplotva_media::optimize_prompt_terminator_definition(variant_count); + let tool = openplotva_media::optimize_prompt_terminator_definition(render_options); let content = self .generate_json( AifarmStructuredJsonRequest { diff --git a/crates/openplotva-llm/src/gemini.rs b/crates/openplotva-llm/src/gemini.rs index e1533da7..f8fd879f 100644 --- a/crates/openplotva-llm/src/gemini.rs +++ b/crates/openplotva-llm/src/gemini.rs @@ -1271,7 +1271,7 @@ where } None => openplotva_media::render_image_optimizer_prompt(render_options)?, }; - let tool = openplotva_media::optimize_prompt_terminator_definition(variant_count); + let tool = openplotva_media::optimize_prompt_terminator_definition(render_options); let payload = self .run_optimizer( &text, diff --git a/crates/openplotva-media/src/lib.rs b/crates/openplotva-media/src/lib.rs index 7be4f4fc..e236f138 100644 --- a/crates/openplotva-media/src/lib.rs +++ b/crates/openplotva-media/src/lib.rs @@ -40,6 +40,36 @@ pub struct ImageOptimize { pub aspect_ratio: String, /// NSFW classifier result. pub nsfw_result: NsfwResult, + /// Request-specific exclusions for the MOSS-Image negative prompt, comma-separated; + /// empty when the request rules nothing out or no slot targets MOSS-Image. + #[serde(default, rename = "slot_moss_negative")] + pub moss_negative: String, + /// Whether the request is a designed image (poster, card, menu, meme, ...) that + /// MOSS-Image renders from `moss_layout` instead of its prose prompt. + #[serde(default, rename = "slot_moss_design")] + pub moss_design: bool, + /// MOSS-Image layout of a designed image: `summary` plus `items` with `yxyx` boxes + /// on a 0-1000 grid. draw-api compiles it to the model's pixel format. + #[serde( + default, + rename = "slot_moss_layout", + deserialize_with = "deserialize_moss_layout" + )] + pub moss_layout: Option, +} + +/// Keep a layout only when it is an object with a non-empty `items` array, so a +/// malformed one degrades to the prose prompt instead of failing the whole payload. +fn deserialize_moss_layout<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Value::deserialize(deserializer)?; + let has_items = value + .get("items") + .and_then(Value::as_array) + .is_some_and(|items| !items.is_empty()); + Ok((value.is_object() && has_items).then_some(value)) } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] @@ -109,11 +139,13 @@ impl ImageModel { /// Most output slots one optimizer call is written for. pub const MAX_IMAGE_SLOTS: usize = 4; -/// Which model each output slot targets, in slot order. +/// Which model each output slot targets, in slot order, and whether the optimizer +/// also writes a MOSS-Image layout for a designed picture. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct ImageTargets { models: [ImageModel; MAX_IMAGE_SLOTS], len: usize, + moss_layout: bool, } impl Default for ImageTargets { @@ -132,9 +164,24 @@ impl ImageTargets { Self { models: [model; MAX_IMAGE_SLOTS], len: 1, + moss_layout: false, } } + /// These targets with the MOSS-Image layout route on or off. Off, a MOSS-Image slot + /// gets its prose prompt and negative prompt only. + #[must_use] + pub const fn with_moss_layout(mut self, enabled: bool) -> Self { + self.moss_layout = enabled; + self + } + + /// Whether a MOSS-Image slot also gets a layout for a designed picture. + #[must_use] + pub fn moss_layout(&self) -> bool { + self.moss_layout && self.models().contains(&ImageModel::Moss) + } + #[must_use] pub fn models(&self) -> &[ImageModel] { &self.models[..self.len] @@ -173,6 +220,7 @@ impl ImageTargets { out.models[out.len] = next.model_for_slot(index); out.len += 1; } + out.moss_layout = self.moss_layout || next.moss_layout; out } } @@ -289,13 +337,37 @@ pub fn part_image_prompt(text: &str) -> ImagePromptParts { #[must_use] pub fn apply_word_replacements(mut optimize: ImageOptimize) -> ImageOptimize { for output in &mut optimize.outputs { - *output = REPLACE_WORD_RE - .replace_all(output, replace_image_word) - .into_owned(); + *output = replace_image_words(output); + } + optimize.moss_negative = replace_image_words(&optimize.moss_negative); + 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") { + replace_image_words_in(caption); + } + if let Some(items) = layout.get_mut("items").and_then(Value::as_array_mut) { + for item in items { + if let Some(caption) = item.get_mut("caption") { + replace_image_words_in(caption); + } + } + } } optimize } +fn replace_image_words(text: &str) -> String { + REPLACE_WORD_RE + .replace_all(text, replace_image_word) + .into_owned() +} + +fn replace_image_words_in(value: &mut Value) { + if let Value::String(text) = value { + *text = replace_image_words(text); + } +} + #[must_use] pub fn normalize_nsfw_result(raw: &str) -> NsfwResult { let raw = raw.trim(); @@ -444,9 +516,7 @@ pub fn truncate_outputs_to_chars(outputs: &mut [String], max_chars: usize) { /// targets, and flags for the per-model rule blocks. fn optimizer_prompt_data(options: OptimizePromptOptions) -> Value { let variant_count = normalize_variant_count(options.variant_count); - let models: Vec = (0..variant_count) - .map(|index| options.targets.model_for_slot(index)) - .collect(); + let models = slot_models(options); let slots: Vec = models .iter() .enumerate() @@ -459,21 +529,47 @@ fn optimizer_prompt_data(options: OptimizePromptOptions) -> Value { "flux3": models.contains(&ImageModel::Flux3), "krea2": models.contains(&ImageModel::Krea2), "moss": models.contains(&ImageModel::Moss), + "moss_layout": options.targets.moss_layout(), }) } +/// The model each output slot of one optimizer call is written for. +fn slot_models(options: OptimizePromptOptions) -> Vec { + (0..normalize_variant_count(options.variant_count)) + .map(|index| options.targets.model_for_slot(index)) + .collect() +} + +/// 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. #[must_use] pub fn optimize_prompt_terminator_definition( - variant_count: usize, + options: OptimizePromptOptions, ) -> OptimizerTerminatorDefinition { + let moss = if !slot_models(options).contains(&ImageModel::Moss) { + MossFields::None + } else if options.targets.moss_layout() { + MossFields::NegativeAndLayout + } else { + MossFields::Negative + }; optimizer_terminator_definition( - variant_count, + options.variant_count, OPTIMIZE_PROMPT_TERMINATOR_TOOL_NAME, "Finalize image prompt optimization and return the structured payload.", true, + moss, ) } +/// The MOSS-Image fields an optimizer payload carries. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum MossFields { + None, + Negative, + NegativeAndLayout, +} + #[must_use] pub fn optimize_edit_prompt_terminator_definition( variant_count: usize, @@ -483,6 +579,7 @@ pub fn optimize_edit_prompt_terminator_definition( OPTIMIZE_EDIT_PROMPT_TERMINATOR_TOOL_NAME, "Finalize image edit prompt optimization and return the structured payload.", false, + MossFields::None, ) } @@ -518,6 +615,29 @@ where normalized } +/// Fewest words a real optimized prompt has; shorter slots are placeholders. +const MIN_PROMPT_WORDS: usize = 4; + +/// A grammar-constrained model sometimes fills the later slots of a multi-slot call +/// with a field name such as "nsfw_result" or "outputs[1] = " instead of a prompt. +/// Each such slot takes the nearest real prompt, the one before it first, so every +/// slot keeps its position and renders the request. +fn replace_placeholder_outputs(outputs: &mut [String]) { + let is_prompt = |output: &str| output.split_whitespace().count() >= MIN_PROMPT_WORDS; + for index in 0..outputs.len() { + if is_prompt(&outputs[index]) { + continue; + } + let donor = (0..index) + .rev() + .chain(index + 1..outputs.len()) + .find(|&other| is_prompt(&outputs[other])); + if let Some(donor) = donor { + outputs[index] = outputs[donor].clone(); + } + } +} + #[must_use] pub fn image_optimize_fallback(text: &str, variant_count: usize) -> ImageOptimize { ImageOptimize { @@ -545,7 +665,22 @@ pub fn normalize_image_optimize( ) -> ImageOptimize { optimized.input = optimized.input.trim().to_owned(); optimized.outputs = normalize_outputs(optimized.outputs, fallback, variant_count); + replace_placeholder_outputs(&mut optimized.outputs); + for output in &mut optimized.outputs { + // A model that escapes twice writes a literal backslash-n; the line break is meant. + if output.contains("\\n") { + *output = output.replace("\\n", "\n"); + } + } optimized.aspect_ratio = optimized.aspect_ratio.trim().to_owned(); + optimized.moss_negative = optimized.moss_negative.trim().to_owned(); + if optimized.nsfw_result == NsfwResult::Forbidden { + optimized.moss_negative.clear(); + optimized.moss_design = false; + } + if !optimized.moss_design { + optimized.moss_layout = None; + } if optimized.input.is_empty() { optimized.input = fallback.trim().to_owned(); } @@ -661,6 +796,7 @@ fn optimizer_terminator_definition( name: &'static str, description: &'static str, include_aspect_ratio: bool, + moss: MossFields, ) -> OptimizerTerminatorDefinition { let count = normalize_variant_count(variant_count); let mut properties = serde_json::Map::from_iter([ @@ -702,6 +838,37 @@ fn optimizer_terminator_definition( ); required.push("aspect_ratio"); } + // Required, so the model decides each one: a grammar-constrained model skips + // optional fields. Schema keys serialize in alphabetical order, which a + // grammar-constrained model writes in: the `slot_` prefix keeps these after the + // prompts, and the design decision comes before the layout it gates. + if moss == MossFields::NegativeAndLayout { + properties.insert( + "slot_moss_design".to_owned(), + json!({ + "type": "boolean", + "description": "True only for a designed image where placement matters, such as a poster, card, cover, menu, infographic, or meme.", + }), + ); + properties.insert( + "slot_moss_layout".to_owned(), + json!({ + "anyOf": [moss_layout_schema(), { "type": "null" }], + "description": "MOSS-Image layout when slot_moss_design is true, otherwise null.", + }), + ); + required.extend(["slot_moss_design", "slot_moss_layout"]); + } + if moss != MossFields::None { + properties.insert( + "slot_moss_negative".to_owned(), + json!({ + "type": "string", + "description": "Up to three short comma-separated things the request rules out for the MOSS-Image picture; empty when it rules nothing out.", + }), + ); + required.push("slot_moss_negative"); + } OptimizerTerminatorDefinition { name, @@ -714,6 +881,51 @@ fn optimizer_terminator_definition( } } +/// MOSS-Image layout in the official `summary` / `items` vocabulary, with boxes as +/// `[y_min, x_min, y_max, x_max]` on a 0-1000 grid instead of pixels: the optimizer +/// picks the aspect ratio, and draw-api the canvas. +fn moss_layout_schema() -> Value { + let strings = json!({ "type": "array", "items": { "type": "string" } }); + json!({ + "type": "object", + "properties": { + "summary": { + "type": "object", + "additionalProperties": false, + "properties": { + "caption": { "type": "string" }, + "meta": strings.clone(), + "canvas": { "type": "string", "enum": ["single", "multi_panel", "collage"] }, + "palette": strings, + }, + "required": ["caption"], + }, + "items": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { "type": "string", "enum": ["text", "object", "character"] }, + "caption": { "type": "string" }, + "text": { "type": "string" }, + "bbox": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "items": { "type": "integer" }, + }, + }, + "required": ["label", "caption", "bbox"], + }, + }, + }, + "required": ["summary", "items"], + "additionalProperties": false, + }) +} + fn image_replacement_word(word: &str) -> Option<&'static str> { if word.eq_ignore_ascii_case("roach") || word.eq_ignore_ascii_case("plotva") @@ -969,6 +1181,285 @@ mod tests { assert!(vip.contains("**Krea 2 Turbo**")); assert!(vip.contains("**FLUX.3 [klein]**")); assert!(vip.contains("**MOSS-Image**")); + 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("written as `\\n` in the JSON string")); + assert!(vip.contains("**MOSS-Image negative prompt**")); + assert!(!vip.contains("slot_moss_layout")); + assert!(!vip.contains("slot_moss_design")); + assert!(!vip.contains("{{")); + + let layout = render_image_optimizer_prompt(OptimizePromptOptions { + variant_count: 1, + targets: ImageTargets::MOSS.with_moss_layout(true), + }) + .expect("render moss layout"); + assert!( + layout.contains("`slot_moss_design`, `slot_moss_layout`, and `slot_moss_negative`") + ); + assert!(layout.contains("**MOSS-Image layout**")); + assert!(layout.contains("Use the same breaks in `slot_moss_layout`.")); + assert!(layout.contains("\"slot_moss_design\": true, \"slot_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")); + } + + #[test] + 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), + }) + .input_schema; + assert_eq!( + prose["required"], + json!([ + "input", + "outputs", + "nsfw_result", + "aspect_ratio", + "slot_moss_negative" + ]) + ); + assert!(prose["properties"]["slot_moss_layout"].is_null()); + + // 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( + 1, + ImageTargets::MOSS.with_moss_layout(true), + 1, + ), + }) + .input_schema; + assert_eq!( + vip["required"], + json!([ + "input", + "outputs", + "nsfw_result", + "aspect_ratio", + "slot_moss_design", + "slot_moss_layout", + "slot_moss_negative" + ]) + ); + assert_eq!(vip["properties"]["slot_moss_design"]["type"], "boolean"); + let layout = &vip["properties"]["slot_moss_layout"]["anyOf"]; + assert_eq!(layout[1], json!({ "type": "null" })); + assert_eq!(layout[0]["additionalProperties"], false); + let bbox = &layout[0]["properties"]["items"]["items"]["properties"]["bbox"]; + assert_eq!( + (bbox["minItems"].clone(), bbox["maxItems"].clone()), + (json!(4), json!(4)) + ); + // The grammar writes keys in serialized order: the prompts before the MOSS fields. + 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("outputs") < position("slot_moss_design")); + assert!(position("slot_moss_design") < position("slot_moss_layout")); + + // Without a MOSS-Image slot the layout flag adds nothing. + let flux3 = optimize_prompt_terminator_definition(OptimizePromptOptions { + variant_count: 1, + targets: ImageTargets::FLUX3.with_moss_layout(true), + }); + assert!(flux3.input_schema["properties"]["slot_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()); + } + + #[test] + fn examples_write_their_keys_in_the_order_the_grammar_does() { + // A grammar-constrained model writes the keys in the schema's serialized order. + // 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::MOSS, + ImageTargets::MOSS.with_moss_layout(true), + ] { + let options = OptimizePromptOptions { + variant_count: 1, + targets, + }; + let prompt = render_image_optimizer_prompt(options).expect("render"); + let schema = optimize_prompt_terminator_definition(options).input_schema; + let keys: Vec<&String> = schema["properties"] + .as_object() + .expect("properties") + .keys() + .collect(); + let mut examples = 0; + for line in prompt.lines().filter(|line| line.starts_with("{\"")) { + let example: Value = serde_json::from_str(line).expect("example parses"); + assert_eq!( + example.as_object().map(serde_json::Map::len), + Some(keys.len()), + "{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}"); + examples += 1; + } + assert!(examples > 0); + } + } + + #[test] + fn moss_layout_decodes_only_for_a_designed_image() { + let layout = json!({ + "items": [{"bbox": [70, 120, 330, 880], "caption": "Bold cream letters.", "label": "text", "text": "Джаз\nу моря"}], + "summary": {"caption": "A flat vector jazz poster."}, + }); + let payload = |design: bool, layout: &Value| { + json!({ + "input": "постер", + "outputs": ["A flat vector illustration poster for a jazz festival."], + "nsfw_result": "safe", + "aspect_ratio": "3:4", + "slot_moss_design": design, + "slot_moss_layout": layout, + "slot_moss_negative": " people ", + }) + .to_string() + }; + let designed = decode_image_optimize_payload(&payload(true, &layout), "постер", 1) + .expect("decode designed"); + assert!(designed.moss_design); + assert_eq!(designed.moss_layout.as_ref(), Some(&layout)); + assert_eq!(designed.moss_negative, "people"); + assert_eq!( + designed + .moss_layout + .as_ref() + .and_then(|l| l.pointer("/items/0/text")), + Some(&json!("Джаз\nу моря")) + ); + + let scene = decode_image_optimize_payload(&payload(false, &layout), "постер", 1) + .expect("decode scene"); + assert_eq!(scene.moss_layout, None); + + // A malformed layout degrades to the prose prompt instead of failing the payload. + for broken in [json!("layout"), json!({"items": []}), json!(null)] { + let decoded = decode_image_optimize_payload(&payload(true, &broken), "постер", 1) + .expect("decode broken layout"); + assert_eq!(decoded.moss_layout, None); + assert_eq!(decoded.outputs.len(), 1); + } + + let legacy = decode_image_optimize_payload( + r#"{"input": "cat", "outputs": ["A cat on a red sofa."], "nsfw_result": "safe"}"#, + "cat", + 1, + ) + .expect("decode legacy"); + assert!(!legacy.moss_design); + assert_eq!(legacy.moss_layout, None); + assert!(legacy.moss_negative.is_empty()); + } + + #[test] + fn placeholder_slots_take_the_nearest_real_prompt() { + let first = "A photograph of a cozy cafe on a rainy evening."; + let third = "A photograph of a cafe sign reading Plotva at dusk."; + let payload = json!({ + "input": "кафе", + "outputs": [first, "nsfw_result", third], + "nsfw_result": "safe", + }) + .to_string(); + let decoded = decode_image_optimize_payload(&payload, "кафе", 3).expect("decode"); + assert_eq!(decoded.outputs, [first, first, third]); + + let payload = json!({ + "input": "кафе", + "outputs": ["outputs[1] = ", "safe", third], + "nsfw_result": "safe", + }) + .to_string(); + let decoded = decode_image_optimize_payload(&payload, "кафе", 3).expect("decode"); + assert_eq!(decoded.outputs, [third, third, third]); + + let payload = json!({ + "input": "меню", + "outputs": ["A chalkboard menu reading \"капучино 250\\nлатте 280\" in white chalk."], + "nsfw_result": "safe", + }) + .to_string(); + let decoded = decode_image_optimize_payload(&payload, "меню", 1).expect("decode"); + assert_eq!( + decoded.outputs, + ["A chalkboard menu reading \"капучино 250\nлатте 280\" in white chalk."] + ); + + // Nothing to borrow from: the slots stay as the model wrote them. + let decoded = decode_image_optimize_payload( + r#"{"input": "кот", "outputs": ["кот"], "nsfw_result": "safe"}"#, + "кот", + 1, + ) + .expect("decode"); + assert_eq!(decoded.outputs, ["кот"]); + } + + #[test] + fn forbidden_requests_drop_the_moss_fields() { + let payload = json!({ + "input": "x", + "outputs": ["Blocked: forbidden CSAM request."], + "nsfw_result": "forbidden", + "slot_moss_design": true, + "slot_moss_layout": {"items": [{"bbox": [0, 0, 10, 10], "caption": "c", "label": "object"}], "summary": {"caption": "c"}}, + "slot_moss_negative": "hat", + }) + .to_string(); + let decoded = decode_image_optimize_payload(&payload, "x", 1).expect("decode"); + assert!(!decoded.moss_design); + assert_eq!(decoded.moss_layout, None); + assert!(decoded.moss_negative.is_empty()); + } + + #[test] + fn word_replacements_reach_layout_captions_but_not_lettering() { + let optimized = ImageOptimize { + outputs: vec!["plotva swims.".to_owned()], + moss_negative: "roach".to_owned(), + moss_design: true, + moss_layout: Some(json!({ + "items": [{"caption": "plotva on a plate.", "label": "text", "text": "plotva", "bbox": [0, 0, 10, 10]}], + "summary": {"caption": "roach poster."}, + })), + ..ImageOptimize::default() + }; + let replaced = apply_word_replacements(optimized); + assert_eq!(replaced.outputs, ["roach-fish swims."]); + assert_eq!(replaced.moss_negative, "roach-fish"); + let layout = replaced.moss_layout.expect("layout"); + assert_eq!( + layout.pointer("/summary/caption"), + Some(&json!("roach-fish poster.")) + ); + assert_eq!( + layout.pointer("/items/0/caption"), + Some(&json!("roach-fish on a plate.")) + ); + assert_eq!(layout.pointer("/items/0/text"), Some(&json!("plotva"))); } #[test] @@ -997,7 +1488,11 @@ mod tests { #[test] fn optimizer_schema_strings_carry_no_length_bound() { - let schema = optimize_prompt_terminator_definition(1).input_schema; + let schema = optimize_prompt_terminator_definition(OptimizePromptOptions { + variant_count: 1, + ..OptimizePromptOptions::default() + }) + .input_schema; assert!(schema["properties"]["outputs"]["items"]["maxLength"].is_null()); assert_eq!(schema["properties"]["outputs"]["maxItems"], 1); } @@ -1076,7 +1571,10 @@ mod tests { assert!(edit_prompt.contains("non-sexual children")); assert!(edit_prompt.contains("minor sexual content")); - let tool = optimize_prompt_terminator_definition(2); + let tool = optimize_prompt_terminator_definition(OptimizePromptOptions { + variant_count: 2, + ..OptimizePromptOptions::default() + }); assert_eq!(tool.name, OPTIMIZE_PROMPT_TERMINATOR_TOOL_NAME); assert_eq!(tool.input_schema["type"], "object"); assert_eq!(tool.input_schema["properties"]["outputs"]["minItems"], 2); diff --git a/prompts/image/optimizer.prompt b/prompts/image/optimizer.prompt index d19ca81e..f23c52e7 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 `input`, `outputs`, `nsfw_result`, and `aspect_ratio`, with no text, labels, or code fences around it. +1. Return exactly one JSON object with the fields `aspect_ratio`, `input`, `nsfw_result`, and `outputs`, in that order{{#if moss_layout}}, plus the MOSS-Image fields `slot_moss_design`, `slot_moss_layout`, and `slot_moss_negative`{{else}}{{#if moss}}, plus the MOSS-Image field `slot_moss_negative`{{/if}}{{/if}}, 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 `\"`. @@ -12,6 +12,7 @@ You are the prompt upsampler of image models. You turn one user request, written - `outputs[{{this.index}}]` is rendered by {{this.model}}; follow the {{this.model}} rules for it. {{/each}} {{#if multi}}All prompts describe the same image idea. They differ only where their models' rules differ and in secondary details. +`outputs` is one array of {{variant_count}} complete prompts in slot order. Every element is a full prompt, never a field name, a slot label, or a placeholder; each example below shows the answer for one slot. {{/if}} ### READ THE REQUEST @@ -35,9 +36,9 @@ Choose by how much the request already fixes, not by its length: 3. Make actions visible through pose, gaze, contact points, and support, and keep who acts on whom unchanged even in an impossible scene. Describe major shapes and defining features before small details, and keep light sources, shadows, colours, and time of day consistent. Time of day is light, not written text. 4. Give each person in focus visible specifics: an age range in words, build, hair, each garment with its colour and material, expression, and pose. Do not assign an ethnicity, skin tone, nationality, name, or life story the request or Context does not give, and add no extra main characters or events. 5. Keep every count across the whole image. Give the members of a small exact group distinct positions or features so each can be counted; describe a crowd by its pattern and a few details. -6. Text that must appear in the image goes in double quotes exactly as the user wrote it: same language, script, case, punctuation, and line breaks, never translated, transliterated, or corrected. Say where it sits and how it is lettered, and for Cyrillic text say "Cyrillic letters". Write display text the request clearly implies, such as a poster saying Happy New Year. Never add other text, captions, labels, or translations; names, titles, and quoted descriptions are not lettering unless the user asks for them to be written. Distant or out-of-focus signs carry no readable letters. +6. Text that must appear in the image goes in double quotes exactly as the user wrote it: same language, script, case, and punctuation, with the user's line breaks kept, never translated, transliterated, or corrected. Say where it sits and how it is lettered, and for Cyrillic text say "Cyrillic letters". Write display text the request clearly implies, such as a poster saying Happy New Year. Never add other text, captions, labels, or translations; names, titles, and quoted descriptions are not lettering unless the user asks for them to be written. Distant or out-of-focus signs carry no readable letters. 7. Never invent names, brands, logos, or celebrities; keep the ones the user named. -8. The image models have no negative prompt, so describe what is there, never what is missing: "an empty street", "a clean white wall". Text after a `|` in the request lists things the user does not want; turn each into a positive description. Never output a `|` or a list of negatives. +8. The prompts in `outputs` describe what is there, never what is missing: "an empty street", "a clean white wall". Text after a `|` in the request lists things the user does not want; turn each into a positive description. Never put a `|` or a list of negatives in `outputs`. 9. Leave out quality slogans such as "masterpiece", "8k", "stunning", or "high quality", invented camera settings, and judgments of beauty or mood; turn a mood into observable facts. Add depth of field, bokeh, or blur only when the user asks for them or the treatment implies them. 10. Ignore seeds and other generation settings in the request. @@ -68,7 +69,23 @@ MOSS-Image learned from detailed captions written by a vision-language recaption 2. Open with the medium and the composition, such as "A photograph of ..." or "A watercolour illustration of ...", then the key subjects and their actions, the setting, the colours, the lighting, and the style. 3. Place each subject with spatial words (left, right, center, foreground, background, above, behind) and keep one consistent order. 4. For every piece of text in the image, give the exact wording in double quotes and its layout: position, line order, alignment, size relative to other text, letterform, and colour. -5. Write prose only: no tag lists, quality tags, or score tags. +5. Text set on several lines carries a line break at each break inside its quotes, written as `\n` in the JSON string, as in `\"Джаз\nу моря\"`. Keep the user's breaks. Set a title, slogan, caption, or sign of four or more words on two or three balanced lines, breaking between words where a designer would; keep shorter text on one line.{{#if moss_layout}} Use the same breaks in `slot_moss_layout`.{{/if}} +6. Write prose only: no tag lists, quality tags, or score tags. + +**MOSS-Image negative prompt** +MOSS-Image also reads `slot_moss_negative`, a short list of things to steer away from. +1. List what the request excludes as up to three short English phrases, comma-separated: the things after a `|`, then what it rules out in words, so "без шапки" gives "hat" and "пустой пляж" gives "people". When it excludes nothing, write an empty string. +2. Name each unwanted thing itself, never with "no" or "without", and never anything the picture contains or a quality word such as "blurry". +{{#if moss_layout}} + +**MOSS-Image layout** +MOSS-Image can also draw from `slot_moss_layout`: a summary of the whole picture plus a box for each block of text and each main object. +1. Set `slot_moss_design` to true only for a designed image where placement matters: a poster, flyer, banner, cover, card, invitation, menu, label or packaging, infographic, chart, diagram, screen or interface, comic or meme with captions, or a picture with two or more separate blocks of text. Set it to false for every other request: a photograph, painting, or illustration of a scene is not a designed image, even with a sign, a shop name, or a label in it. +2. When `slot_moss_design` is false, `slot_moss_layout` is null. Otherwise the layout shows the same picture as the MOSS-Image prompt in `outputs`: the same subjects, text, colours, and composition. +3. `summary.caption` describes the whole picture in 40 to 120 words: medium, style, background, palette, light, and the overall arrangement, without repeating the wording of the text. `summary.meta` holds one to three short medium or genre tags, such as "poster" or "flat vector illustration". `summary.canvas` is "single", "multi_panel" for comic panels, or "collage". +4. `items` lists every block of text and every main object or character, from top to bottom and left to right, at most twelve. Each has a `label` ("text", "object", or "character"), a `caption` of 10 to 40 words on its look (for text: weight, style, case, size, colour, and alignment), and a `bbox`. Only a text item has `text`: the exact wording with `\n` at each line break. Give each separately placed block of text its own item, and never repeat a text in two items. +5. `bbox` is `[y_min, x_min, y_max, x_max]` on a 0 to 1000 grid over the whole picture in the chosen `aspect_ratio`: y runs top to bottom, x left to right, and `[0, 0, 1000, 1000]` is the full frame. The grid stretches with the frame, so on a wide picture one unit of x is longer than one unit of y. Size each text box for its words at a readable size, larger for longer text, and keep margins from the edges. Boxes may overlap where elements overlap, such as a title over a background picture; a box marks placement and draws no frame. +{{/if}} {{/if}} ### CONTEXT @@ -106,57 +123,57 @@ Each example shows one request and the answer for a single FLUX.3 [klein] slot. Request: нічнии потяг мчить через молочнии шлях, зима -{"input": "нічнии потяг мчить через молочнии шлях, зима", "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."], "nsfw_result": "safe", "aspect_ratio": "3:2"} +{"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."]} Request: “i’m here, typography” -{"input": "“i’m here, typography”", "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."], "nsfw_result": "safe", "aspect_ratio": "3:4"} +{"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: Greek statue of a man tripping over a cat. -{"input": "Greek statue of a man tripping over a cat.", "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."], "nsfw_result": "safe", "aspect_ratio": "4:3"} +{"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."]} Request: a cat with four eyes -{"input": "a cat with four eyes", "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."], "nsfw_result": "safe", "aspect_ratio": "3:4"} +{"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: a snail on the rim of a flower pot -{"input": "a snail on the rim of a flower pot", "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."], "nsfw_result": "safe", "aspect_ratio": "4:3"} +{"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: a meal -{"input": "a meal", "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."], "nsfw_result": "safe", "aspect_ratio": "4:3"} +{"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."]} 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. -{"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.", "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."], "nsfw_result": "safe", "aspect_ratio": "3:4"} +{"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: the interior of a modern contemporary studio apartment but the ceiling is the night sky with bright starts and the milky way visible. -{"input": "the interior of a modern contemporary studio apartment but the ceiling is the night sky with bright starts and the milky way visible.", "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."], "nsfw_result": "safe", "aspect_ratio": "16:9"} +{"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: плакат с надписью С новым годом 2:3 -{"input": "плакат с надписью С новым годом 2:3", "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."], "nsfw_result": "safe", "aspect_ratio": "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."]} Request: обнажённая натурщица, картина маслом в стиле Ренессанса -{"input": "обнажённая натурщица, картина маслом в стиле Ренессанса", "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."], "nsfw_result": "adult", "aspect_ratio": "4:3"} +{"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."]} Request: sexual depiction of a prepubescent child -{"input": "sexual depiction of a prepubescent child", "outputs": ["Blocked: forbidden CSAM request."], "nsfw_result": "forbidden", "aspect_ratio": "1:1"} +{"aspect_ratio": "1:1", "input": "sexual depiction of a prepubescent child", "nsfw_result": "forbidden", "outputs": ["Blocked: forbidden CSAM request."]} {{/if}} {{#if krea2}} @@ -165,7 +182,7 @@ The next example shows the answer for a single Krea 2 Turbo slot. Request: кот-астронавт на луне -{"input": "кот-астронавт на луне", "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."], "nsfw_result": "safe", "aspect_ratio": "16:9"} +{"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}} @@ -174,6 +191,11 @@ The next example shows the answer for a single MOSS-Image slot. Request: записка на холодильнике: купить молоко и хлеб -{"input": "записка на холодильнике: купить молоко и хлеб", "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."], "nsfw_result": "safe", "aspect_ratio": "3:4"} +{"aspect_ratio": "3:4", "input": "записка на холодильнике: купить молоко и хлеб", "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."], {{#if moss_layout}}"slot_moss_design": false, "slot_moss_layout": null, {{/if}}"slot_moss_negative": ""} + + + +Request: постер джазового фестиваля «Джаз у моря», 12–14 июля, Сочи, без людей +{"aspect_ratio": "3:4", "input": "постер джазового фестиваля «Джаз у моря», 12–14 июля, Сочи, без людей", "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."], {{#if moss_layout}}"slot_moss_design": true, "slot_moss_layout": {"items": [{"bbox": [70, 120, 330, 880], "caption": "Large bold rounded cream display letters on two centered lines, the second line slightly smaller.", "label": "text", "text": "Джаз\nу моря"}, {"bbox": [340, 300, 760, 700], "caption": "A golden saxophone standing upright on a low sand dune at the waterline, bell turned right, keys with small highlights.", "label": "object"}, {"bbox": [790, 230, 860, 770], "caption": "Medium white clean sans-serif letters on one centered line.", "label": "text", "text": "12–14 июля"}, {"bbox": [880, 380, 930, 620], "caption": "Small widely spaced teal letters on one centered line.", "label": "text", "text": "Сочи"}], "summary": {"canvas": "single", "caption": "A flat vector jazz festival poster: a calm teal and deep blue sea at dusk in the lower half, a warm orange and violet sky above, a low sun on the horizon, a golden saxophone on a sand dune in the center, smooth gradients, crisp edges, and generous empty space around large cream and white lettering.", "meta": ["poster", "flat vector illustration"]}}, {{/if}}"slot_moss_negative": "people"} {{/if}}